diff options
Diffstat (limited to 'libs')
270 files changed, 7780 insertions, 1304 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 4d73c20fe39f..ca3d8d18db83 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -375,7 +375,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { return TaskFragmentAnimationParams.DEFAULT; } return new TaskFragmentAnimationParams.Builder() - .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + // TODO(b/263047900): Update extensions API. + // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) .build(); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index faf7c3999402..b5c32bbe78fa 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -854,7 +854,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return new SplitAttributes.Builder() .setSplitType(splitTypeToUpdate) .setLayoutDirection(splitAttributes.getLayoutDirection()) - .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + // TODO(b/263047900): Update extensions API. + // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) .build(); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index 9607b78bacf0..60beb0b7f0a4 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -17,6 +17,7 @@ package androidx.window.extensions; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + import static com.google.common.truth.Truth.assertThat; import android.app.ActivityTaskManager; @@ -69,6 +70,7 @@ public class WindowExtensionsTest { .isEqualTo(SplitAttributes.LayoutDirection.LOCALE); assertThat(splitAttributes.getSplitType()) .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f)); - assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0); + // TODO(b/263047900): Update extensions API. + // assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 50cfd941adb3..4c2433fab2f8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -443,7 +443,8 @@ public class OverlayPresentationTest { assertThat(taskContainer.getTaskFragmentContainers()).containsExactly(overlayContainer); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(Configuration.EMPTY, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); mSplitController.updateOverlayContainer(mTransaction, overlayContainer); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 02031a67e7e3..8c274a26177d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -1139,7 +1139,8 @@ public class SplitControllerTest { public void testOnTransactionReady_taskFragmentParentInfoChanged() { final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */); transaction.addChange(new TaskFragmentTransaction.Change( TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) .setTaskId(TASK_ID) diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index e56c8ab686e7..7b77235f66f7 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -79,14 +79,16 @@ public class TaskContainerTest { configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertEquals(WINDOWING_MODE_FREEFORM, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); @@ -106,13 +108,15 @@ public class TaskContainerTest { configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertFalse(taskContainer.isInPictureInPicture()); configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertTrue(taskContainer.isInPictureInPicture()); } diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index fd4522e02438..5ad144d50b87 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -160,6 +160,7 @@ android_library { "kotlinx-coroutines-core", "iconloader_base", "com_android_wm_shell_flags_lib", + "com.android.window.flags.window-aconfig-java", "WindowManager-Shell-proto", "dagger2", "jsr330", diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index c366ccd235db..4511f3b91c5c 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -42,3 +42,18 @@ flag { description: "Enables PiP UI state callback on entering" bug: "303718131" } + +flag { + name: "enable_pip2_implementation" + namespace: "multitasking" + description: "Enables the new implementation of PiP (PiP2)" + bug: "290220798" + is_fixed_read_only: true +} + +flag { + name: "enable_left_right_split_in_portrait" + namespace: "multitasking" + description: "Enables left/right split in portrait" + bug: "291018646" +} diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml deleted file mode 100644 index ef3006042261..000000000000 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2022 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 android:shape="rectangle" - android:tintMode="multiply" - android:tint="@color/decor_title_color" - xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="@android:color/white" /> -</shape> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml index c525a297b2e0..85bf2c1e4dca 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml @@ -21,8 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" - android:orientation="horizontal" - android:background="@drawable/desktop_mode_decor_title"> + android:orientation="horizontal"> <LinearLayout android:id="@+id/open_menu_button" @@ -45,7 +44,6 @@ android:layout_width="0dp" android:layout_height="20dp" android:minWidth="80dp" - android:textColor="@color/desktop_mode_caption_app_name_dark" android:textAppearance="@android:style/TextAppearance.Material.Title" android:textSize="14sp" android:textFontWeight="500" @@ -62,7 +60,6 @@ android:layout_height="16dp" android:contentDescription="@string/expand_menu_text" android:src="@drawable/ic_baseline_expand_more_24" - android:tint="@color/desktop_mode_caption_expand_button_dark" android:background="@null" android:scaleType="fitCenter" android:clickable="false" @@ -87,8 +84,7 @@ android:src="@drawable/decor_desktop_mode_maximize_button_dark" android:scaleType="fitCenter" android:gravity="end" - android:background="@null" - android:tint="@color/desktop_mode_caption_maximize_button_dark"/> + android:background="@null"/> <ImageButton android:id="@+id/close_window" @@ -100,6 +96,5 @@ android:src="@drawable/decor_close_button_dark" android:scaleType="fitCenter" android:gravity="end" - android:background="@null" - android:tint="@color/desktop_mode_caption_close_button_dark"/> + android:background="@null"/> </com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml index 7638132d6562..cec7ee233236 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml @@ -20,8 +20,7 @@ android:id="@+id/desktop_mode_caption" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center_horizontal" - android:background="@drawable/desktop_mode_decor_title"> + android:gravity="center_horizontal"> <ImageButton android:id="@+id/caption_handle" @@ -33,5 +32,4 @@ tools:tint="@color/desktop_mode_caption_handle_bar_dark" android:scaleType="fitXY" android:background="?android:selectableItemBackground"/> - </com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml deleted file mode 100644 index d732b01ce106..000000000000 --- a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2015 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.legacysplitscreen.DividerView - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_height="match_parent" - android:layout_width="match_parent"> - - <View - style="@style/DockedDividerBackground" - android:id="@+id/docked_divider_background" - android:background="@color/split_divider_background"/> - - <com.android.wm.shell.legacysplitscreen.MinimizedDockShadow - style="@style/DockedDividerMinimizedShadow" - android:id="@+id/minimized_dock_shadow" - android:alpha="0"/> - - <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.legacysplitscreen.DividerView> diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml index e3be700469a7..db35c8c57456 100644 --- a/libs/WindowManager/Shell/res/layout/split_divider.xml +++ b/libs/WindowManager/Shell/res/layout/split_divider.xml @@ -24,17 +24,16 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <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:layout_height="match_parent" + android:layout_width="match_parent" + android:layout_gravity="center" android:contentDescription="@string/accessibility_divider" android:background="@null"/> <com.android.wm.shell.common.split.DividerRoundedCorner + android:id="@+id/docked_divider_rounded_corner" android:layout_width="match_parent" android:layout_height="match_parent"/> diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index 2471cbac0c84..90b13cdc79b4 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Laat los"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"App sal dalk nie met verdeelde skerm werk nie"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App steun nie verdeelde skerm nie"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Hierdie app kan net in 1 venster oopgemaak word"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Program sal dalk nie op \'n sekondêre skerm werk nie."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Program steun nie begin op sekondêre skerms nie."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Skermverdeler"</string> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index 387478c66267..478585aace68 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"መተግበሪያ ከተከፈለ ማያ ገፅ ጋር ላይሠራ ይችላል"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"መተግበሪያው የተከፈለ ማያ ገጽን አይደግፍም"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ይህ መተግበሪያ መከፈት የሚችለው በ1 መስኮት ብቻ ነው"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"መተግበሪያ በሁለተኛ ማሳያ ላይ ላይሠራ ይችላል።"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"መተግበሪያ በሁለተኛ ማሳያዎች ላይ ማስጀመርን አይደግፍም።"</string> <string name="accessibility_divider" msgid="6407584574218956849">"የተከፈለ የማያ ገፅ ከፋይ"</string> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index fb92ed7650de..b2a522c89f8c 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"إظهار"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"قد لا يعمل التطبيق بشكل سليم في وضع تقسيم الشاشة."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"لا يعمل التطبيق في وضع تقسيم الشاشة."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"لا يمكن فتح هذا التطبيق إلا في نافذة واحدة."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"قد لا يعمل التطبيق على شاشة عرض ثانوية."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"لا يمكن تشغيل التطبيق على شاشات عرض ثانوية."</string> <string name="accessibility_divider" msgid="6407584574218956849">"أداة تقسيم الشاشة"</string> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 5e44e4723252..897c38f66e4b 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"দেখুৱাওক"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"এপ্টোৱে বিভাজিত স্ক্ৰীনৰ সৈতে কাম নকৰিব পাৰে"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"এপ্টোৱে বিভাজিত স্ক্ৰীন সমৰ্থন নকৰে"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"এই এপ্টো কেৱল ১ খন ৱিণ্ড’ত খুলিব পাৰি"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"গৌণ ডিছপ্লেত এপে সঠিকভাৱে কাম নকৰিব পাৰে।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"গৌণ ডিছপ্লেত এপ্ লঞ্চ কৰিব নোৱাৰি।"</string> <string name="accessibility_divider" msgid="6407584574218956849">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 4e2f9b98fba7..4854e0db7ed5 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Güvənli məkandan çıxarın"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Tətbiq bölünmüş ekranda işləməyə bilər"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Tətbiq bölünmüş ekranı dəstəkləmir"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Bu tətbiq yalnız 1 pəncərədə açıla bilər"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Tətbiq ikinci ekranda işləməyə bilər."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Tətbiq ikinci ekranda başlamağı dəstəkləmir."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Bölünmüş ekran ayırıcısı"</string> 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 990aed8af763..cac4e67b1cfc 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Uklonite iz tajne memorije"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikacija možda neće raditi sa podeljenim ekranom."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikacija ne podržava podeljeni ekran."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ova aplikacija može da se otvori samo u jednom prozoru"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionisati na sekundarnom ekranu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Razdelnik podeljenog ekrana"</string> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index febb0646968d..cac76df15910 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Паказаць"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Праграма можа не працаваць у рэжыме падзеленага экрана"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Праграма не падтрымлівае рэжым падзеленага экрана"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Гэту праграму можна адкрыць толькі ў адным акне"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Праграма можа не працаваць на дадатковых экранах."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Праграма не падтрымлівае запуск на дадатковых экранах."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Раздзяляльнік падзеленага экрана"</string> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 8bbdf9174458..ac9a20806b72 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Отмяна на съхраняването"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Приложението може да не работи в режим на разделен екран"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Приложението не поддържа разделен екран"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Това приложение може да се отвори само в 1 прозорец"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Възможно е приложението да не работи на алтернативни дисплеи."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложението не поддържа използването на алтернативни дисплеи."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Разделител в режима за разделен екран"</string> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 4678b174685c..3b83dcb461ee 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"আনস্ট্যাস করুন"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"স্প্লিট স্ক্রিনে এই অ্যাপ নাও কাজ করতে পারে"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"স্প্লিট স্ক্রিনে এই অ্যাপ কাজ করে না"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"এই অ্যাপটি শুধুমাত্র ১টি উইন্ডোতে খোলা যেতে পারে"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"সেকেন্ডারি ডিসপ্লেতে অ্যাপটি কাজ নাও করতে পারে।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"সেকেন্ডারি ডিসপ্লেতে অ্যাপ লঞ্চ করা যাবে না।"</string> <string name="accessibility_divider" msgid="6407584574218956849">"স্প্লিট স্ক্রিন বিভাজক"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 076a17d4973e..813d163d07fe 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vađenje iz stasha"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikacija možda neće funkcionirati na podijeljenom ekranu"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikacija ne podržava podijeljeni ekran"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ova aplikacija se može otvoriti samo u 1 prozoru"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće raditi na sekundarnom ekranu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim ekranima."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Razdjelnik podijeljenog ekrana"</string> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 311599824779..d00c50bb0294 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Deixa d\'amagar"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"És possible que l\'aplicació no funcioni amb la pantalla dividida"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"L\'aplicació no admet la pantalla dividida"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Aquesta aplicació només pot obrir-se en 1 finestra"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"És possible que l\'aplicació no funcioni en una pantalla secundària."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'aplicació no es pot obrir en pantalles secundàries."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Separador de pantalla dividida"</string> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index fe8a7eed7254..40132e4f67f8 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušit uložení"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikace v režimu rozdělené obrazovky nemusí fungovat"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikace nepodporuje režim rozdělené obrazovky"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Tuto aplikaci lze otevřít jen v jednom okně"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikace na sekundárním displeji nemusí fungovat."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikace nepodporuje spuštění na sekundárních displejích."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Čára rozdělující obrazovku"</string> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 81c5b228c159..6e9738dc7398 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vis"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Appen fungerer muligvis ikke i opdelt skærm"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Appen understøtter ikke opdelt skærm"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Denne app kan kun åbnes i 1 vindue"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer muligvis ikke på sekundære skærme."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke åbnes på sekundære skærme."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Adskiller til opdelt skærm"</string> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 4b9556d4515f..5da224d35360 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Aus Stash entfernen"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Die App funktioniert im Splitscreen-Modus unter Umständen nicht"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Splitscreen wird in dieser App nicht unterstützt"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Diese App kann nur in einem einzigen Fenster geöffnet werden"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Die App funktioniert auf einem sekundären Display möglicherweise nicht."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Die App unterstützt den Start auf sekundären Displays nicht."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Bildschirmteiler"</string> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index 907ef38b5c3c..822b5526c6fd 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Κατάργηση απόκρυψης"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Η εφαρμογή ενδέχεται να μην λειτουργεί με διαχωρισμό οθόνης."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Η εφαρμογή δεν υποστηρίζει διαχωρισμό οθόνης."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Αυτή η εφαρμογή μπορεί να ανοίξει μόνο σε ένα παράθυρο"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Η εφαρμογή ίσως να μην λειτουργήσει σε δευτερεύουσα οθόνη."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Η εφαρμογή δεν υποστηρίζει την εκκίνηση σε δευτερεύουσες οθόνες."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Διαχωριστικό οθόνης"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 843c8ed3090b..76464b398f89 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"App may not work with split screen"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App does not support split screen"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"This app can only be opened in one window"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 0d9d09a0465f..c0c46cd608ee 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"App may not work with split screen"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App does not support split screen"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"This app can only be opened in 1 window"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 843c8ed3090b..76464b398f89 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"App may not work with split screen"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App does not support split screen"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"This app can only be opened in one window"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 843c8ed3090b..76464b398f89 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"App may not work with split screen"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App does not support split screen"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"This app can only be opened in one window"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 4fbb5b830f29..f089938fd9cb 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"App may not work with split screen"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App does not support split screen"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"This app can only be opened in 1 window"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App may not work on a secondary display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App does not support launch on secondary displays."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 9aa985d21819..6bbc1e37671f 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Dejar de almacenar de manera segura"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Es posible que la app no funcione en el modo de pantalla dividida"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"La app no es compatible con la función de pantalla dividida"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Esta app solo puede estar abierta en 1 ventana"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la app no funcione en una pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La app no puede iniciarse en pantallas secundarias."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de pantalla dividida"</string> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index e51f73541f5f..c662ff603dd7 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"No esconder"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Puede que la aplicación no funcione con la pantalla dividida"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"La aplicación no es compatible con la pantalla dividida"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Esta aplicación solo puede abrirse en una ventana"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Es posible que la aplicación no funcione en una pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"La aplicación no se puede abrir en pantallas secundarias."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de pantalla dividida"</string> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index b3f30e76cd81..ade5e2d18645 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Eemalda hoidlast"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Rakendus ei pruugi jagatud ekraanikuvaga töötada."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Rakendus ei toeta jagatud ekraanikuva."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Selle rakenduse saab avada ainult ühes aknas"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Rakendus ei pruugi teisesel ekraanil töötada."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Rakendus ei toeta teisestel ekraanidel käivitamist."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Jagatud ekraanikuva jaotur"</string> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 0d9d70637107..d6cb66885cf5 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ez gorde"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Baliteke aplikazioak ez funtzionatzea pantaila zatituan"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikazioak ez du onartzen pantaila zatitua"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Leiho bakar batean ireki daiteke aplikazioa"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Baliteke aplikazioak ez funtzionatzea bigarren mailako pantailetan."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikazioa ezin da exekutatu bigarren mailako pantailatan."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Pantaila-zatitzailea"</string> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index 267daaabbb2c..ba0f51cb1490 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"لغو مخفیسازی"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"ممکن است برنامه با صفحهٔ دونیمه کار نکند"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"برنامه از صفحهٔ دونیمه پشتیبانی نمیکند"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"این برنامه فقط در ۱ پنجره میتواند باز شود"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن است برنامه در نمایشگر ثانویه کار نکند."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"برنامه از راهاندازی در نمایشگرهای ثانویه پشتیبانی نمیکند."</string> <string name="accessibility_divider" msgid="6407584574218956849">"تقسیمکننده صفحهٔ دونیمه"</string> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 7be7557323ca..a53f861e1d18 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poista turvasäilytyksestä"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Sovellus ei ehkä toimi jaetulla näytöllä"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Sovellus ei tue jaetun näytön tilaa"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Tämän sovelluksen voi avata vain yhdessä ikkunassa"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Sovellus ei ehkä toimi toissijaisella näytöllä."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Sovellus ei tue käynnistämistä toissijaisilla näytöillä."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Näytönjakaja"</string> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index 5cee03786bdc..4563556657af 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Retirer de la réserve"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"L\'application peut ne pas fonctionner avec l\'écran partagé"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"L\'application ne prend pas en charge l\'écran partagé"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Cette application ne peut être ouverte que dans une seule fenêtre."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Séparateur d\'écran partagé"</string> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 0b3b364202cf..895757184b23 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"L\'appli peut ne pas fonctionner en mode Écran partagé"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Appli incompatible avec l\'écran partagé"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Cette appli ne peut être ouverte que dans 1 fenêtre"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Il est possible que l\'application ne fonctionne pas sur un écran secondaire."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'application ne peut pas être lancée sur des écrans secondaires."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Séparateur d\'écran partagé"</string> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index a46c9e74a178..54c864eced47 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Non esconder"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"É posible que a aplicación non funcione coa pantalla dividida"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"A aplicación non admite a función de pantalla dividida"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Esta aplicación só se pode abrir en 1 ventá"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É posible que a aplicación non funcione nunha pantalla secundaria."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A aplicación non se pode iniciar en pantallas secundarias."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de pantalla dividida"</string> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index ad27a79e21a4..2b092795b035 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"બતાવો"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"વિભાજિત સ્ક્રીન સાથે ઍપ કદાચ કામ ન કરે"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ઍપ વિભાજિત સ્ક્રીનને સપોર્ટ કરતી નથી"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"આ ઍપ માત્ર 1 વિન્ડોમાં ખોલી શકાય છે"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ઍપ્લિકેશન ગૌણ ડિસ્પ્લે પર કદાચ કામ ન કરે."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ઍપ્લિકેશન ગૌણ ડિસ્પ્લે પર લૉન્ચનું સમર્થન કરતી નથી."</string> <string name="accessibility_divider" msgid="6407584574218956849">"સ્ક્રીનને વિભાજિત કરતું વિભાજક"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 9b10fdcf07a0..35b099ac6d38 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"दिखाएं"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"मुमकिन है कि ऐप्लिकेशन, स्प्लिट स्क्रीन मोड में काम न करे"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"यह ऐप्लिकेशन, स्प्लिट स्क्रीन मोड पर काम नहीं करता"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"इस ऐप्लिकेशन को सिर्फ़ एक विंडो में खोला जा सकता है"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"हो सकता है कि ऐप प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर काम न करे."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर ऐप लॉन्च नहीं किया जा सकता."</string> <string name="accessibility_divider" msgid="6407584574218956849">"स्प्लिट स्क्रीन डिवाइडर मोड"</string> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index ae4ff2d95a50..f2c3c22414df 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poništite sakrivanje"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikacija možda neće funkcionirati s podijeljenim zaslonom"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikacija ne podržava podijeljeni zaslon"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ova se aplikacija može otvoriti samo u jednom prozoru"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija možda neće funkcionirati na sekundarnom zaslonu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podržava pokretanje na sekundarnim zaslonima."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Razdjelnik podijeljenog zaslona"</string> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 070743c46fa9..d94bb29f1d73 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Félretevés megszüntetése"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Lehet, hogy az alkalmazás nem működik osztott képernyős nézetben"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Az alkalmazás nem támogatja az osztott képernyőt"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ez az alkalmazás csak egy ablakban nyitható meg"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Előfordulhat, hogy az alkalmazás nem működik másodlagos kijelzőn."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Az alkalmazást nem lehet másodlagos kijelzőn elindítani."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Elválasztó az osztott képernyős nézetben"</string> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index dfdc97479743..f2945c16e50b 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ցուցադրել"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Հավելվածը չի կարող աշխատել տրոհված էկրանի ռեժիմում"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Հավելվածը չի աջակցում էկրանի տրոհումը"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Այս հավելվածը հնարավոր է բացել միայն մեկ պատուհանում"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Հավելվածը կարող է չաշխատել լրացուցիչ էկրանի վրա"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Հավելվածը չի աջակցում գործարկումը լրացուցիչ էկրանների վրա"</string> <string name="accessibility_divider" msgid="6407584574218956849">"Տրոհված էկրանի բաժանիչ"</string> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index f6161a9d0afd..c39b429e489c 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Batalkan stash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikasi mungkin tidak berfungsi dengan layar terpisah"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikasi tidak mendukung layar terpisah"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Aplikasi ini hanya dapat dibuka di 1 jendela"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikasi mungkin tidak berfungsi pada layar sekunder."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikasi tidak mendukung peluncuran pada layar sekunder."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Pembagi layar terpisah"</string> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 0cc1006da65e..630eaa3855c1 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Taka úr geymslu"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Forritið virkar hugsanlega ekki með skjáskiptingu"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Forritið styður ekki skjáskiptingu"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Aðeins er hægt að opna þetta forrit í 1 glugga"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Hugsanlegt er að forritið virki ekki á öðrum skjá."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Forrit styður ekki opnun á öðrum skjá."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Skilrúm skjáskiptingar"</string> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index bddde703bc80..77893c9e38cf 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Annulla accantonamento"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"L\'app potrebbe non funzionare con lo schermo diviso"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"L\'app non supporta la modalità schermo diviso"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Questa app può essere aperta soltanto in 1 finestra"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"L\'app potrebbe non funzionare su un display secondario."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"L\'app non supporta l\'avvio su display secondari."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Strumento per schermo diviso"</string> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 32b75ec9cba7..4f28c23ba5df 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ביטול ההסתרה הזמנית"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"יכול להיות שהאפליקציה לא תפעל עם מסך מפוצל"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"האפליקציה לא תומכת במסך מפוצל"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ניתן לפתוח את האפליקציה הזו רק בחלון אחד"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ייתכן שהאפליקציה לא תפעל במסך משני."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"האפליקציה אינה תומכת בהפעלה במסכים משניים."</string> <string name="accessibility_divider" msgid="6407584574218956849">"מחלק מסך מפוצל"</string> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index ea19f5ac2d1e..60b4d7eb8b4d 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"表示"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"アプリは分割画面では動作しないことがあります"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"アプリで分割画面がサポートされていません"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"このアプリはウィンドウが 1 つの場合のみ開くことができます"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"アプリはセカンダリ ディスプレイでは動作しないことがあります。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"アプリはセカンダリ ディスプレイでの起動に対応していません。"</string> <string name="accessibility_divider" msgid="6407584574218956849">"分割画面の分割線"</string> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 1a4d17c73a32..28d2257786a7 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"გადანახვის გაუქმება"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"აპმა შეიძლება არ იმუშაოს გაყოფილი ეკრანის რეჟიმში"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ეკრანის გაყოფა არ არის მხარდაჭერილი აპის მიერ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ამ აპის გახსნა შესაძლებელია მხოლოდ 1 ფანჯარაში"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"აპმა შეიძლება არ იმუშაოს მეორეულ ეკრანზე."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"აპს არ გააჩნია მეორეული ეკრანის მხარდაჭერა."</string> <string name="accessibility_divider" msgid="6407584574218956849">"ეკრანის გაყოფის გამყოფი"</string> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 4d5ac830ce8a..441df8d70e95 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Көрсету"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Қолданба экранды бөлу режимінде жұмыс істемеуі мүмкін."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Қолданбада экранды бөлу мүмкін емес."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Бұл қолданбаны тек 1 терезеден ашуға болады."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Қолданба қосымша дисплейде жұмыс істемеуі мүмкін."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Қолданба қосымша дисплейлерде іске қосуды қолдамайды."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Экранды бөлу режимінің бөлгіші"</string> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index f90ca9a1bcaf..efa6418eaa47 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ឈប់លាក់ជាបណ្ដោះអាសន្ន"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"កម្មវិធីអាចមិនដំណើរការជាមួយមុខងារបំបែកអេក្រង់ទេ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"កម្មវិធីមិនអាចប្រើមុខងារបំបែកអេក្រង់បានទេ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"អាចបើកកម្មវិធីនេះបានតែក្នុងវិនដូ 1 ប៉ុណ្ណោះ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"កម្មវិធីនេះប្រហែលជាមិនដំណើរការនៅលើអេក្រង់បន្ទាប់បន្សំទេ។"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"កម្មវិធីនេះមិនអាចចាប់ផ្តើមនៅលើអេក្រង់បន្ទាប់បន្សំបានទេ។"</string> <string name="accessibility_divider" msgid="6407584574218956849">"បន្ទាត់ខណ្ឌចែកក្នុងមុខងារបំបែកអេក្រង់"</string> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 57aa771a2548..0cda44509b54 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ಅನ್ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ಆ್ಯಪ್ ಕೆಲಸ ಮಾಡದೇ ಇರಬಹುದು"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಆ್ಯಪ್ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ಈ ಆ್ಯಪ್ ಅನ್ನು 1 ವಿಂಡೋದಲ್ಲಿ ಮಾತ್ರ ತೆರೆಯಬಹುದು"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಅಪ್ಲಿಕೇಶನ್ ಕಾರ್ಯ ನಿರ್ವಹಿಸದೇ ಇರಬಹುದು."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ಸೆಕೆಂಡರಿ ಡಿಸ್ಪ್ಲೇಗಳಲ್ಲಿ ಪ್ರಾರಂಭಿಸುವಿಕೆಯನ್ನು ಅಪ್ಲಿಕೇಶನ್ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string> <string name="accessibility_divider" msgid="6407584574218956849">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಡಿವೈಡರ್"</string> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index e777fa7dc7a0..676506fc68dd 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"숨기기 취소"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"앱이 화면 분할 모드로는 작동하지 않을 수 있습니다"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"앱이 화면 분할을 지원하지 않습니다"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"이 앱은 창 1개에서만 열 수 있습니다."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"앱이 보조 디스플레이에서 작동하지 않을 수도 있습니다."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"앱이 보조 디스플레이에서의 실행을 지원하지 않습니다."</string> <string name="accessibility_divider" msgid="6407584574218956849">"화면 분할기"</string> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index a96f3c8eb87a..57253ef55085 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Сейфтен чыгаруу"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Колдонмодо экран бөлүнбөшү мүмкүн"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Колдонмодо экран бөлүнбөйт"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Бул колдонмону 1 терезеде гана ачууга болот"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Колдонмо кошумча экранда иштебей коюшу мүмкүн."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Колдонмону кошумча экрандарда иштетүүгө болбойт."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Экранды бөлгүч"</string> diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml index a95323fd4801..1b96fa227383 100644 --- a/libs/WindowManager/Shell/res/values-land/dimens.xml +++ b/libs/WindowManager/Shell/res/values-land/dimens.xml @@ -16,13 +16,6 @@ */ --> <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--> <dimen name="bubble_padding_top">4dp</dimen> diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml deleted file mode 100644 index e89f65bef792..000000000000 --- a/libs/WindowManager/Shell/res/values-land/styles.xml +++ /dev/null @@ -1,36 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- 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. ---> - -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - <style name="DockedDividerBackground"> - <item name="android:layout_width">@dimen/split_divider_bar_width</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</item> - <item name="android:layout_width">48dp</item> - <item name="android:layout_height">96dp</item> - </style> - - <style name="DockedDividerMinimizedShadow"> - <item name="android:layout_width">8dp</item> - <item name="android:layout_height">match_parent</item> - </style> -</resources> - diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 6ab04c596cd3..c5f6e2245b31 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ເອົາອອກຈາກບ່ອນເກັບສ່ວນຕົວ"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"ແອັບອາດໃຊ້ບໍ່ໄດ້ກັບໂໝດແບ່ງໜ້າຈໍ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ແອັບບໍ່ຮອງຮັບການແບ່ງໜ້າຈໍ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ແອັບນີ້ສາມາດເປີດໄດ້ໃນ 1 ໜ້າຈໍເທົ່ານັ້ນ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ແອັບອາດບໍ່ສາມາດໃຊ້ໄດ້ໃນໜ້າຈໍທີສອງ."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ແອັບບໍ່ຮອງຮັບການເປີດໃນໜ້າຈໍທີສອງ."</string> <string name="accessibility_divider" msgid="6407584574218956849">"ເສັ້ນແບ່ງໜ້າຈໍ"</string> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index 9ef541e91c5b..eeed5a416fdc 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Nebeslėpti"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Programa gali neveikti naudojant išskaidyto ekrano režimą"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Programoje nepalaikomas išskaidyto ekrano režimas"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Šią programą galima atidaryti tik viename lange"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Programa gali neveikti antriniame ekrane."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Programa nepalaiko paleisties antriniuose ekranuose."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Išskaidyto ekrano režimo daliklis"</string> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index 634db045b754..4324d468042b 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Rādīt"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Iespējams, lietotne nedarbosies ekrāna sadalīšanas režīmā"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Lietotnē netiek atbalstīta ekrāna sadalīšana"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Šo lietotni var atvērt tikai vienā logā"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Lietotne, iespējams, nedarbosies sekundārajā displejā."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Lietotnē netiek atbalstīta palaišana sekundārajos displejos."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Ekrāna sadalītājs"</string> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 6272b05ea768..471f5bdfcf1a 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Прикажете"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Апликацијата можеби нема да работи со поделен екран"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Апликацијата не поддржува поделен екран"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Апликацијава може да се отвори само во еден прозорец"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликацијата може да не функционира на друг екран."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликацијата не поддржува стартување на други екрани."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Разделник на поделен екран"</string> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index dcc12a752f31..5bc694a10747 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"അൺസ്റ്റാഷ് ചെയ്യൽ"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"സ്ക്രീൻ വിഭജന മോഡിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"സ്ക്രീൻ വിഭജന മോഡിനെ ആപ്പ് പിന്തുണയ്ക്കുന്നില്ല"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ഈ ആപ്പ് ഒരു വിൻഡോയിൽ മാത്രമേ തുറക്കാനാകൂ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"രണ്ടാം ഡിസ്പ്ലേയിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"രണ്ടാം ഡിസ്പ്ലേകളിൽ സമാരംഭിക്കുന്നതിനെ ആപ്പ് അനുവദിക്കുന്നില്ല."</string> <string name="accessibility_divider" msgid="6407584574218956849">"സ്ക്രീൻ വിഭജന മോഡ് ഡിവൈഡർ"</string> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 5ff9c97d3432..0268c649380d 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ил гаргах"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Апп дэлгэцийг хуваах горимтой ажиллахгүй байж магадгүй"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Апп дэлгэцийг хуваах горимыг дэмждэггүй"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Энэ аппыг зөвхөн 1 цонхонд нээх боломжтой"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апп хоёрдогч дэлгэцэд ажиллахгүй."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Аппыг хоёрдогч дэлгэцэд эхлүүлэх боломжгүй."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Дэлгэцийг хуваах хуваагч"</string> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 1dc4151e506f..2e6163e65668 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्टॅश करा"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"अॅप कदाचित स्प्लिट स्क्रीनसह काम करणार नाही"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"अॅप हे स्प्लिट स्क्रीनला सपोर्ट करत नाही"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"हे अॅप फक्त एका विंडोमध्ये उघडले जाऊ शकते"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"दुसऱ्या डिस्प्लेवर अॅप कदाचित चालणार नाही."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"दुसऱ्या डिस्प्लेवर अॅप लाँच होणार नाही."</string> <string name="accessibility_divider" msgid="6407584574218956849">"स्प्लिट स्क्रीन विभाजक"</string> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index c20f2b19afa1..a60e61b892cb 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Tunjukkan"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Apl mungkin tidak berfungsi dengan skrin pisah"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Apl tidak menyokong skrin pisah"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Apl ini hanya boleh dibuka dalam 1 tetingkap"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Apl mungkin tidak berfungsi pada paparan kedua."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Apl tidak menyokong pelancaran pada paparan kedua."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Pembahagi skrin pisah"</string> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index c07511bde9f9..6b91d4676621 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"မသိုဝှက်ရန်"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းဖြင့် အက်ပ်သည် အလုပ်မလုပ်ပါ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"အက်ပ်တွင် မျက်နှာပြင် ခွဲ၍ပြသခြင်းကို မပံ့ပိုးပါ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ဤအက်ပ်ကို ဝင်းဒိုး ၁ ခုတွင်သာ ဖွင့်နိုင်သည်"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ဤအက်ပ်အနေဖြင့် ဒုတိယဖန်သားပြင်ပေါ်တွင် အလုပ်လုပ်မည် မဟုတ်ပါ။"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ဤအက်ပ်အနေဖြင့် ဖွင့်ရန်စနစ်ကို ဒုတိယဖန်သားပြင်မှ အသုံးပြုရန် ပံ့ပိုးမထားပါ။"</string> <string name="accessibility_divider" msgid="6407584574218956849">"မျက်နှာပြင် ခွဲ၍ပြသခြင်း ပိုင်းခြားစနစ်"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 458487c35d63..ec9ece3484b8 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Avslutt oppbevaring"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Det kan hende at appen ikke fungerer med delt skjerm"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Appen støtter ikke delt skjerm"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Denne appen kan bare åpnes i ett vindu"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen fungerer kanskje ikke på en sekundær skjerm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan ikke kjøres på sekundære skjermer."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Skilleelement for delt skjerm"</string> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 09d8396eed59..8bb07be12c48 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्ट्यास गर्नुहोस्"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"यो एपले स्प्लिट स्क्रिन मोडमा काम नगर्न सक्छ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"यो एप स्प्लिट स्क्रिन मोडमा प्रयोग गर्न मिल्दैन"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"यो एप एउटा विन्डोमा मात्र खोल्न मिल्छ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"यो एपले सहायक प्रदर्शनमा काम नगर्नसक्छ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"अनुप्रयोगले सहायक प्रदर्शनहरूमा लञ्च सुविधालाई समर्थन गर्दैन।"</string> <string name="accessibility_divider" msgid="6407584574218956849">"स्प्लिट स्क्रिन डिभाइडर"</string> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 8b91fa299847..c6c60ae4b1f2 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Niet meer verbergen"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"De app werkt misschien niet met gesplitst scherm"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"App ondersteunt geen gesplitst scherm"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Deze app kan maar in 1 venster worden geopend"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"App werkt mogelijk niet op een secundair scherm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"App kan niet op secundaire displays worden gestart."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Scheiding voor gesplitst scherm"</string> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index de1c9983fac9..927dde40134d 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ଦେଖାନ୍ତୁ"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନରେ ଆପ କାମ କରିନପାରେ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନକୁ ଆପ ସମର୍ଥନ କରେ ନାହିଁ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ଏହି ଆପକୁ କେବଳ 1ଟି ୱିଣ୍ଡୋରେ ଖୋଲାଯାଇପାରିବ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ କାମ ନକରିପାରେ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ସେକେଣ୍ଡାରୀ ଡିସପ୍ଲେରେ ଆପ୍ ଲଞ୍ଚ ସପୋର୍ଟ କରେ ନାହିଁ।"</string> <string name="accessibility_divider" msgid="6407584574218956849">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଡିଭାଇଡର"</string> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 2a700d375961..0e12fb872005 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ਅਣਸਟੈਸ਼"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ਐਪ ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ਇਹ ਐਪ ਸਿਰਫ਼ 1 ਵਿੰਡੋ ਵਿੱਚ ਖੋਲ੍ਹੀ ਜਾ ਸਕਦੀ ਹੈ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇ \'ਤੇ ਕੰਮ ਨਾ ਕਰੇ।"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ਐਪ ਸੈਕੰਡਰੀ ਡਿਸਪਲੇਆਂ \'ਤੇ ਲਾਂਚ ਕਰਨ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ"</string> <string name="accessibility_divider" msgid="6407584574218956849">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਿਭਾਜਕ"</string> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index 43cfa6461cc8..75a8ce6bc16d 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zabierz ze schowka"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikacja może nie działać przy podzielonym ekranie"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikacja nie obsługuje podzielonego ekranu"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ta aplikacja może być otwarta tylko w 1 oknie."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacja może nie działać na dodatkowym ekranie."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacja nie obsługuje uruchamiania na dodatkowych ekranach."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Linia dzielenia ekranu"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index fb1ef6880dc9..b84a0ded4939 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"É possível que o app não funcione com a tela dividida"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"O app não oferece suporte à divisão de tela"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Esse app só pode ser aberto em uma única janela"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"O app não é compatível com a inicialização em telas secundárias."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de tela"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 702c0433fa0a..d84bfcdd73ff 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Remover do armazenamento"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"A app pode não funcionar com o ecrã dividido"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"A app não é compatível com o ecrã dividido"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Esta app só pode ser aberta em 1 janela"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"A app pode não funcionar num ecrã secundário."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"A app não é compatível com o início em ecrãs secundários."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Divisor do ecrã dividido"</string> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index fb1ef6880dc9..b84a0ded4939 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"É possível que o app não funcione com a tela dividida"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"O app não oferece suporte à divisão de tela"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Esse app só pode ser aberto em uma única janela"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"É possível que o app não funcione em uma tela secundária."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"O app não é compatível com a inicialização em telas secundárias."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de tela"</string> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index 596eebc7dd95..eeea428cc8fa 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulează stocarea"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Este posibil ca aplicația să nu funcționeze cu ecranul împărțit"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplicația nu acceptă ecranul împărțit"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Aplicația se poate deschide într-o singură fereastră"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Este posibil ca aplicația să nu funcționeze pe un ecran secundar."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplicația nu acceptă lansare pe ecrane secundare."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Separator pentru ecranul împărțit"</string> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index f2f1f6fdd857..26b0f94eb585 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показать"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Когда включено разделение экрана, приложение может работать нестабильно."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Приложение не поддерживает разделение экрана."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Это приложение можно открыть только в одном окне."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Приложение может не работать на дополнительном экране"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Приложение не поддерживает запуск на дополнительных экранах"</string> <string name="accessibility_divider" msgid="6407584574218956849">"Разделитель экрана"</string> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index 0a1fd94a8998..9b9a430ce73f 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"සඟවා තැබීම ඉවත් කරන්න"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"යෙදුම බෙදීම් තිරය සමග ක්රියා නොකළ හැක"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"යෙදුම බෙදුම් තිරයට සහාය නොදක්වයි"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"මෙම යෙදුම විවෘත කළ හැක්කේ 1 කවුළුවක පමණයි"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"යෙදුම ද්විතියික සංදර්ශකයක ක්රියා නොකළ හැකිය."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"යෙදුම ද්විතීයික සංදර්ශක මත දියත් කිරීම සඳහා සහාය නොදක්වයි."</string> <string name="accessibility_divider" msgid="6407584574218956849">"බෙදුම් තිර වෙන්කරණය"</string> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index 6e746b01f2c3..4b2153180cbe 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušiť skrytie"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikácia nemusí fungovať s rozdelenou obrazovkou"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikácia nepodporuje rozdelenú obrazovku"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Táto aplikácia môže byť otvorená iba v jednom okne"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikácia nemusí fungovať na sekundárnej obrazovke."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikácia nepodporuje spúšťanie na sekundárnych obrazovkách."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Rozdeľovač obrazovky"</string> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 9536e05ef057..581cf5b815c6 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Razkrij"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikacija morda ne deluje v načinu razdeljenega zaslona."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikacija ne podpira načina razdeljenega zaslona."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"To aplikacijo je mogoče odpreti samo v enem oknu"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacija morda ne bo delovala na sekundarnem zaslonu."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacija ne podpira zagona na sekundarnih zaslonih."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Razdelilnik zaslonov"</string> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index a0fff68554e7..9dc7b7ebef99 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Mos e fshih"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Aplikacioni mund të mos funksionojë me ekranin e ndarë"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Aplikacioni nuk mbështet ekranin e ndarë"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ky aplikacion mund të hapet vetëm në 1 dritare"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Aplikacioni mund të mos funksionojë në një ekran dytësor."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Aplikacioni nuk mbështet nisjen në ekrane dytësore."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Ndarësi i ekranit të ndarë"</string> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index ad9ba9038eb8..cd532f79dd78 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Уклоните из тајне меморије"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Апликација можда неће радити са подељеним екраном."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Апликација не подржава подељени екран."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ова апликација може да се отвори само у једном прозору"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Апликација можда неће функционисати на секундарном екрану."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Апликација не подржава покретање на секундарним екранима."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Разделник подељеног екрана"</string> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 488af391b45b..386dda7e088d 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Återställ stash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Appen kanske inte fungerar med delad skärm"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Appen har inte stöd för delad skärm"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Denna app kan bara vara öppen i ett fönster"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Appen kanske inte fungerar på en sekundär skärm."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Appen kan inte köras på en sekundär skärm."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Avdelare för delad skärm"</string> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 38cbfe38007c..69b2e34ada3c 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Fichua"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Huenda programu isifanye kazi kwenye skrini iliyogawanywa"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Programu haifanyi kazi kwenye skrini iliyogawanywa"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Programu hii inaweza kufunguliwa katika dirisha 1 pekee"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Huenda programu isifanye kazi kwenye dirisha lingine."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Programu hii haiwezi kufunguliwa kwenye madirisha mengine."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Kitenganishi cha kugawa skrini"</string> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 616aa274fa15..037b5aba22f5 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"திரைப் பிரிப்புப் பயன்முறையில் ஆப்ஸ் செயல்படாமல் போகக்கூடும்"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"திரைப் பிரிப்புப் பயன்முறையை ஆப்ஸ் ஆதரிக்காது"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"இந்த ஆப்ஸை 1 சாளரத்தில் மட்டுமே திறக்க முடியும்"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"இரண்டாம்நிலைத் திரையில் ஆப்ஸ் வேலை செய்யாமல் போகக்கூடும்."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"இரண்டாம்நிலைத் திரைகளில் பயன்பாட்டைத் தொடங்க முடியாது."</string> <string name="accessibility_divider" msgid="6407584574218956849">"திரைப் பிரிப்பான்"</string> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 6fe1995692ec..694ecb951210 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ఆన్స్టాచ్"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"స్ప్లిట్ స్క్రీన్తో యాప్ పని చేయకపోవచ్చు"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"యాప్లో స్ప్లిట్ స్క్రీన్కు సపోర్ట్ లేదు"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"ఈ యాప్ను 1 విండోలో మాత్రమే తెరవవచ్చు"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ప్రత్యామ్నాయ డిస్ప్లేలో యాప్ పని చేయకపోవచ్చు."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ప్రత్యామ్నాయ డిస్ప్లేల్లో ప్రారంభానికి యాప్ మద్దతు లేదు."</string> <string name="accessibility_divider" msgid="6407584574218956849">"స్ప్లిట్ స్క్రీన్ డివైడర్"</string> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index 65e5ff77f6a6..d4b6aff2ee7d 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"เอาออกจากที่เก็บส่วนตัว"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"แอปอาจใช้ไม่ได้กับโหมดแยกหน้าจอ"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"แอปไม่รองรับการแยกหน้าจอ"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"แอปนี้เปิดได้ใน 1 หน้าต่างเท่านั้น"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"แอปอาจไม่ทำงานในจอแสดงผลรอง"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"แอปไม่รองรับการเรียกใช้ในจอแสดงผลรอง"</string> <string name="accessibility_divider" msgid="6407584574218956849">"เส้นแยกหน้าจอ"</string> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 74e0abee6732..db9303c0fd9c 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"I-unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Posibleng hindi gumana sa split screen ang app"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Hindi sinusuportahan ng app ang split-screen"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Sa 1 window lang puwedeng buksan ang app na ito"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Maaaring hindi gumana ang app sa pangalawang display."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Hindi sinusuportahan ng app ang paglulunsad sa mga pangalawang display."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Divider ng split screen"</string> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 2d169240afa8..818666c79973 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Depolama"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Uygulama bölünmüş ekranda çalışmayabilir"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Uygulama bölünmüş ekranı desteklemiyor."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Bu uygulama yalnızca 1 pencerede açılabilir"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uygulama ikincil ekranda çalışmayabilir."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uygulama ikincil ekranlarda başlatılmayı desteklemiyor."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Bölünmüş ekran ayırıcı"</string> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index 01031281818b..85fb8e114476 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показати"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Додаток може не працювати в режимі розділення екрана"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Додаток не підтримує розділення екрана"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Цей додаток можна відкрити лише в одному вікні"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Додаток може не працювати на додатковому екрані."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Додаток не підтримує запуск на додаткових екранах."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Розділювач екрана"</string> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 2fdcfe8015cf..813870b134b4 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"ممکن ہے کہ ایپ اسپلٹ اسکرین کے ساتھ کام نہ کرے"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ایپ اسپلٹ اسکرین کو سپورٹ نہیں کرتی ہے"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"یہ ایپ صرف 1 ونڈو میں کھولی جا سکتی ہے"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"ممکن ہے ایپ ثانوی ڈسپلے پر کام نہ کرے۔"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"ایپ ثانوی ڈسپلیز پر شروعات کا تعاون نہیں کرتی۔"</string> <string name="accessibility_divider" msgid="6407584574218956849">"اسپلٹ اسکرین ڈیوائیڈر"</string> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index a0d7cb6d228c..7bcacbb93f1f 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Chiqarish"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Bu ilovada ekranni ikkiga ajratish rejimi ishlamaydi."</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Bu ilovada ekranni ikkiga ajratish ishlamaydi."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Bu ilovani faqat 1 ta oynada ochish mumkin"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Bu ilova qo‘shimcha ekranda ishlamasligi mumkin."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Bu ilova qo‘shimcha ekranlarda ishga tushmaydi."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Ekranni ikkiga ajratish chizigʻi"</string> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 957a4577430b..416dd91162c2 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Hiện"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Có thể ứng dụng không dùng được chế độ chia đôi màn hình"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Ứng dụng không hỗ trợ chế độ chia đôi màn hình"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Ứng dụng này chỉ có thể mở trong 1 cửa sổ"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Ứng dụng có thể không hoạt động trên màn hình phụ."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Ứng dụng không hỗ trợ khởi chạy trên màn hình phụ."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Trình chia đôi màn hình"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index e4bf0765ef06..6ad172807f6a 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消隐藏"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"应用可能无法在分屏模式下正常运行"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"应用不支持分屏"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"此应用只能在 1 个窗口中打开"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"应用可能无法在辅显示屏上正常运行。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"应用不支持在辅显示屏上启动。"</string> <string name="accessibility_divider" msgid="6407584574218956849">"分屏分隔线"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 20076211d2b6..b5b94ec40fd1 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消保護"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"應用程式可能無法在分割螢幕中運作"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"應用程式不支援分割螢幕"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"此應用程式只可在 1 個視窗中開啟"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示屏上運作。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示屏上啟動。"</string> <string name="accessibility_divider" msgid="6407584574218956849">"分割螢幕分隔線"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index 1d69edd8b421..0f2a052dbbe0 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消暫時隱藏"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"應用程式可能無法在分割畫面中運作"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"這個應用程式不支援分割畫面"</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"這個應用程式只能在 1 個視窗中開啟"</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"應用程式可能無法在次要顯示器上運作。"</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"應用程式無法在次要顯示器上啟動。"</string> <string name="accessibility_divider" msgid="6407584574218956849">"分割畫面分隔線"</string> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 099879bca415..a696f9ec6251 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -34,8 +34,7 @@ <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Susa isiteshi"</string> <string name="dock_forced_resizable" msgid="7429086980048964687">"Ama-app okungenzeka angasebenzi ngesikrini esihlukanisiwe"</string> <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"I-app ayisekeli isikrini esihlukanisiwe."</string> - <!-- no translation found for dock_multi_instances_not_supported_text (5011042177901502928) --> - <skip /> + <string name="dock_multi_instances_not_supported_text" msgid="5011042177901502928">"Le-app ingavulwa kuphela ewindini eli-1."</string> <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"Uhlelo lokusebenza kungenzeka lungasebenzi kusibonisi sesibili."</string> <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"Uhlelo lokusebenza alusekeli ukuqalisa kuzibonisi zesibili."</string> <string name="accessibility_divider" msgid="6407584574218956849">"Isihlukanisi sokuhlukanisa isikrini"</string> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 9bfd1b44dcca..fae71efe3b39 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -60,14 +60,6 @@ <!-- Desktop Mode --> <color name="desktop_mode_caption_handle_bar_light">#EFF1F2</color> <color name="desktop_mode_caption_handle_bar_dark">#1C1C17</color> - <color name="desktop_mode_caption_expand_button_light">#EFF1F2</color> - <color name="desktop_mode_caption_expand_button_dark">#48473A</color> - <color name="desktop_mode_caption_close_button_light">#EFF1F2</color> - <color name="desktop_mode_caption_close_button_dark">#1C1C17</color> - <color name="desktop_mode_caption_maximize_button_light">#EFF1F2</color> - <color name="desktop_mode_caption_maximize_button_dark">#1C1C17</color> - <color name="desktop_mode_caption_app_name_light">#EFF1F2</color> - <color name="desktop_mode_caption_app_name_dark">#1C1C17</color> <color name="desktop_mode_resize_veil_light">#EFF1F2</color> <color name="desktop_mode_resize_veil_dark">#1C1C17</color> <color name="desktop_mode_maximize_menu_button">#DDDACD</color> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 6a6f2b02766d..e4abae48c8fd 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -141,4 +141,7 @@ window. If false, the splash screen will be a solid color splash screen whenever the app has not provided a windowSplashScreenAnimatedIcon. --> <bool name="config_canUseAppIconForSplashScreen">true</bool> + + <!-- Whether CompatUIController is enabled --> + <bool name="config_enableCompatUIController">true</bool> </resources> diff --git a/libs/WindowManager/Shell/res/values/config_tv.xml b/libs/WindowManager/Shell/res/values/config_tv.xml new file mode 100644 index 000000000000..3da5539c9ae6 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/config_tv.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2023 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. +--> +<resources> + <integer name="config_tvPipEnterFadeOutDuration">500</integer> + <integer name="config_tvPipEnterFadeInDuration">1500</integer> + <integer name="config_tvPipExitFadeOutDuration">500</integer> + <integer name="config_tvPipExitFadeInDuration">500</integer> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f20d44df21b1..8f9de6168bc7 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -96,6 +96,9 @@ <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_region_width">96dp</dimen> + <dimen name="split_divider_handle_region_height">48dp</dimen> + <dimen name="split_divider_handle_width">72dp</dimen> <dimen name="split_divider_handle_height">3dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 468cfd5260cc..08c2a02acf55 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -60,20 +60,9 @@ <style name="DockedDividerBackground"> <item name="android:layout_width">match_parent</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"> - <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">8dp</item> - </style> - - <style name="DockedDividerHandle"> + <item name="android:layout_height">match_parent</item> <item name="android:layout_gravity">center</item> - <item name="android:layout_width">96dp</item> - <item name="android:layout_height">48dp</item> + <item name="android:background">@color/split_divider_background</item> </style> <style name="TvPipEduText"> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index 6cd1324c7d24..efa5a1a64ade 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -21,6 +21,7 @@ import static android.app.ActivityOptions.ANIM_CUSTOM; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo; import android.annotation.NonNull; import android.annotation.Nullable; @@ -253,7 +254,8 @@ class ActivityEmbeddingAnimationSpec { private boolean shouldShowBackdrop(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change) { - final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE, + final int type = getTransitionTypeFromInfo(info); + final Animation a = loadAttributeAnimation(type, info, change, WALLPAPER_TRANSITION_NONE, mTransitionAnimation, false); return a != null && a.getShowBackdrop(); } 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 85ea8097a2c1..7a3210e0a46d 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 @@ -637,7 +637,7 @@ public class Bubble implements BubbleViewProvider { * @return the last time this bubble was updated or accessed, whichever is most recent. */ long getLastActivity() { - return isAppBubble() ? Long.MAX_VALUE : Math.max(mLastUpdated, mLastAccessed); + return Math.max(mLastUpdated, mLastAccessed); } /** 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 f0da35df39ee..249f52bd6156 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 @@ -553,8 +553,9 @@ public class BubbleController implements ConfigurationChangeListener, * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. */ void hideCurrentInputMethod() { + int displayId = mWindowManager.getDefaultDisplay().getDisplayId(); try { - mBarService.hideCurrentInputMethodForBubbles(); + mBarService.hideCurrentInputMethodForBubbles(displayId); } catch (RemoteException e) { e.printStackTrace(); } @@ -787,7 +788,7 @@ public class BubbleController implements ConfigurationChangeListener, mLayerView.setOnApplyWindowInsetsListener((view, windowInsets) -> { if (!windowInsets.equals(mWindowInsets) && mLayerView != null) { mWindowInsets = windowInsets; - mBubblePositioner.update(); + mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager)); mLayerView.onDisplaySizeChanged(); } return windowInsets; @@ -797,7 +798,7 @@ public class BubbleController implements ConfigurationChangeListener, mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> { if (!windowInsets.equals(mWindowInsets) && mStackView != null) { mWindowInsets = windowInsets; - mBubblePositioner.update(); + mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager)); mStackView.onDisplaySizeChanged(); } return windowInsets; @@ -979,7 +980,7 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void onConfigurationChanged(Configuration newConfig) { if (mBubblePositioner != null) { - mBubblePositioner.update(); + mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager)); } if (mStackView != null && newConfig != null) { if (newConfig.densityDpi != mDensityDpi @@ -1279,7 +1280,14 @@ public class BubbleController implements ConfigurationChangeListener, * Dismiss bubble if it exists and remove it from the stack */ public void dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason) { - mBubbleData.dismissBubbleWithKey(bubble.getKey(), reason); + dismissBubble(bubble.getKey(), reason); + } + + /** + * Dismiss bubble with given key if it exists and remove it from the stack + */ + public void dismissBubble(String key, @Bubbles.DismissReason int reason) { + mBubbleData.dismissBubbleWithKey(key, reason); } /** 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 595a4afbfc86..bbb4b74c2a17 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 @@ -784,8 +784,7 @@ public class BubbleData { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED || reason == Bubbles.DISMISS_USER_GESTURE - || reason == Bubbles.DISMISS_RELOAD_FROM_DISK) - || bubble.isAppBubble()) { + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { return; } if (DEBUG_BUBBLE_DATA) { 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 c7ab6aa3934e..a3eb429b1d7e 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 @@ -412,6 +412,23 @@ public class BubbleExpandedView extends LinearLayout { setLayoutDirection(LAYOUT_DIRECTION_LOCALE); } + + /** Updates the width of the task view if it changed. */ + void updateTaskViewContentWidth() { + if (mTaskView != null) { + int width = getContentWidth(); + if (mTaskView.getWidth() != width) { + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(width, MATCH_PARENT); + mTaskView.setLayoutParams(lp); + } + } + } + + private int getContentWidth() { + boolean isStackOnLeft = mPositioner.isStackOnLeft(mStackView.getStackPosition()); + return mPositioner.getTaskViewContentWidth(isStackOnLeft); + } + /** * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need * to be called after view inflate. @@ -438,7 +455,12 @@ public class BubbleExpandedView extends LinearLayout { mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); mTaskView = new TaskView(mContext, mTaskViewTaskController); mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); - mExpandedViewContainer.addView(mTaskView); + + // set a fixed width so it is not recalculated as part of a rotation. the width will be + // updated manually after the rotation. + FrameLayout.LayoutParams lp = + new FrameLayout.LayoutParams(getContentWidth(), MATCH_PARENT); + mExpandedViewContainer.addView(mTaskView, lp); bringChildToFront(mTaskView); } } @@ -963,7 +985,11 @@ public class BubbleExpandedView extends LinearLayout { && mTaskView.isAttachedToWindow()) { // post this to the looper, because if the device orientation just changed, we need to // let the current shell transition complete before updating the task view bounds. - post(() -> mTaskView.onLocationChanged()); + post(() -> { + if (mTaskView != null) { + mTaskView.onLocationChanged(); + } + }); } if (mIsOverflow) { // post this to the looper so that the view has a chance to be laid out before it can 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 09ae84a50328..662a5c47d633 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -16,10 +16,7 @@ package com.android.wm.shell.bubbles; -import static android.view.View.LAYOUT_DIRECTION_RTL; - import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Point; @@ -28,9 +25,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; import android.view.Surface; -import android.view.WindowInsets; import android.view.WindowManager; -import android.view.WindowMetrics; import androidx.annotation.VisibleForTesting; @@ -68,15 +63,12 @@ public class BubblePositioner { private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f; private Context mContext; - private WindowManager mWindowManager; + private DeviceConfig mDeviceConfig; private Rect mScreenRect; private @Surface.Rotation int mRotation = Surface.ROTATION_0; private Insets mInsets; private boolean mImeVisible; private int mImeHeight; - private boolean mIsLargeScreen; - private boolean mIsSmallTablet; - private Rect mPositionRect; private int mDefaultMaxBubbles; private int mMaxBubbles; @@ -110,44 +102,27 @@ public class BubblePositioner { public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; - mWindowManager = windowManager; - update(); + mDeviceConfig = DeviceConfig.create(context, windowManager); + update(mDeviceConfig); } /** * Available space and inset information. Call this when config changes * occur or when added to a window. */ - public void update() { - WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); - if (windowMetrics == null) { - return; - } - WindowInsets metricInsets = windowMetrics.getWindowInsets(); - Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() - | WindowInsets.Type.statusBars() - | WindowInsets.Type.displayCutout()); - - final Rect bounds = windowMetrics.getBounds(); - Configuration config = mContext.getResources().getConfiguration(); - mIsLargeScreen = config.smallestScreenWidthDp >= 600; - if (mIsLargeScreen) { - float largestEdgeDp = Math.max(config.screenWidthDp, config.screenHeightDp); - mIsSmallTablet = largestEdgeDp < 960; - } else { - mIsSmallTablet = false; - } + public void update(DeviceConfig deviceConfig) { + mDeviceConfig = deviceConfig; if (BubbleDebugConfig.DEBUG_POSITIONER) { Log.w(TAG, "update positioner:" + " rotation: " + mRotation - + " insets: " + insets - + " isLargeScreen: " + mIsLargeScreen - + " isSmallTablet: " + mIsSmallTablet + + " insets: " + deviceConfig.getInsets() + + " isLargeScreen: " + deviceConfig.isLargeScreen() + + " isSmallTablet: " + deviceConfig.isSmallTablet() + " showingInBubbleBar: " + mShowingInBubbleBar - + " bounds: " + bounds); + + " bounds: " + deviceConfig.getWindowBounds()); } - updateInternal(mRotation, insets, bounds); + updateInternal(mRotation, deviceConfig.getInsets(), deviceConfig.getWindowBounds()); } @VisibleForTesting @@ -175,15 +150,15 @@ public class BubblePositioner { mExpandedViewLargeScreenWidth = isLandscape() ? (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT) : (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT); - } else if (mIsSmallTablet) { + } else if (mDeviceConfig.isSmallTablet()) { mExpandedViewLargeScreenWidth = (int) (bounds.width() * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); } else { mExpandedViewLargeScreenWidth = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_largescreen_width); } - if (mIsLargeScreen) { - if (mIsSmallTablet) { + if (mDeviceConfig.isLargeScreen()) { + if (mDeviceConfig.isSmallTablet()) { final int centeredInset = (bounds.width() - mExpandedViewLargeScreenWidth) / 2; mExpandedViewLargeScreenInsetClosestEdge = centeredInset; mExpandedViewLargeScreenInsetFurthestEdge = centeredInset; @@ -264,13 +239,24 @@ public class BubblePositioner { /** @return whether the device is in landscape orientation. */ public boolean isLandscape() { - return mContext.getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE; + return mDeviceConfig.isLandscape(); + } + + /** + * On large screen (not small tablet), while in portrait, expanded bubbles are aligned to + * the bottom of the screen. + * + * @return whether bubbles are bottom aligned while expanded + */ + public boolean areBubblesBottomAligned() { + return isLargeScreen() + && !mDeviceConfig.isSmallTablet() + && !isLandscape(); } /** @return whether the screen is considered large. */ public boolean isLargeScreen() { - return mIsLargeScreen; + return mDeviceConfig.isLargeScreen(); } /** @@ -281,7 +267,7 @@ public class BubblePositioner { * to the left or right side. */ public boolean showBubblesVertically() { - return isLandscape() || mIsLargeScreen; + return isLandscape() || mDeviceConfig.isLargeScreen(); } /** Size of the bubble. */ @@ -334,7 +320,7 @@ public class BubblePositioner { } private int getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow) { - if (isOverflow && mIsLargeScreen) { + if (isOverflow && mDeviceConfig.isLargeScreen()) { return mScreenRect.width() - mExpandedViewLargeScreenInsetClosestEdge - mOverflowWidth; @@ -358,7 +344,7 @@ public class BubblePositioner { final int pointerTotalHeight = getPointerSize(); final int expandedViewLargeScreenInsetFurthestEdge = getExpandedViewLargeScreenInsetFurthestEdge(isOverflow); - if (mIsLargeScreen) { + if (mDeviceConfig.isLargeScreen()) { // Note: // If we're in portrait OR if we're a small tablet, then the two insets values will // be equal. If we're landscape and a large tablet, the two values will be different. @@ -401,6 +387,13 @@ public class BubblePositioner { } } + /** Returns the width of the task view content. */ + public int getTaskViewContentWidth(boolean onLeft) { + int[] paddings = getExpandedViewContainerPadding(onLeft, /* isOverflow = */ false); + int pointerOffset = showBubblesVertically() ? getPointerSize() : 0; + return mPositionRect.width() - paddings[0] - paddings[2] - pointerOffset; + } + /** Gets the y position of the expanded view if it was top-aligned. */ public float getExpandedViewYTopAligned() { final int top = getAvailableRect().top; @@ -416,6 +409,9 @@ public class BubblePositioner { * the screen and the size of the elements around it (e.g. padding, pointer, manage button). */ public int getMaxExpandedViewHeight(boolean isOverflow) { + if (mDeviceConfig.isLargeScreen() && !mDeviceConfig.isSmallTablet() && !isOverflow) { + return getExpandedViewHeightForLargeScreen(); + } // Subtract top insets because availableRect.height would account for that int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top; int paddingTop = showBubblesVertically() @@ -434,24 +430,28 @@ public class BubblePositioner { } /** + * Returns the height to use for the expanded view when showing on a large screen. + */ + public int getExpandedViewHeightForLargeScreen() { + // the expanded view height on large tablets is calculated based on the shortest screen + // size and is the same in both portrait and landscape + int maxVerticalInset = Math.max(mInsets.top, mInsets.bottom); + int shortestScreenSide = Math.min(getScreenRect().height(), getScreenRect().width()); + // Subtract pointer size because it's laid out in LinearLayout with the expanded view. + return shortestScreenSide - maxVerticalInset * 2 + - mManageButtonHeight - mPointerWidth - mExpandedViewPadding * 2; + } + + /** * 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) { + if (isOverflow && showBubblesVertically() && !mDeviceConfig.isLargeScreen()) { // overflow in landscape on phone is max return MAX_HEIGHT; } - - if (mIsLargeScreen && !mIsSmallTablet && !isOverflow) { - // the expanded view height on large tablets is calculated based on the shortest screen - // size and is the same in both portrait and landscape - int maxVerticalInset = Math.max(mInsets.top, mInsets.bottom); - int shortestScreenSide = Math.min(mScreenRect.height(), mScreenRect.width()); - return shortestScreenSide - 2 * maxVerticalInset - mManageButtonHeight; - } - float desiredHeight = isOverflow ? mOverflowHeight : ((Bubble) bubble).getDesiredHeight(mContext); @@ -475,13 +475,21 @@ public class BubblePositioner { boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); float expandedViewHeight = getExpandedViewHeight(bubble); float topAlignment = getExpandedViewYTopAligned(); + int manageButtonHeight = + isOverflow ? mExpandedViewPadding : mManageButtonHeightIncludingMargins; + + // On largescreen portrait bubbles are bottom aligned. + if (areBubblesBottomAligned() && expandedViewHeight == MAX_HEIGHT) { + return mPositionRect.bottom - manageButtonHeight + - getExpandedViewHeightForLargeScreen() - mPointerWidth; + } + 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 : mManageButtonHeightIncludingMargins; float pointerPosition = getPointerPosition(bubblePosition); float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight; float topIfCentered = pointerPosition - (expandedViewHeight / 2); @@ -529,11 +537,9 @@ public class BubblePositioner { */ public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) { boolean showBubblesVertically = showBubblesVertically(); - boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() - == LAYOUT_DIRECTION_RTL; int onScreenIndex; - if (showBubblesVertically || !isRtl) { + if (showBubblesVertically || !mDeviceConfig.isRtl()) { onScreenIndex = index; } else { // If bubbles are shown horizontally, check if RTL language is used. @@ -541,23 +547,17 @@ public class BubblePositioner { // Last bubble has screen index 0 and first bubble has max screen index value. onScreenIndex = state.numberOfBubbles - 1 - index; } - final float positionInRow = onScreenIndex * (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); + final float rowStart = getBubbleRowStart(state); float x; float y; if (showBubblesVertically) { int inset = mExpandedViewLargeScreenInsetClosestEdge; y = rowStart + positionInRow; - int left = mIsLargeScreen + int left = mDeviceConfig.isLargeScreen() ? inset - mExpandedViewPadding - mBubbleSize : mPositionRect.left; - int right = mIsLargeScreen + int right = mDeviceConfig.isLargeScreen() ? mPositionRect.right - inset + mExpandedViewPadding : mPositionRect.right - mBubbleSize; x = state.onLeft @@ -574,6 +574,25 @@ public class BubblePositioner { return new PointF(x, y); } + private float getBubbleRowStart(BubbleStackView.StackViewState state) { + final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); + final float rowStart; + if (areBubblesBottomAligned()) { + final float expandedViewHeight = getExpandedViewHeightForLargeScreen(); + final float expandedViewBottom = mScreenRect.bottom + - Math.max(mInsets.bottom, mInsets.top) + - mManageButtonHeight - mPointerWidth; + final float expandedViewCenter = expandedViewBottom - (expandedViewHeight / 2f); + rowStart = expandedViewCenter - (expandedStackSize / 2f); + } else { + final float centerPosition = showBubblesVertically() + ? mPositionRect.centerY() + : mPositionRect.centerX(); + rowStart = centerPosition - (expandedStackSize / 2f); + } + return rowStart; + } + /** * Returns the position of the bubble on-screen when the stack is expanded and the IME * is showing. @@ -594,9 +613,8 @@ public class BubblePositioner { final float bottomHeight = getImeHeight() + mInsets.bottom + (mSpacingBetweenBubbles * 2); final float bottomInset = mScreenRect.bottom - bottomHeight; final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); - final float centerPosition = mPositionRect.centerY(); - final float rowBottom = centerPosition + (expandedStackSize / 2f); - final float rowTop = centerPosition - (expandedStackSize / 2f); + final float rowTop = getBubbleRowStart(state); + final float rowBottom = rowTop + expandedStackSize; float rowTopForIme = rowTop; if (rowBottom > bottomInset) { // We overlap with IME, must shift the bubbles @@ -693,13 +711,10 @@ public class BubblePositioner { * @param isAppBubble whether this start position is for an app bubble or not. */ public PointF getDefaultStartPosition(boolean isAppBubble) { - final int layoutDirection = mContext.getResources().getConfiguration().getLayoutDirection(); // Normal bubbles start on the left if we're in LTR, right otherwise. // TODO (b/294284894): update language around "app bubble" here // App bubbles start on the right in RTL, left otherwise. - final boolean startOnLeft = isAppBubble - ? layoutDirection == LAYOUT_DIRECTION_RTL - : layoutDirection != LAYOUT_DIRECTION_RTL; + final boolean startOnLeft = isAppBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl(); return getStartPosition(startOnLeft ? StackPinnedEdge.LEFT : StackPinnedEdge.RIGHT); } 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 2cee675e83be..b7f749e8a8b6 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 @@ -61,6 +61,7 @@ import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewPropertyAnimator; import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.view.WindowManagerPolicyConstants; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; @@ -536,8 +537,8 @@ public class BubbleStackView extends FrameLayout return; } - final boolean clickedBubbleIsCurrentlyExpandedBubble = - clickedBubble.getKey().equals(mExpandedBubble.getKey()); + final boolean clickedBubbleIsCurrentlyExpandedBubble = mExpandedBubble != null + && clickedBubble.getKey().equals(mExpandedBubble.getKey()); if (isExpanded()) { mExpandedAnimationController.onGestureFinished(); @@ -1001,7 +1002,8 @@ public class BubbleStackView extends FrameLayout mOrientationChangedListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - mPositioner.update(); + mPositioner.update(DeviceConfig.create(mContext, mContext.getSystemService( + WindowManager.class))); onDisplaySizeChanged(); mExpandedAnimationController.updateResources(); mStackAnimationController.updateResources(); @@ -1021,7 +1023,13 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); updatePointerPosition(false); requestUpdate(); - showManageMenu(mShowingManage); + if (mShowingManage) { + // if we're showing the menu after rotation, post it to the looper + // to make sure that the location of the menu button is correct + post(() -> showManageMenu(true)); + } else { + showManageMenu(false); + } PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), getState()); @@ -1508,6 +1516,11 @@ public class BubbleStackView extends FrameLayout updateExpandedView(); } setUpManageMenu(); + if (mShowingManage) { + // the manage menu location depends on the manage button location which may need a + // layout pass, so post this to the looper + post(() -> showManageMenu(true)); + } } @Override @@ -1522,7 +1535,8 @@ public class BubbleStackView extends FrameLayout @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - mPositioner.update(); + WindowManager windowManager = mContext.getSystemService(WindowManager.class); + mPositioner.update(DeviceConfig.create(mContext, Objects.requireNonNull(windowManager))); getViewTreeObserver().addOnComputeInternalInsetsListener(this); getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); } @@ -2448,6 +2462,7 @@ public class BubbleStackView extends FrameLayout final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack( mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(), + /* fadeBubblesDuringCollapse= */ mRemovingLastBubbleWhileExpanded, () -> { mBubbleContainer.setActiveController(mStackAnimationController); updateOverflowVisibility(); @@ -3285,6 +3300,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, mPositioner.showBubblesVertically() ? p.y : p.x)); mExpandedViewContainer.setTranslationX(0f); + mExpandedBubble.getExpandedView().updateTaskViewContentWidth(); mExpandedBubble.getExpandedView().updateView( mExpandedViewContainer.getLocationOnScreen()); updatePointerPosition(false /* forIme */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt new file mode 100644 index 000000000000..929330918174 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 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.bubbles + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.graphics.Insets +import android.graphics.Rect +import android.view.View.LAYOUT_DIRECTION_RTL +import android.view.WindowInsets +import android.view.WindowManager +import kotlin.math.max + +/** Contains device configuration used for positioning bubbles on the screen. */ +data class DeviceConfig( + val isLargeScreen: Boolean, + val isSmallTablet: Boolean, + val isLandscape: Boolean, + val isRtl: Boolean, + val windowBounds: Rect, + val insets: Insets +) { + companion object { + + private const val LARGE_SCREEN_MIN_EDGE_DP = 600 + private const val SMALL_TABLET_MAX_EDGE_DP = 960 + + @JvmStatic + fun create(context: Context, windowManager: WindowManager): DeviceConfig { + val windowMetrics = windowManager.currentWindowMetrics + val metricInsets = windowMetrics.windowInsets + val insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() + or WindowInsets.Type.statusBars() + or WindowInsets.Type.displayCutout()) + val windowBounds = windowMetrics.bounds + val config: Configuration = context.resources.configuration + val isLargeScreen = config.smallestScreenWidthDp >= LARGE_SCREEN_MIN_EDGE_DP + val largestEdgeDp = max(config.screenWidthDp, config.screenHeightDp) + val isSmallTablet = isLargeScreen && largestEdgeDp < SMALL_TABLET_MAX_EDGE_DP + val isLandscape = context.resources.configuration.orientation == ORIENTATION_LANDSCAPE + val isRtl = context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL + return DeviceConfig( + isLargeScreen = isLargeScreen, + isSmallTablet = isSmallTablet, + isLandscape = isLandscape, + isRtl = isRtl, + windowBounds = windowBounds, + insets = insets + ) + } + } +} 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 79f306ece283..5b0239f6d659 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 @@ -107,6 +107,7 @@ public class ExpandedAnimationController private Runnable mAfterExpand; private Runnable mAfterCollapse; private PointF mCollapsePoint; + private boolean mFadeBubblesDuringCollapse = false; /** * Whether the dragged out bubble is springing towards the touch point, rather than using the @@ -201,12 +202,14 @@ public class ExpandedAnimationController } /** Animate collapsing the bubbles back to their stacked position. */ - public void collapseBackToStack(PointF collapsePoint, Runnable after) { + public void collapseBackToStack(PointF collapsePoint, boolean fadeBubblesDuringCollapse, + Runnable after) { mAnimatingExpand = false; mPreparingToCollapse = false; mAnimatingCollapse = true; mAfterCollapse = after; mCollapsePoint = collapsePoint; + mFadeBubblesDuringCollapse = fadeBubblesDuringCollapse; startOrUpdatePathAnimation(false /* expanding */); } @@ -253,6 +256,7 @@ public class ExpandedAnimationController } mAfterCollapse = null; + mFadeBubblesDuringCollapse = false; }; } @@ -262,7 +266,7 @@ public class ExpandedAnimationController == LAYOUT_DIRECTION_RTL; // Animate each bubble individually, since each path will end in a different spot. - animationsForChildrenFromIndex(0, (index, animation) -> { + animationsForChildrenFromIndex(0, mFadeBubblesDuringCollapse, (index, animation) -> { final View bubble = mLayout.getChildAt(index); // Start a path at the bubble's current position. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java index f3cc514d2972..ed00da848a14 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java @@ -204,6 +204,13 @@ public class PhysicsAnimationLayout extends FrameLayout { return animationForChild(mLayout.getChildAt(index)); } + + protected MultiAnimationStarter animationsForChildrenFromIndex( + int startIndex, ChildAnimationConfigurator configurator) { + return animationsForChildrenFromIndex(startIndex, /* fadeChildren= */ false, + configurator); + } + /** * Returns a {@link MultiAnimationStarter} whose startAll method will start the physics * animations for all children from startIndex onward. The provided configurator will be @@ -211,14 +218,16 @@ public class PhysicsAnimationLayout extends FrameLayout { * animation appropriately. */ protected MultiAnimationStarter animationsForChildrenFromIndex( - int startIndex, ChildAnimationConfigurator configurator) { + int startIndex, boolean fadeChildren, ChildAnimationConfigurator configurator) { final Set<DynamicAnimation.ViewProperty> allAnimatedProperties = new HashSet<>(); final List<PhysicsPropertyAnimator> allChildAnims = new ArrayList<>(); // Retrieve the animator for each child, ask the configurator to configure it, then save // it and the properties it chose to animate. for (int i = startIndex; i < mLayout.getChildCount(); i++) { - final PhysicsPropertyAnimator anim = animationForChildAtIndex(i); + final PhysicsPropertyAnimator anim = fadeChildren + ? animationForChildAtIndex(i).alpha(0) + : animationForChildAtIndex(i); configurator.configureAnimationForChildAtIndex(i, anim); allAnimatedProperties.addAll(anim.getAnimatedProperties()); allChildAnims.add(anim); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 79f188ab2611..d073f1df938a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -386,4 +386,11 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView setContentVisibility(mIsContentVisible); } } + + /** + * Check whether the view is animating + */ + public boolean isAnimating() { + return mIsAnimating; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt new file mode 100644 index 000000000000..4ea18f78f5b2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 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.bubbles.bar + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.graphics.PointF +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.common.bubbles.DismissView +import com.android.wm.shell.common.bubbles.RelativeTouchListener + +/** Controller for handling drag interactions with [BubbleBarExpandedView] */ +class BubbleBarExpandedViewDragController( + private val expandedView: BubbleBarExpandedView, + private val dismissView: DismissView, + private val onDismissed: () -> Unit +) { + + init { + expandedView.handleView.setOnTouchListener(HandleDragListener()) + } + + private fun finishDrag(x: Float, y: Float, viewInitialX: Float, viewInitialY: Float) { + val dismissCircleBounds = Rect().apply { dismissView.circle.getBoundsOnScreen(this) } + if (dismissCircleBounds.contains(x.toInt(), y.toInt())) { + onDismissed() + } else { + resetExpandedViewPosition(viewInitialX, viewInitialY) + } + dismissView.hide() + } + + private fun resetExpandedViewPosition(initialX: Float, initialY: Float) { + val listener = + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + expandedView.isAnimating = true + } + + override fun onAnimationEnd(animation: Animator) { + expandedView.isAnimating = false + } + } + expandedView + .animate() + .translationX(initialX) + .translationY(initialY) + .setDuration(RESET_POSITION_ANIM_DURATION) + .setInterpolator(Interpolators.EMPHASIZED_DECELERATE) + .setListener(listener) + .start() + } + + private inner class HandleDragListener : RelativeTouchListener() { + + private val expandedViewRestPosition = PointF() + + override fun onDown(v: View, ev: MotionEvent): Boolean { + // While animating, don't allow new touch events + if (expandedView.isAnimating) { + return false + } + expandedViewRestPosition.x = expandedView.translationX + expandedViewRestPosition.y = expandedView.translationY + return true + } + + override fun onMove( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float, + dx: Float, + dy: Float + ) { + expandedView.translationX = expandedViewRestPosition.x + dx + expandedView.translationY = expandedViewRestPosition.y + dy + dismissView.show() + } + + override fun onUp( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float, + dx: Float, + dy: Float, + velX: Float, + velY: Float + ) { + finishDrag(ev.rawX, ev.rawY, expandedViewRestPosition.x, expandedViewRestPosition.y) + } + + override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) { + resetExpandedViewPosition(expandedViewRestPosition.x, expandedViewRestPosition.y) + dismissView.hide() + } + } + + companion object { + const val RESET_POSITION_ANIM_DURATION = 300L + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index e788341df5f8..bdb0e206e490 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -28,17 +28,24 @@ import android.graphics.drawable.ColorDrawable; import android.view.TouchDelegate; import android.view.View; import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.widget.FrameLayout; +import com.android.wm.shell.R; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; - -import java.util.function.Consumer; +import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.bubbles.DeviceConfig; +import com.android.wm.shell.bubbles.DismissViewUtils; +import com.android.wm.shell.common.bubbles.DismissView; import kotlin.Unit; +import java.util.Objects; +import java.util.function.Consumer; + /** * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window * manager to display bubbles. However, it is only used when bubbles are being displayed in @@ -60,7 +67,11 @@ public class BubbleBarLayerView extends FrameLayout @Nullable private BubbleViewProvider mExpandedBubble; + @Nullable private BubbleBarExpandedView mExpandedView; + @Nullable + private BubbleBarExpandedViewDragController mDragController; + private DismissView mDismissView; private @Nullable Consumer<String> mUnBubbleConversationCallback; // TODO(b/273310265) - currently the view is always on the right, need to update for RTL. @@ -98,13 +109,16 @@ public class BubbleBarLayerView extends FrameLayout mScrimView.setBackgroundDrawable(new ColorDrawable( getResources().getColor(android.R.color.system_neutral1_1000))); + setUpDismissView(); + setOnClickListener(view -> hideMenuOrCollapse()); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - mPositioner.update(); + WindowManager windowManager = mContext.getSystemService(WindowManager.class); + mPositioner.update(DeviceConfig.create(mContext, Objects.requireNonNull(windowManager))); getViewTreeObserver().addOnComputeInternalInsetsListener(this); } @@ -192,6 +206,13 @@ public class BubbleBarLayerView extends FrameLayout } }); + mDragController = new BubbleBarExpandedViewDragController(mExpandedView, mDismissView, + () -> { + mBubbleController.dismissBubble(mExpandedBubble.getKey(), + Bubbles.DISMISS_USER_GESTURE); + return Unit.INSTANCE; + }); + addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); } @@ -223,6 +244,7 @@ public class BubbleBarLayerView extends FrameLayout mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); mBubbleController.getSysuiProxy().onStackExpandChanged(false); mExpandedView = null; + mDragController = null; setTouchDelegate(null); showScrim(false); } @@ -248,6 +270,18 @@ public class BubbleBarLayerView extends FrameLayout mUnBubbleConversationCallback = unBubbleConversationCallback; } + private void setUpDismissView() { + if (mDismissView != null) { + removeView(mDismissView); + } + mDismissView = new DismissView(getContext()); + DismissViewUtils.setup(mDismissView); + int elevation = getResources().getDimensionPixelSize(R.dimen.bubble_elevation); + + addView(mDismissView); + mDismissView.setElevation(elevation); + } + /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */ private void hideMenuOrCollapse() { if (mEducationViewController.isEducationVisible()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index e9344ffcce0c..1c74f415ec90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -322,13 +322,12 @@ public class SystemWindows { } @Override - public void remove(android.view.IWindow window) throws RemoteException { - super.remove(window); + public void remove(IBinder clientToken) throws RemoteException { + super.remove(clientToken); synchronized(this) { - IBinder token = window.asBinder(); - new SurfaceControl.Transaction().remove(mLeashForWindow.get(token)) + new SurfaceControl.Transaction().remove(mLeashForWindow.get(clientToken)) .apply(); - mLeashForWindow.remove(token); + mLeashForWindow.remove(clientToken); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt index d45e1265daac..4e55ba23407b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt @@ -78,8 +78,15 @@ abstract class RelativeTouchListener : View.OnTouchListener { velY: Float ) + open fun onCancel( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float + ) {} + /** The raw coordinates of the last ACTION_DOWN event. */ - private val touchDown = PointF() + private var touchDown: PointF? = null /** The coordinates of the view, at the time of the last ACTION_DOWN event. */ private val viewPositionOnTouchDown = PointF() @@ -91,12 +98,11 @@ abstract class RelativeTouchListener : View.OnTouchListener { private var performedLongClick = false - @Suppress("UNCHECKED_CAST") override fun onTouch(v: View, ev: MotionEvent): Boolean { addMovement(ev) - val dx = ev.rawX - touchDown.x - val dy = ev.rawY - touchDown.y + val dx = touchDown?.let { ev.rawX - it.x } ?: 0f + val dy = touchDown?.let { ev.rawY - it.y } ?: 0f when (ev.action) { MotionEvent.ACTION_DOWN -> { @@ -108,7 +114,7 @@ abstract class RelativeTouchListener : View.OnTouchListener { // last gesture. touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop - touchDown.set(ev.rawX, ev.rawY) + touchDown = PointF(ev.rawX, ev.rawY) viewPositionOnTouchDown.set(v.translationX, v.translationY) performedLongClick = false @@ -120,6 +126,7 @@ abstract class RelativeTouchListener : View.OnTouchListener { } MotionEvent.ACTION_MOVE -> { + if (touchDown == null) return false if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) { movedEnough = true v.handler?.removeCallbacksAndMessages(null) @@ -131,6 +138,7 @@ abstract class RelativeTouchListener : View.OnTouchListener { } MotionEvent.ACTION_UP -> { + if (touchDown == null) return false if (movedEnough) { velocityTracker.computeCurrentVelocity(1000 /* units */) onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy, @@ -143,12 +151,16 @@ abstract class RelativeTouchListener : View.OnTouchListener { velocityTracker.clear() movedEnough = false + touchDown = null } MotionEvent.ACTION_CANCEL -> { + if (touchDown == null) return false v.handler?.removeCallbacksAndMessages(null) velocityTracker.clear() movedEnough = false + touchDown = null + onCancel(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index d520ff791e07..8b6c7b663f82 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -258,7 +258,7 @@ public class PipBoundsState { ActivityTaskManager.getService().onPictureInPictureStateChanged( new PictureInPictureUiState(stashedState != STASH_TYPE_NONE /* isStashed */) ); - } catch (RemoteException e) { + } catch (RemoteException | IllegalStateException e) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Unable to set alert PiP state change.", TAG); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt index 108aa8275009..1e30d8feb132 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt @@ -21,13 +21,13 @@ import android.app.WindowConfiguration import android.content.ComponentName import android.content.Context import android.os.RemoteException -import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.util.Pair import android.util.TypedValue import android.window.TaskSnapshot import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.Flags import com.android.wm.shell.protolog.ShellProtoLogGroup import kotlin.math.abs @@ -37,7 +37,6 @@ object PipUtils { // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal. private const val EPSILON = 1e-7 - private const val ENABLE_PIP2_IMPLEMENTATION = "persist.wm.debug.enable_pip2_implementation" /** * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack. @@ -138,5 +137,5 @@ object PipUtils { @JvmStatic val isPip2ExperimentEnabled: Boolean - get() = SystemProperties.getBoolean(ENABLE_PIP2_IMPLEMENTATION, false) + get() = Flags.enablePip2Implementation() }
\ No newline at end of file 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 ec2680085fb5..999da2443248 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 @@ -68,24 +68,33 @@ public class DividerHandleView extends View { }; private final Paint mPaint = new Paint(); - private final int mWidth; - private final int mHeight; - private final int mTouchingWidth; - private final int mTouchingHeight; + private int mWidth; + private int mHeight; + private int mTouchingWidth; + private int mTouchingHeight; private int mCurrentWidth; private int mCurrentHeight; private AnimatorSet mAnimator; private boolean mTouching; private boolean mHovering; - private final int mHoveringWidth; - private final int mHoveringHeight; + private int mHoveringWidth; + private int mHoveringHeight; + private boolean mIsLeftRightSplit; public DividerHandleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null)); mPaint.setAntiAlias(true); - mWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_width); - mHeight = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_height); + updateDimens(); + } + + private void updateDimens() { + mWidth = getResources().getDimensionPixelSize(mIsLeftRightSplit + ? R.dimen.split_divider_handle_height + : R.dimen.split_divider_handle_width); + mHeight = getResources().getDimensionPixelSize(mIsLeftRightSplit + ? R.dimen.split_divider_handle_width + : R.dimen.split_divider_handle_height); mCurrentWidth = mWidth; mCurrentHeight = mHeight; mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth; @@ -94,6 +103,11 @@ public class DividerHandleView extends View { mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight; } + void setIsLeftRightSplit(boolean isLeftRightSplit) { + mIsLeftRightSplit = isLeftRightSplit; + updateDimens(); + } + /** Sets touching state for this handle view. */ public void setTouching(boolean touching, boolean animate) { if (touching == mTouching) { 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 index 364bb651d55d..834c15d6b8d6 100644 --- 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 @@ -16,7 +16,6 @@ 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; @@ -47,6 +46,7 @@ public class DividerRoundedCorner extends View { private InvertedRoundedCornerDrawInfo mTopRightCorner; private InvertedRoundedCornerDrawInfo mBottomLeftCorner; private InvertedRoundedCornerDrawInfo mBottomRightCorner; + private boolean mIsLeftRightSplit; public DividerRoundedCorner(Context context, @Nullable AttributeSet attrs) { super(context, attrs); @@ -98,8 +98,8 @@ public class DividerRoundedCorner extends View { return false; } - private boolean isLandscape() { - return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; + void setIsLeftRightSplit(boolean isLeftRightSplit) { + mIsLeftRightSplit = isLeftRightSplit; } /** @@ -134,7 +134,7 @@ public class DividerRoundedCorner extends View { } private void calculateStartPos(Point outPos) { - if (isLandscape()) { + if (mIsLeftRightSplit) { // Place left corner at the right side of the divider bar. outPos.x = isLeftCorner() ? getWidth() / 2 + mDividerWidth / 2 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 0b0c6937553b..0f0fbd9cc12f 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 @@ -16,7 +16,6 @@ package com.android.wm.shell.common.split; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; @@ -27,6 +26,8 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import android.os.Bundle; import android.provider.DeviceConfig; @@ -65,12 +66,15 @@ 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; + private final Paint mPaint = new Paint(); + private final Rect mBackgroundRect = new Rect(); private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); private SplitLayout mSplitLayout; private SplitWindowManager mSplitWindowManager; private SurfaceControlViewHost mViewHost; private DividerHandleView mHandle; + private DividerRoundedCorner mCorners; private View mBackground; private int mTouchElevation; @@ -81,6 +85,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private boolean mInteractive; private boolean mSetTouchRegion = true; private int mLastDraggingPosition; + private int mHandleRegionWidth; + private int mHandleRegionHeight; /** * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with @@ -123,7 +129,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; - if (isLandscape()) { + if (mSplitLayout.isLeftRightSplit()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_left_full))); if (snapAlgorithm.isFirstSplitTargetAvailable()) { @@ -215,6 +221,17 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mViewHost = viewHost; layout.getDividerBounds(mDividerBounds); onInsetsChanged(insetsState, false /* animate */); + + final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit(); + mHandle.setIsLeftRightSplit(isLeftRightSplit); + mCorners.setIsLeftRightSplit(isLeftRightSplit); + + mHandleRegionWidth = getResources().getDimensionPixelSize(isLeftRightSplit + ? R.dimen.split_divider_handle_region_height + : R.dimen.split_divider_handle_region_width); + mHandleRegionHeight = getResources().getDimensionPixelSize(isLeftRightSplit + ? R.dimen.split_divider_handle_region_width + : R.dimen.split_divider_handle_region_height); } void onInsetsChanged(InsetsState insetsState, boolean animate) { @@ -255,30 +272,47 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { super.onFinishInflate(); mDividerBar = findViewById(R.id.divider_bar); mHandle = findViewById(R.id.docked_divider_handle); - mBackground = findViewById(R.id.docked_divider_background); + mCorners = findViewById(R.id.docked_divider_rounded_corner); mTouchElevation = getResources().getDimensionPixelSize( R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); mInteractive = true; setOnTouchListener(this); mHandle.setAccessibilityDelegate(mHandleDelegate); + setWillNotDraw(false); + mPaint.setColor(getResources().getColor(R.color.split_divider_background, null)); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); } @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()); + int startX = (mDividerBounds.width() - mHandleRegionWidth) / 2; + int startY = (mDividerBounds.height() - mHandleRegionHeight) / 2; + mTempRect.set(startX, startY, startX + mHandleRegionWidth, + startY + mHandleRegionHeight); mSplitWindowManager.setTouchRegion(mTempRect); mSetTouchRegion = false; } + + if (changed) { + boolean isHorizontalSplit = mSplitLayout.isLeftRightSplit(); + int dividerSize = getResources().getDimensionPixelSize(R.dimen.split_divider_bar_width); + left = isHorizontalSplit ? (getWidth() - dividerSize) / 2 : 0; + top = isHorizontalSplit ? 0 : (getHeight() - dividerSize) / 2; + right = isHorizontalSplit ? left + dividerSize : getWidth(); + bottom = isHorizontalSplit ? getHeight() : top + dividerSize; + mBackgroundRect.set(left, top, right, bottom); + } } @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { return PointerIcon.getSystemIcon(getContext(), - isLandscape() ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW); + mSplitLayout.isLeftRightSplit() ? TYPE_HORIZONTAL_DOUBLE_ARROW + : TYPE_VERTICAL_DOUBLE_ARROW); } @Override @@ -295,8 +329,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { // 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(); - final int touchPos = (int) (isLandscape ? event.getX() : event.getY()); + final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit(); + final int touchPos = (int) (isLeftRightSplit ? event.getX() : event.getY()); switch (action) { case MotionEvent.ACTION_DOWN: mVelocityTracker = VelocityTracker.obtain(); @@ -328,7 +362,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mVelocityTracker.addMovement(event); mVelocityTracker.computeCurrentVelocity(1000 /* units */); - final float velocity = isLandscape + final float velocity = isLeftRightSplit ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity(); final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; @@ -410,6 +444,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { .start(); } + @Override + protected void onDraw(@NonNull Canvas canvas) { + canvas.drawRect(mBackgroundRect, mPaint); + } + @VisibleForTesting void releaseHovering() { mHandle.setHovering(false, true); @@ -446,10 +485,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mHandle.setVisibility(!mInteractive && hideHandle ? View.INVISIBLE : View.VISIBLE); } - private boolean isLandscape() { - return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; - } - private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDoubleTap(MotionEvent e) { 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 63cdb4f151ff..b699533374df 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 @@ -79,7 +79,7 @@ import java.util.function.Consumer; * divide position changes. */ public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { - + private static final String TAG = "SplitLayout"; public static final int PARALLAX_NONE = 0; public static final int PARALLAX_DISMISSING = 1; public static final int PARALLAX_ALIGN_CENTER = 2; @@ -121,12 +121,15 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private int mDividerPosition; private boolean mInitialized = false; private boolean mFreezeDividerWindow = false; + private boolean mIsLargeScreen = false; private int mOrientation; private int mRotation; private int mDensity; private int mUiMode; private final boolean mDimNonImeSide; + private final boolean mAllowLeftRightSplitInPortrait; + private boolean mIsLeftRightSplit; private ValueAnimator mDividerFlingAnimator; public SplitLayout(String windowName, Context context, Configuration configuration, @@ -138,6 +141,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mOrientation = configuration.orientation; mRotation = configuration.windowConfiguration.getRotation(); mDensity = configuration.densityDpi; + mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; mSplitLayoutHandler = splitLayoutHandler; mDisplayController = displayController; mDisplayImeController = displayImeController; @@ -147,14 +151,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType); + final Resources res = mContext.getResources(); + mDimNonImeSide = res.getBoolean(R.bool.config_dimNonImeAttachedSide); + mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res); + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + configuration); + updateDividerConfig(mContext); mRootBounds.set(configuration.windowConfiguration.getBounds()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); resetDividerPosition(); - - mDimNonImeSide = mContext.getResources().getBoolean(R.bool.config_dimNonImeAttachedSide); - updateInvisibleRect(); } @@ -284,17 +291,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Returns the divider position as a fraction from 0 to 1. */ public float getDividerPositionAsFraction() { - return Math.min(1f, Math.max(0f, isLandscape() + return Math.min(1f, Math.max(0f, mIsLeftRightSplit ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); } private void updateInvisibleRect() { mInvisibleBounds.set(mRootBounds.left, mRootBounds.top, - isLandscape() ? mRootBounds.right / 2 : mRootBounds.right, - isLandscape() ? mRootBounds.bottom : mRootBounds.bottom / 2); - mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0, - isLandscape() ? 0 : mRootBounds.bottom); + mIsLeftRightSplit ? mRootBounds.right / 2 : mRootBounds.right, + mIsLeftRightSplit ? mRootBounds.bottom : mRootBounds.bottom / 2); + mInvisibleBounds.offset(mIsLeftRightSplit ? mRootBounds.right : 0, + mIsLeftRightSplit ? 0 : mRootBounds.bottom); } /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ @@ -309,6 +316,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final int orientation = configuration.orientation; final int density = configuration.densityDpi; final int uiMode = configuration.uiMode; + final boolean wasLeftRightSplit = mIsLeftRightSplit; if (mOrientation == orientation && mRotation == rotation @@ -326,9 +334,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mRotation = rotation; mDensity = density; mUiMode = uiMode; + mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + configuration); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); updateDividerConfig(mContext); - initDividerPosition(mTempRect); + initDividerPosition(mTempRect, wasLeftRightSplit); updateInvisibleRect(); return true; @@ -347,18 +358,27 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } // We only need new bounds here, other configuration should be update later. + final boolean wasLeftRightSplit = SplitScreenUtils.isLeftRightSplit( + mAllowLeftRightSplitInPortrait, mIsLargeScreen, + mRootBounds.width() >= mRootBounds.height()); mTempRect.set(mRootBounds); mRootBounds.set(tmpRect); + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + mIsLargeScreen, mRootBounds.width() >= mRootBounds.height()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); - initDividerPosition(mTempRect); + initDividerPosition(mTempRect, wasLeftRightSplit); } - private void initDividerPosition(Rect oldBounds) { + /** + * Updates the divider position to the position in the current orientation and bounds using the + * snap fraction calculated based on the previous orientation and bounds. + */ + private void initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit) { final float snapRatio = (float) mDividerPosition - / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height()); + / (float) (wasLeftRightSplit ? oldBounds.width() : oldBounds.height()); // Estimate position by previous ratio. final float length = - (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height()); + (float) (mIsLeftRightSplit ? mRootBounds.width() : mRootBounds.height()); final int estimatePosition = (int) (length * snapRatio); // Init divider position by estimated position using current bounds snap algorithm. mDividerPosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( @@ -376,8 +396,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange dividerBounds.set(mRootBounds); bounds1.set(mRootBounds); bounds2.set(mRootBounds); - final boolean isLandscape = isLandscape(mRootBounds); - if (isLandscape) { + if (mIsLeftRightSplit) { position += mRootBounds.left; dividerBounds.left = position - mDividerInsets; dividerBounds.right = dividerBounds.left + mDividerWindowWidth; @@ -393,7 +412,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */); DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */); if (setEffectBounds) { - mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape); + mSurfaceEffectPolicy.applyDividerPosition(position, mIsLeftRightSplit); } } @@ -563,13 +582,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) { - final boolean isLandscape = isLandscape(rootBounds); final Rect insets = getDisplayStableInsets(context); // Make split axis insets value same as the larger one to avoid bounds1 and bounds2 // have difference for avoiding size-compat mode when switching unresizable apps in // landscape while they are letterboxed. - if (!isLandscape) { + if (!mIsLeftRightSplit) { final int largerInsets = Math.max(insets.top, insets.bottom); insets.set(insets.left, largerInsets, insets.right, largerInsets); } @@ -579,9 +597,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange rootBounds.width(), rootBounds.height(), mDividerSize, - !isLandscape, + !mIsLeftRightSplit, insets, - isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); + mIsLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } /** Fling divider from current position to end or start position then exit */ @@ -643,13 +661,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** Switch both surface position with animation. */ public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, Consumer<Rect> finishCallback) { - final boolean isLandscape = isLandscape(); final Rect insets = getDisplayStableInsets(mContext); - insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top, - isLandscape ? insets.right : 0, isLandscape ? 0 : insets.bottom); + insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top, + mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom); final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( - isLandscape ? mBounds2.width() : mBounds2.height()).position; + mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position; final Rect distBounds1 = new Rect(); final Rect distBounds2 = new Rect(); final Rect distDividerBounds = new Rect(); @@ -740,15 +757,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange .toRect(); } - private static boolean isLandscape(Rect bounds) { - return bounds.width() > bounds.height(); - } - /** - * Return if this layout is landscape. + * @return {@code true} if we should create a left-right split, {@code false} if we should + * create a top-bottom split. */ - public boolean isLandscape() { - return isLandscape(mRootBounds); + public boolean isLeftRightSplit() { + return mIsLeftRightSplit; } /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ @@ -850,9 +864,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** 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()); + final String innerPrefix = prefix + "\t"; + pw.println(prefix + TAG + ":"); + pw.println(innerPrefix + "mAllowLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait); + pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit); + pw.println(innerPrefix + "bounds1=" + mBounds1.toShortString()); + pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString()); + pw.println(innerPrefix + "bounds2=" + mBounds2.toShortString()); } /** Handles layout change event. */ @@ -937,32 +955,32 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * 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 + * @param isLeftRightSplit indicates whether it's splitting horizontally or vertically */ - void applyDividerPosition(int position, boolean isLandscape) { + void applyDividerPosition(int position, boolean isLeftRightSplit) { mDismissingSide = DOCKED_INVALID; mParallaxOffset.set(0, 0); mDismissingDimValue = 0; int totalDismissingDistance = 0; if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) { - mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; + mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position - mDividerSnapAlgorithm.getFirstSplitTarget().position; } else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) { - mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; + mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position - mDividerSnapAlgorithm.getDismissEndTarget().position; } - final boolean topLeftShrink = isLandscape + final boolean topLeftShrink = isLeftRightSplit ? position < mWinBounds1.right : position < mWinBounds1.bottom; if (topLeftShrink) { - mShrinkSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; + mShrinkSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; mContentBounds.set(mWinBounds1); mSurfaceBounds.set(mBounds1); } else { - mShrinkSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; + mShrinkSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; mContentBounds.set(mWinBounds2); mSurfaceBounds.set(mBounds2); } @@ -973,7 +991,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); if (mParallaxType == PARALLAX_DISMISSING) { fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); - if (isLandscape) { + if (isLeftRightSplit) { mParallaxOffset.x = (int) (fraction * totalDismissingDistance); } else { mParallaxOffset.y = (int) (fraction * totalDismissingDistance); @@ -982,7 +1000,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } if (mParallaxType == PARALLAX_ALIGN_CENTER) { - if (isLandscape) { + if (isLeftRightSplit) { mParallaxOffset.x = (mSurfaceBounds.width() - mContentBounds.width()) / 2; } else { @@ -1129,7 +1147,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Calculate target bounds offset for IME mLastYOffset = mYOffsetForIme; final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT - && !isFloating && !isLandscape(mRootBounds) && mImeShown; + && !isFloating && !mIsLeftRightSplit && mImeShown; mTargetYOffset = needOffset ? getTargetYOffset() : 0; if (mTargetYOffset != mLastYOffset) { 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 index e73430056c89..49db8d9c54a6 100644 --- 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 @@ -26,11 +26,12 @@ import android.annotation.IntDef; /** Helper utility class of methods and constants that are available to be imported in Launcher. */ public class SplitScreenConstants { - /** - * Duration used for every split fade-in or fade-out. - */ + /** Duration used for every split fade-in or fade-out. */ public static final int FADE_DURATION = 133; + /** Key for passing in widget intents when invoking split from launcher workspace. */ + public static final String KEY_EXTRA_WIDGET_INTENT = "key_extra_widget_intent"; + /////////////// // IMPORTANT for the following SPLIT_POSITION and SNAP_TO constants: // These int values must not be changed -- they are persisted to user-defined app pairs, and diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index d7ea1c0c620d..0693543515b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -16,6 +16,8 @@ package com.android.wm.shell.common.split; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; + import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -25,9 +27,14 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import android.annotation.Nullable; import android.app.ActivityManager; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; import com.android.internal.util.ArrayUtils; +import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; /** Helper utility class for split screen components to use. */ @@ -94,4 +101,38 @@ public class SplitScreenUtils { public static String splitFailureMessage(String caller, String reason) { return "(" + caller + ") Splitscreen aborted: " + reason; } + + /** + * Returns whether left/right split is allowed in portrait. + */ + public static boolean allowLeftRightSplitInPortrait(Resources res) { + return Flags.enableLeftRightSplitInPortrait() && res.getBoolean( + com.android.internal.R.bool.config_leftRightSplitInPortrait); + } + + /** + * Returns whether left/right split is supported in the given configuration. + */ + public static boolean isLeftRightSplit(boolean allowLeftRightSplitInPortrait, + Configuration config) { + // Compare the max bounds sizes as on near-square devices, the insets may result in a + // configuration in the other orientation + final boolean isLargeScreen = config.smallestScreenWidthDp >= 600; + final Rect maxBounds = config.windowConfiguration.getMaxBounds(); + final boolean isLandscape = maxBounds.width() >= maxBounds.height(); + return isLeftRightSplit(allowLeftRightSplitInPortrait, isLargeScreen, isLandscape); + } + + /** + * Returns whether left/right split is supported in the given configuration state. This method + * is useful for cases where we need to calculate this given last saved state. + */ + public static boolean isLeftRightSplit(boolean allowLeftRightSplitInPortrait, + boolean isLargeScreen, boolean isLandscape) { + if (allowLeftRightSplitInPortrait && isLargeScreen) { + return !isLandscape; + } else { + return isLandscape; + } + } } 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 index 54cf84c32276..3c6bc1754c5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -221,34 +221,57 @@ public abstract class WMShellBaseModule { Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, - CompatUIController compatUI, + Optional<CompatUIController> compatUI, Optional<UnfoldAnimationController> unfoldAnimationController, Optional<RecentTasksController> recentTasksOptional, - @ShellMainThread ShellExecutor mainExecutor - ) { + @ShellMainThread ShellExecutor mainExecutor) { if (!context.getResources().getBoolean(R.bool.config_registerShellTaskOrganizerOnInit)) { // TODO(b/238217847): Force override shell init if registration is disabled shellInit = new ShellInit(mainExecutor); } - return new ShellTaskOrganizer(shellInit, shellCommandHandler, compatUI, - unfoldAnimationController, recentTasksOptional, mainExecutor); + return new ShellTaskOrganizer( + shellInit, + shellCommandHandler, + compatUI.orElse(null), + unfoldAnimationController, + recentTasksOptional, + mainExecutor); } @WMSingleton @Provides - static CompatUIController provideCompatUIController(Context context, + static Optional<CompatUIController> provideCompatUIController( + Context context, ShellInit shellInit, ShellController shellController, - DisplayController displayController, DisplayInsetsController displayInsetsController, - DisplayImeController imeController, SyncTransactionQueue syncQueue, - @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy, - DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, - CompatUIShellCommandHandler compatUIShellCommandHandler, - AccessibilityManager accessibilityManager) { - return new CompatUIController(context, shellInit, shellController, displayController, - displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy, - dockStateReader, compatUIConfiguration, compatUIShellCommandHandler, - accessibilityManager); + DisplayController displayController, + DisplayInsetsController displayInsetsController, + DisplayImeController imeController, + SyncTransactionQueue syncQueue, + @ShellMainThread ShellExecutor mainExecutor, + Lazy<Transitions> transitionsLazy, + Lazy<DockStateReader> dockStateReader, + Lazy<CompatUIConfiguration> compatUIConfiguration, + Lazy<CompatUIShellCommandHandler> compatUIShellCommandHandler, + Lazy<AccessibilityManager> accessibilityManager) { + if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { + return Optional.empty(); + } + return Optional.of( + new CompatUIController( + context, + shellInit, + shellController, + displayController, + displayInsetsController, + imeController, + syncQueue, + mainExecutor, + transitionsLazy, + dockStateReader.get(), + compatUIConfiguration.get(), + compatUIShellCommandHandler.get(), + accessibilityManager.get())); } @WMSingleton @@ -301,13 +324,16 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static SystemPerformanceHinter provideSystemPerformanceHinter(Context context, + static Optional<SystemPerformanceHinter> provideSystemPerformanceHinter(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, RootTaskDisplayAreaOrganizer rootTdaOrganizer) { + if (!com.android.window.flags.Flags.explicitRefreshRateHints()) { + return Optional.empty(); + } final PerfHintController perfHintController = new PerfHintController(context, shellInit, shellCommandHandler, rootTdaOrganizer); - return perfHintController.getHinter(); + return Optional.of(perfHintController.getHinter()); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java index a9675f976fa9..1947097c2f15 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java @@ -20,6 +20,8 @@ import android.content.Context; import android.os.Handler; import android.os.SystemClock; +import androidx.annotation.NonNull; + import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; @@ -41,7 +43,6 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipParamsChangedForwarder; 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.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipBoundsController; @@ -78,11 +79,12 @@ public abstract class TvPipModule { PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, + PipTransitionState pipTransitionState, PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, - PipTransitionController pipTransitionController, + TvPipTransition tvPipTransition, TvPipNotificationController tvPipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, @@ -99,9 +101,10 @@ public abstract class TvPipModule { pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipBoundsController, + pipTransitionState, pipAppOpsListener, pipTaskOrganizer, - pipTransitionController, + tvPipTransition, tvPipMenuController, pipMediaController, tvPipNotificationController, @@ -151,25 +154,23 @@ public abstract class TvPipModule { return new LegacySizeSpecSource(context, pipDisplayLayoutState); } - // Handler needed for loadDrawableAsync() in PipControlsViewController @WMSingleton @Provides - static PipTransitionController provideTvPipTransition( + static TvPipTransition provideTvPipTransition( Context context, - ShellInit shellInit, - ShellTaskOrganizer shellTaskOrganizer, - Transitions transitions, + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, TvPipBoundsState tvPipBoundsState, - PipDisplayLayoutState pipDisplayLayoutState, - PipTransitionState pipTransitionState, - TvPipMenuController pipMenuController, + TvPipMenuController tvPipMenuController, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + PipTransitionState pipTransitionState, PipAnimationController pipAnimationController, - PipSurfaceTransactionHelper pipSurfaceTransactionHelper) { + PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + PipDisplayLayoutState pipDisplayLayoutState) { return new TvPipTransition(context, shellInit, shellTaskOrganizer, transitions, - tvPipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController, - tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper, - Optional.empty()); + tvPipBoundsState, tvPipMenuController, tvPipBoundsAlgorithm, pipTransitionState, + pipAnimationController, pipSurfaceTransactionHelper, pipDisplayLayoutState); } @WMSingleton @@ -207,7 +208,7 @@ public abstract class TvPipModule { PipTransitionState pipTransitionState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, - PipTransitionController pipTransitionController, + TvPipTransition tvPipTransition, PipParamsChangedForwarder pipParamsChangedForwarder, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreenController> splitScreenControllerOptional, @@ -217,7 +218,7 @@ public abstract class TvPipModule { return new TvPipTaskOrganizer(context, syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, - pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, + pipSurfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder, splitScreenControllerOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 4a9ea6fed73f..144555dd70c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -21,7 +21,6 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN -import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.Context @@ -321,24 +320,10 @@ class DesktopTasksController( } /** Move a task with given `taskId` to fullscreen */ - fun moveToFullscreen(taskId: Int) { - shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) } - } - - /** Move a task to fullscreen */ - fun moveToFullscreen(task: RunningTaskInfo) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToFullscreen taskId=%d", - task.taskId - ) - - val wct = WindowContainerTransaction() - addMoveToFullscreenChanges(wct, task) - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) - } else { - shellTaskOrganizer.applyTransaction(wct) + fun moveToFullscreen(taskId: Int, windowDecor: DesktopModeWindowDecoration) { + shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> + windowDecor.incrementRelayoutBlock() + moveToFullscreenWithAnimation(task, task.positionInParent) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 75d27d99b9ac..95d7ad5c416f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -25,12 +25,14 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TransitionHandler +import com.android.wm.shell.util.KtProtoLog import com.android.wm.shell.util.TransitionUtil import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.MoveToDesktopAnimator @@ -68,6 +70,10 @@ class DragToDesktopTransitionHandler( private var splitScreenController: SplitScreenController? = null private var transitionState: TransitionState? = null + /** Whether a drag-to-desktop transition is in progress. */ + val inProgress: Boolean + get() = transitionState != null + /** Sets a listener to receive callback about events during the transition animation. */ fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) { dragToDesktopStateListener = listener @@ -92,19 +98,22 @@ class DragToDesktopTransitionHandler( dragToDesktopAnimator: MoveToDesktopAnimator, windowDecoration: DesktopModeWindowDecoration ) { - if (transitionState != null) { + if (inProgress) { error("A drag to desktop is already in progress") } val options = ActivityOptions.makeBasic().apply { setTransientLaunch() setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis()) + pendingIntentCreatorBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED } val pendingIntent = PendingIntent.getActivity( context, 0 /* requestCode */, launchHomeIntent, - FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT + FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT, + options.toBundle() ) val wct = WindowContainerTransaction() wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) @@ -135,6 +144,12 @@ class DragToDesktopTransitionHandler( * inside the desktop drop zone. */ fun finishDragToDesktopTransition(wct: WindowContainerTransaction) { + if (requireTransitionState().startAborted) { + // Don't attempt to complete the drag-to-desktop since the start transition didn't + // succeed as expected. Just reset the state as if nothing happened. + clearState() + return + } transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) } @@ -147,6 +162,12 @@ class DragToDesktopTransitionHandler( */ fun cancelDragToDesktopTransition() { val state = requireTransitionState() + if (state.startAborted) { + // Don't attempt to cancel the drag-to-desktop since the start transition didn't + // succeed as expected. Just reset the state as if nothing happened. + clearState() + return + } state.cancelled = true if (state.draggedTaskChange != null) { // Regular case, transient launch of Home happened as is waiting for the cancel @@ -409,6 +430,21 @@ class DragToDesktopTransitionHandler( return null } + override fun onTransitionConsumed( + transition: IBinder, + aborted: Boolean, + finishTransaction: SurfaceControl.Transaction? + ) { + val state = transitionState ?: return + if (aborted && state.startTransitionToken == transition) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DragToDesktop: onTransitionConsumed() start transition aborted" + ) + state.startAborted = true + } + } + private fun isHomeChange(change: Change): Boolean { return change.taskInfo?.activityType == ACTIVITY_TYPE_HOME } @@ -508,6 +544,7 @@ class DragToDesktopTransitionHandler( abstract var homeToken: WindowContainerToken? abstract var draggedTaskChange: Change? abstract var cancelled: Boolean + abstract var startAborted: Boolean data class FromFullscreen( override val draggedTaskId: Int, @@ -520,6 +557,7 @@ class DragToDesktopTransitionHandler( override var homeToken: WindowContainerToken? = null, override var draggedTaskChange: Change? = null, override var cancelled: Boolean = false, + override var startAborted: Boolean = false, ) : TransitionState() data class FromSplit( override val draggedTaskId: Int, @@ -532,6 +570,7 @@ class DragToDesktopTransitionHandler( override var homeToken: WindowContainerToken? = null, override var draggedTaskChange: Change? = null, override var cancelled: Boolean = false, + override var startAborted: Boolean = false, var splitRootChange: Change? = null, ) : TransitionState() } 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 0bf8ec32c6c0..fdfb6f3680b2 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 @@ -94,6 +94,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll private ShellExecutor mMainExecutor; private ArrayList<DragAndDropListener> mListeners = new ArrayList<>(); + // Map of displayId -> per-display info private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); /** @@ -362,7 +363,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll */ private boolean isReadyToHandleDrag() { for (int i = 0; i < mDisplayDropTargets.size(); i++) { - if (mDisplayDropTargets.valueAt(i).mHasDrawn) { + if (mDisplayDropTargets.valueAt(i).hasDrawn) { return true; } } @@ -398,8 +399,13 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll * Dumps information about this controller. */ public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; pw.println(prefix + TAG); - pw.println(prefix + " listeners=" + mListeners.size()); + pw.println(innerPrefix + "listeners=" + mListeners.size()); + pw.println(innerPrefix + "Per display:"); + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + mDisplayDropTargets.valueAt(i).dump(pw, innerPrefix); + } } /** @@ -440,7 +446,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll final FrameLayout rootView; final DragLayout dragLayout; // Tracks whether the window has fully drawn since it was last made visible - boolean mHasDrawn; + boolean hasDrawn; boolean isHandlingDrag; // A count of the number of active drags in progress to ensure that we only hide the window @@ -464,17 +470,29 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll rootView.setVisibility(visibility); if (visibility == View.VISIBLE) { rootView.requestApplyInsets(); - if (!mHasDrawn && rootView.getViewRootImpl() != null) { + if (!hasDrawn && rootView.getViewRootImpl() != null) { rootView.getViewRootImpl().registerRtFrameCallback(this); } } else { - mHasDrawn = false; + hasDrawn = false; } } @Override public void onFrameDraw(long frame) { - mHasDrawn = true; + hasDrawn = true; + } + + /** + * Dumps information about this display's shell drop target. + */ + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(innerPrefix + "displayId=" + displayId); + pw.println(innerPrefix + "hasDrawn=" + hasDrawn); + pw.println(innerPrefix + "isHandlingDrag=" + isHandlingDrag); + pw.println(innerPrefix + "activeDragCount=" + activeDragCount); + dragLayout.dump(pw, innerPrefix); } } } 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 e70768b6b752..a31a773a76a0 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 @@ -40,6 +40,7 @@ import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPL 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 android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; @@ -138,7 +139,7 @@ public class DragAndDropPolicy { final Rect displayRegion = new Rect(l, t, l + iw, t + ih); final Rect fullscreenDrawRegion = new Rect(displayRegion); final Rect fullscreenHitRegion = new Rect(displayRegion); - final boolean inLandscape = mSession.displayLayout.isLandscape(); + final boolean isLeftRightSplit = mSplitScreen != null && mSplitScreen.isLeftRightSplit(); final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); final float dividerWidth = mContext.getResources().getDimensionPixelSize( R.dimen.split_divider_bar_width); @@ -155,7 +156,7 @@ public class DragAndDropPolicy { topOrLeftBounds.intersect(displayRegion); bottomOrRightBounds.intersect(displayRegion); - if (inLandscape) { + if (isLeftRightSplit) { final Rect leftHitRegion = new Rect(); final Rect rightHitRegion = new Rect(); @@ -246,8 +247,15 @@ public class DragAndDropPolicy { @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) - ? intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) : new Bundle(); + final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); + baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + final Bundle opts = baseActivityOpts.toBundle(); + if (intent.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { + opts.putAll(intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); + } + // Put BAL flags to avoid activity start aborted. + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); + opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); final UserHandle user = intent.getParcelableExtra(EXTRA_USER); if (isTask) { @@ -259,9 +267,6 @@ public class DragAndDropPolicy { mStarter.startShortcut(packageName, id, position, opts, user); } else { final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); - // Put BAL flags to avoid activity start aborted. - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); mStarter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, position, opts); } 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 205a455200bd..445ba897c173 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 @@ -20,6 +20,7 @@ import static android.app.StatusBarManager.DISABLE_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -47,14 +48,18 @@ 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.animation.Interpolators; +import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +import java.io.PrintWriter; import java.util.ArrayList; /** @@ -74,6 +79,11 @@ public class DragLayout extends LinearLayout { private final StatusBarManager mStatusBarManager; private final Configuration mLastConfiguration = new Configuration(); + // Whether this device supports left/right split in portrait + private final boolean mAllowLeftRightSplitInPortrait; + // Whether the device is currently in left/right split mode + private boolean mIsLeftRightSplit; + private DragAndDropPolicy.Target mCurrentTarget = null; private DropZoneView mDropZoneView1; private DropZoneView mDropZoneView2; @@ -106,17 +116,18 @@ public class DragLayout extends LinearLayout { setLayoutDirection(LAYOUT_DIRECTION_LTR); 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)); + 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; - int orientation = getResources().getConfiguration().orientation; - setOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE - ? LinearLayout.HORIZONTAL - : LinearLayout.VERTICAL); - updateContainerMargins(getResources().getConfiguration().orientation); + // We don't use the configuration orientation here to determine landscape because + // near-square devices may report the same orietation with insets taken into account + mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait( + context.getResources()); + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + getResources().getConfiguration()); + setOrientation(mIsLeftRightSplit ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); + updateContainerMargins(mIsLeftRightSplit); } @Override @@ -124,11 +135,12 @@ public class DragLayout extends LinearLayout { mInsets = insets.getInsets(Type.tappableElement() | Type.displayCutout()); recomputeDropTargets(); - final int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + boolean isLeftRightSplit = mSplitScreenController != null + && mSplitScreenController.isLeftRightSplit(); + if (isLeftRightSplit) { mDropZoneView1.setBottomInset(mInsets.bottom); mDropZoneView2.setBottomInset(mInsets.bottom); - } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + } else { mDropZoneView1.setBottomInset(0); mDropZoneView2.setBottomInset(mInsets.bottom); } @@ -136,14 +148,12 @@ public class DragLayout extends LinearLayout { } public void onConfigChanged(Configuration newConfig) { - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE - && getOrientation() != HORIZONTAL) { - setOrientation(LinearLayout.HORIZONTAL); - updateContainerMargins(newConfig.orientation); - } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT - && getOrientation() != VERTICAL) { - setOrientation(LinearLayout.VERTICAL); - updateContainerMargins(newConfig.orientation); + boolean isLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + newConfig); + if (isLeftRightSplit != mIsLeftRightSplit) { + mIsLeftRightSplit = isLeftRightSplit; + setOrientation(mIsLeftRightSplit ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); + updateContainerMargins(mIsLeftRightSplit); } final int diff = newConfig.diff(mLastConfiguration); @@ -162,14 +172,14 @@ public class DragLayout extends LinearLayout { mDropZoneView2.setContainerMargin(0, 0, 0, 0); } - private void updateContainerMargins(int orientation) { + private void updateContainerMargins(boolean isLeftRightSplit) { final float halfMargin = mDisplayMargin / 2f; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (isLeftRightSplit) { mDropZoneView1.setContainerMargin( mDisplayMargin, mDisplayMargin, halfMargin, mDisplayMargin); mDropZoneView2.setContainerMargin( halfMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin); - } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + } else { mDropZoneView1.setContainerMargin( mDisplayMargin, mDisplayMargin, mDisplayMargin, halfMargin); mDropZoneView2.setContainerMargin( @@ -257,23 +267,21 @@ public class DragLayout extends LinearLayout { * @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 { + if (mIsLeftRightSplit) { 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; + } else { + 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; } dropZoneView1.weight = bounds1 != null ? 0 : 1; dropZoneView2.weight = bounds2 != null ? 0 : 1; @@ -371,7 +379,7 @@ public class DragLayout extends LinearLayout { // Reset the state if we previously force-ignore the bottom margin mDropZoneView1.setForceIgnoreBottomMargin(false); mDropZoneView2.setForceIgnoreBottomMargin(false); - updateContainerMargins(getResources().getConfiguration().orientation); + updateContainerMargins(mIsLeftRightSplit); mCurrentTarget = null; } @@ -481,4 +489,19 @@ public class DragLayout extends LinearLayout { final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); } + + /** + * Dumps information about this drag layout. + */ + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + "DragLayout:"); + pw.println(innerPrefix + "mIsLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait); + pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit); + pw.println(innerPrefix + "mDisplayMargin=" + mDisplayMargin); + pw.println(innerPrefix + "mDividerSize=" + mDividerSize); + pw.println(innerPrefix + "mIsShowing=" + mIsShowing); + pw.println(innerPrefix + "mHasDropped=" + mHasDropped); + pw.println(innerPrefix + "mCurrentTarget=" + mCurrentTarget); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java index 478b6a9d95f6..353d702e5bc4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java @@ -18,31 +18,17 @@ package com.android.wm.shell.draganddrop; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.content.ClipDescription.EXTRA_PENDING_INTENT; -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.content.Intent.EXTRA_USER; import android.app.ActivityManager; import android.app.ActivityTaskManager; -import android.app.PendingIntent; import android.app.WindowConfiguration; import android.content.ClipData; -import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.net.Uri; -import android.os.UserHandle; - -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import com.android.wm.shell.common.DisplayLayout; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.List; /** 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 cbed4b5a501f..a58d94ecd19b 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 @@ -81,15 +81,35 @@ public class PipSurfaceTransactionHelper { */ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, Rect sourceBounds, Rect destinationBounds) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, RectF destinationBounds) { return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */); } /** - * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. + * Operates the scale (setMatrix) on a given transaction and leash * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, Rect sourceBounds, Rect destinationBounds, float degrees) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, RectF destinationBounds, float degrees) { mTmpSourceRectF.set(sourceBounds); // We want the matrix to position the surface relative to the screen coordinates so offset // the source to 0,0 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 c05601b3f04f..c1164fca22f2 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 @@ -123,7 +123,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private static final int EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS = SystemProperties.getInt( - "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 0); + "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 400); private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; @@ -297,9 +297,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // changed RunningTaskInfo when it finishes. private ActivityManager.RunningTaskInfo mDeferredTaskInfo; private WindowContainerToken mToken; - private SurfaceControl mLeash; + protected SurfaceControl mLeash; protected PipTransitionState mPipTransitionState; - private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + protected PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; protected PictureInPictureParams mPictureInPictureParams; private IntConsumer mOnDisplayIdChangeCallback; @@ -973,7 +973,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } - cancelCurrentAnimator(); + cancelAnimationOnTaskVanished(); onExitPipFinished(info); if (Transitions.ENABLE_SHELL_TRANSITIONS) { @@ -981,6 +981,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } } + protected void cancelAnimationOnTaskVanished() { + cancelCurrentAnimator(); + } + @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); @@ -1100,7 +1104,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** Called when exiting PIP transition is finished to do the state cleanup. */ - void onExitPipFinished(TaskInfo info) { + public void onExitPipFinished(TaskInfo info) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onExitPipFinished: %s, state=%s leash=%s", info.topActivity, mPipTransitionState, mLeash); 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 d5fab441cd46..fe4980a9eb16 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 @@ -554,6 +554,11 @@ public class PipTransition extends PipTransitionController { } } } + // if overlay is present remove it immediately, as exit transition came before it faded out + if (mPipOrganizer.mSwipePipToHomeOverlay != null) { + startTransaction.remove(mPipOrganizer.mSwipePipToHomeOverlay); + clearSwipePipToHomeOverlay(); + } if (pipChange == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: No window of exiting PIP is found. Can't play expand animation", TAG); @@ -1007,7 +1012,6 @@ public class PipTransition extends PipTransitionController { // the overlay to the final PIP task. startTransaction.reparent(swipePipToHomeOverlay, leash) .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE); - mPipOrganizer.mSwipePipToHomeOverlay = null; } final Rect sourceBounds = pipTaskInfo.configuration.windowConfiguration.getBounds(); @@ -1029,7 +1033,7 @@ public class PipTransition extends PipTransitionController { sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); if (swipePipToHomeOverlay != null) { mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay, - null /* callback */, false /* withStartDelay */); + this::clearSwipePipToHomeOverlay /* callback */, false /* withStartDelay */); } mPipTransitionState.setInSwipePipToHomeTransition(false); } @@ -1173,6 +1177,10 @@ public class PipTransition extends PipTransitionController { mPipMenuController.updateMenuBounds(destinationBounds); } + private void clearSwipePipToHomeOverlay() { + mPipOrganizer.mSwipePipToHomeOverlay = null; + } + @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java index a48e969fde35..72c0cd71f198 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -44,6 +44,7 @@ import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import java.util.Collections; import java.util.Set; /** @@ -101,12 +102,29 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0 && !mTvPipBoundsState.isTvPipManuallyCollapsed(); if (isPipExpanded) { - updateGravityOnExpansionToggled(/* expanding= */ true); + updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded); } mTvPipBoundsState.setTvPipExpanded(isPipExpanded); return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); } + @Override + public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: getEntryDestinationBoundsIgnoringKeepClearAreas()", TAG); + + updateExpandedPipSize(); + final boolean isPipExpanded = mTvPipBoundsState.isTvExpandedPipSupported() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0 + && !mTvPipBoundsState.isTvPipManuallyCollapsed(); + if (isPipExpanded) { + updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded); + } + mTvPipBoundsState.setTvPipExpanded(isPipExpanded); + return adjustBoundsForTemporaryDecor(getTvPipPlacement(Collections.emptySet(), + Collections.emptySet()).getUnstashedBounds()); + } + /** Returns the current bounds adjusted to the new aspect ratio, if valid. */ @Override public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) { @@ -133,16 +151,25 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { */ @NonNull public Placement getTvPipPlacement() { + final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas(); + final Set<Rect> unrestrictedKeepClearAreas = + mTvPipBoundsState.getUnrestrictedKeepClearAreas(); + + return getTvPipPlacement(restrictedKeepClearAreas, unrestrictedKeepClearAreas); + } + + /** + * Calculates the PiP bounds. + */ + @NonNull + private Placement getTvPipPlacement(Set<Rect> restrictedKeepClearAreas, + Set<Rect> unrestrictedKeepClearAreas) { final Size pipSize = getPipSize(); final Rect displayBounds = mTvPipBoundsState.getDisplayBounds(); final Size screenSize = new Size(displayBounds.width(), displayBounds.height()); final Rect insetBounds = new Rect(); getInsetBounds(insetBounds); - final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas(); - final Set<Rect> unrestrictedKeepClearAreas = - mTvPipBoundsState.getUnrestrictedKeepClearAreas(); - mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity()); mKeepClearAlgorithm.setScreenSize(screenSize); mKeepClearAlgorithm.setMovementBounds(insetBounds); @@ -189,8 +216,11 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { int updatedGravity; if (expanding) { - // Save collapsed gravity. - mTvPipBoundsState.setTvPipPreviousCollapsedGravity(mTvPipBoundsState.getTvPipGravity()); + if (!mTvPipBoundsState.isTvPipExpanded()) { + // Save collapsed gravity. + mTvPipBoundsState.setTvPipPreviousCollapsedGravity( + mTvPipBoundsState.getTvPipGravity()); + } if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) { updatedGravity = Gravity.CENTER_HORIZONTAL | currentY; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java index 2b3a93e3c3e8..5ee3734e371d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java @@ -131,6 +131,7 @@ public class TvPipBoundsState extends PipBoundsState { mTvFixedPipOrientation = ORIENTATION_UNDETERMINED; mTvPipGravity = mDefaultGravity; mPreviousCollapsedGravity = mDefaultGravity; + mIsTvPipExpanded = false; mTvPipManuallyCollapsed = false; } 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 72115fdefa05..cd3d38b6500c 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 @@ -56,6 +56,7 @@ import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipParamsChangedForwarder; 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.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellController; @@ -122,6 +123,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final PipDisplayLayoutState mPipDisplayLayoutState; private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; private final TvPipBoundsController mTvPipBoundsController; + private final PipTransitionState mPipTransitionState; private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; @@ -157,6 +159,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, + PipTransitionState pipTransitionState, PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, @@ -177,6 +180,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipBoundsController, + pipTransitionState, pipAppOpsListener, pipTaskOrganizer, pipTransitionController, @@ -199,6 +203,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipDisplayLayoutState pipDisplayLayoutState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, TvPipBoundsController tvPipBoundsController, + PipTransitionState pipTransitionState, PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, @@ -212,6 +217,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal Handler mainHandler, ShellExecutor mainExecutor) { mContext = context; + mPipTransitionState = pipTransitionState; mMainHandler = mainHandler; mMainExecutor = mainExecutor; mShellController = shellController; @@ -365,7 +371,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */); - onPipDisappeared(); } private void togglePipExpansion() { @@ -420,6 +425,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) { + if (!mPipTransitionState.hasEnteredPip()) { + // Do not schedule a move animation while we're still transitioning into/out of PiP + return; + } + mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds, animationDuration, null); mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds); @@ -447,7 +457,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal return; } mPipTaskOrganizer.removePip(); - onPipDisappeared(); + mTvPipMenuController.closeMenu(); } @Override @@ -477,7 +487,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPipNotificationController.dismiss(); mActionBroadcastReceiver.unregister(); - mTvPipMenuController.closeMenu(); + mTvPipMenuController.detach(); mTvPipActionsProvider.reset(); mTvPipBoundsState.resetTvPipState(); mTvPipBoundsController.reset(); @@ -501,8 +511,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void onPipTransitionCanceled(int direction) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); - mTvPipMenuController.onPipTransitionFinished( - PipAnimationController.isInPipDirection(direction)); mTvPipActionsProvider.updatePipExpansionState(mTvPipBoundsState.isTvPipExpanded()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index ee55211a73a9..c6803f7beebd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -262,8 +262,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void detach() { - closeMenu(); detachPipMenu(); + switchToMenuMode(MODE_NO_MENU); mLeash = null; } @@ -320,10 +320,21 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction pipTx, Rect pipBounds, float alpha) { + movePipMenu(pipTx, pipBounds, alpha); + } + + /** + * Move the PiP menu with the given bounds and update its opacity. + * The PiP SurfaceControl is given if there is a need to synchronize the movements + * on the same frame as PiP. + */ + public void movePipMenu(@Nullable SurfaceControl.Transaction pipTx, @Nullable Rect pipBounds, + float alpha) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePipMenu: %s, alpha %s", TAG, pipBounds.toShortString(), alpha); + "%s: movePipMenu: %s, alpha %s", TAG, + pipBounds != null ? pipBounds.toShortString() : null, alpha); - if (pipBounds.isEmpty()) { + if ((pipBounds == null || pipBounds.isEmpty()) && alpha == ALPHA_NO_CHANGE) { if (pipTx == null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: no transaction given", TAG); @@ -334,28 +345,36 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis return; } - final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView); - final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView); - final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds); if (pipTx == null) { pipTx = new SurfaceControl.Transaction(); } - pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top); - pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top); + + final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView); + final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView); + + if (pipBounds != null) { + final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds); + pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top); + pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top); + updateMenuBounds(pipBounds); + } if (alpha != ALPHA_NO_CHANGE) { pipTx.setAlpha(frontSurface, alpha); pipTx.setAlpha(backSurface, alpha); } - // Synchronize drawing the content in the front and back surfaces together with the pip - // transaction and the position change for the front and back surfaces - final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip"); - syncGroup.add(mPipMenuView.getRootSurfaceControl(), null); - syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null); - updateMenuBounds(pipBounds); - syncGroup.addTransaction(pipTx); - syncGroup.markSyncReady(); + if (pipBounds != null) { + // Synchronize drawing the content in the front and back surfaces together with the pip + // transaction and the position change for the front and back surfaces + final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip"); + syncGroup.add(mPipMenuView.getRootSurfaceControl(), null); + syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null); + syncGroup.addTransaction(pipTx); + syncGroup.markSyncReady(); + } else { + pipTx.apply(); + } } private boolean isMenuAttached() { @@ -388,14 +407,19 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString()); - mSystemWindows.updateViewLayout(mPipBackgroundView, - getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(), - menuBounds.height())); - mSystemWindows.updateViewLayout(mPipMenuView, - getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(), - menuBounds.height())); - if (mPipMenuView != null) { - mPipMenuView.setPipBounds(pipBounds); + + boolean needsRelayout = mPipBackgroundView.getLayoutParams().width != menuBounds.width() + || mPipBackgroundView.getLayoutParams().height != menuBounds.height(); + if (needsRelayout) { + mSystemWindows.updateViewLayout(mPipBackgroundView, + getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(), + menuBounds.height())); + mSystemWindows.updateViewLayout(mPipMenuView, + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(), + menuBounds.height())); + if (mPipMenuView != null) { + mPipMenuView.setPipBounds(pipBounds); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java index f86f987039ba..202d36f0dfbd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java @@ -168,6 +168,9 @@ class TvPipMenuEduTextDrawer extends FrameLayout { * that the edu text will be marqueed */ private boolean isEduTextMarqueed() { + if (mEduTextView.getLayout() == null) { + return false; + } final int availableWidth = (int) mEduTextView.getWidth() - mEduTextView.getCompoundPaddingLeft() - mEduTextView.getCompoundPaddingRight(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index f315afba9a03..21223c9ac362 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -35,7 +35,6 @@ import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipParamsChangedForwarder; 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.splitscreen.SplitScreenController; @@ -46,6 +45,7 @@ import java.util.Optional; * TV specific changes to the PipTaskOrganizer. */ public class TvPipTaskOrganizer extends PipTaskOrganizer { + private final TvPipTransition mTvPipTransition; public TvPipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @@ -56,7 +56,7 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, - @NonNull PipTransitionController pipTransitionController, + @NonNull TvPipTransition tvPipTransition, @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @@ -65,9 +65,10 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { ShellExecutor mainExecutor) { super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController, - surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, + surfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder, splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); + mTvPipTransition = tvPipTransition; } @Override @@ -105,4 +106,14 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { // when the menu alpha is 0 (e.g. when a fade-in animation starts). return true; } + + @Override + protected void cancelAnimationOnTaskVanished() { + mTvPipTransition.cancelAnimations(); + if (mLeash != null) { + mSurfaceControlTransactionFactory.getTransaction() + .setAlpha(mLeash, 0f) + .apply(); + } + } } 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 f24b2b385cad..571c839adf11 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 @@ -16,43 +16,822 @@ package com.android.wm.shell.pip.tv; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +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_PIP; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.transitTypeToString; + +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; +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 static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE; +import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP; +import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP; +import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP; +import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; + +import android.animation.AnimationHandler; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.app.TaskInfo; import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.IBinder; +import android.os.Trace; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; +import androidx.annotation.FloatRange; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; -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.splitscreen.SplitScreenController; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.TransitionUtil; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; /** * PiP Transition for TV. */ -public class TvPipTransition extends PipTransition { +public class TvPipTransition extends PipTransitionController { + private static final String TAG = "TvPipTransition"; + private static final float ZOOM_ANIMATION_SCALE_FACTOR = 0.97f; + + private final PipTransitionState mPipTransitionState; + private final PipAnimationController mPipAnimationController; + private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; + private final TvPipMenuController mTvPipMenuController; + private final PipDisplayLayoutState mPipDisplayLayoutState; + private final PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory + mTransactionFactory; + + private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = + ThreadLocal.withInitial(() -> { + AnimationHandler handler = new AnimationHandler(); + handler.setProvider(new SfVsyncFrameCallbackProvider()); + return handler; + }); + + private final long mEnterFadeOutDuration; + private final long mEnterFadeInDuration; + private final long mExitFadeOutDuration; + private final long mExitFadeInDuration; + + @Nullable + private Animator mCurrentAnimator; + + /** + * The Task window that is currently in PIP windowing mode. + */ + @Nullable + private WindowContainerToken mCurrentPipTaskToken; + + @Nullable + private IBinder mPendingExitTransition; public TvPipTransition(Context context, @NonNull ShellInit shellInit, @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, TvPipBoundsState tvPipBoundsState, - PipDisplayLayoutState pipDisplayLayoutState, - PipTransitionState pipTransitionState, TvPipMenuController tvPipMenuController, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + PipTransitionState pipTransitionState, PipAnimationController pipAnimationController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - Optional<SplitScreenController> splitScreenOptional) { - super(context, shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, - pipDisplayLayoutState, pipTransitionState, tvPipMenuController, - tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper, - splitScreenOptional); + PipDisplayLayoutState pipDisplayLayoutState) { + super(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, tvPipMenuController, + tvPipBoundsAlgorithm); + mPipTransitionState = pipTransitionState; + mPipAnimationController = pipAnimationController; + mSurfaceTransactionHelper = pipSurfaceTransactionHelper; + mTvPipMenuController = tvPipMenuController; + mPipDisplayLayoutState = pipDisplayLayoutState; + mTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + + mEnterFadeOutDuration = context.getResources().getInteger( + R.integer.config_tvPipEnterFadeOutDuration); + mEnterFadeInDuration = context.getResources().getInteger( + R.integer.config_tvPipEnterFadeInDuration); + mExitFadeOutDuration = context.getResources().getInteger( + R.integer.config_tvPipExitFadeOutDuration); + mExitFadeInDuration = context.getResources().getInteger( + R.integer.config_tvPipExitFadeInDuration); + } + + @Override + public void startExitTransition(int type, WindowContainerTransaction out, + @Nullable Rect destinationBounds) { + cancelAnimations(); + mPendingExitTransition = mTransitions.startTransition(type, out, this); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + + if (isCloseTransition(info)) { + // PiP is closing (without reentering fullscreen activity) + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Starting close animation", TAG); + cancelAnimations(); + startCloseAnimation(info, startTransaction, finishTransaction, finishCallback); + mCurrentPipTaskToken = null; + return true; + + } else if (transition.equals(mPendingExitTransition)) { + // PiP is exiting (reentering fullscreen activity) + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Starting exit animation", TAG); + + final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info); + mPendingExitTransition = null; + // PipTaskChange can be null if the PIP task has been detached, for example, when the + // task contains multiple activities, the PIP will be moved to a new PIP task when + // entering, and be moved back when exiting. In that case, the PIP task will be removed + // immediately. + final TaskInfo pipTaskInfo = currentPipTaskChange != null + ? currentPipTaskChange.getTaskInfo() + : mPipOrganizer.getTaskInfo(); + if (pipTaskInfo == null) { + throw new RuntimeException("Cannot find the pip task for exit-pip transition."); + } + + final int type = info.getType(); + switch (type) { + case TRANSIT_EXIT_PIP -> { + TransitionInfo.Change pipChange = currentPipTaskChange; + SurfaceControl activitySc = null; + if (mCurrentPipTaskToken == null) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG); + } else if (pipChange == null) { + // The pipTaskChange is null, this can happen if we are reparenting the + // PIP activity back to its original Task. In that case, we should animate + // the activity leash instead, which should be the change whose last parent + // is the recorded PiP Task. + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (mCurrentPipTaskToken.equals(change.getLastParent())) { + // Find the activity that is exiting PiP. + pipChange = change; + activitySc = change.getLeash(); + break; + } + } + } + if (pipChange == null) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: No window of exiting PIP is found. Can't play expand " + + "animation", + TAG); + removePipImmediately(info, pipTaskInfo, startTransaction, finishTransaction, + finishCallback); + return true; + } + final TransitionInfo.Root root = TransitionUtil.getRootFor(pipChange, info); + final SurfaceControl pipLeash; + if (activitySc != null) { + // Use a local leash to animate activity in case the activity has + // letterbox which may be broken by PiP animation, e.g. always end at 0,0 + // in parent and unable to include letterbox area in crop bounds. + final SurfaceControl activitySurface = pipChange.getLeash(); + pipLeash = new SurfaceControl.Builder() + .setName(activitySc + "_pip-leash") + .setContainerLayer() + .setHidden(false) + .setParent(root.getLeash()) + .build(); + startTransaction.reparent(activitySurface, pipLeash); + // Put the activity at local position with offset in case it is letterboxed. + final Point activityOffset = pipChange.getEndRelOffset(); + startTransaction.setPosition(activitySc, activityOffset.x, + activityOffset.y); + } else { + pipLeash = pipChange.getLeash(); + startTransaction.reparent(pipLeash, root.getLeash()); + } + startTransaction.setLayer(pipLeash, Integer.MAX_VALUE); + final Rect currentBounds = mPipBoundsState.getBounds(); + final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds()); + cancelAnimations(); + startExitAnimation(pipTaskInfo, pipLeash, currentBounds, destinationBounds, + startTransaction, + finishTransaction, finishCallback); + } + // pass through here is intended + case TRANSIT_TO_BACK, TRANSIT_REMOVE_PIP -> removePipImmediately(info, pipTaskInfo, + startTransaction, finishTransaction, + finishCallback + ); + default -> { + return false; + } + } + mCurrentPipTaskToken = null; + return true; + + } else if (isEnteringPip(info)) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Starting enter animation", TAG); + + // Search for an Enter PiP transition + TransitionInfo.Change enterPip = 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().getWindowingMode() == WINDOWING_MODE_PINNED) { + enterPip = change; + } + } + if (enterPip == null) { + throw new IllegalStateException("Trying to start PiP animation without a pip" + + "participant"); + } + + // Make sure other open changes are visible as entering PIP. Some may be hidden in + // Transitions#setupStartState because the transition type is OPEN (such as auto-enter). + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change == enterPip) continue; + if (TransitionUtil.isOpeningType(change.getMode())) { + final SurfaceControl leash = change.getLeash(); + startTransaction.show(leash).setAlpha(leash, 1.f); + } + } + + cancelAnimations(); + startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback); + return true; + } + + return false; + } + + /** + * For {@link Transitions#TRANSIT_REMOVE_PIP}, we just immediately remove the PIP Task. + */ + private void removePipImmediately(@NonNull TransitionInfo info, + @NonNull TaskInfo taskInfo, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: removePipImmediately", TAG); + cancelAnimations(); + startTransaction.apply(); + finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), + mPipDisplayLayoutState.getDisplayBounds()); + mTvPipMenuController.detach(); + mPipOrganizer.onExitPipFinished(taskInfo); + finishCallback.onTransitionFinished(/* wct= */ null); + + mPipTransitionState.setTransitionState(UNDEFINED); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK); + } + + private void startCloseAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + final TransitionInfo.Change pipTaskChange = findCurrentPipTaskChange(info); + final SurfaceControl pipLeash = pipTaskChange.getLeash(); + + final List<SurfaceControl> closeLeashes = new ArrayList<>(); + for (TransitionInfo.Change change : info.getChanges()) { + if (TransitionUtil.isClosingType(change.getMode()) && change != pipTaskChange) { + closeLeashes.add(change.getLeash()); + } + } + + final Rect pipBounds = mPipBoundsState.getBounds(); + mSurfaceTransactionHelper + .resetScale(startTransaction, pipLeash, pipBounds) + .crop(startTransaction, pipLeash, pipBounds) + .shadow(startTransaction, pipLeash, false); + + final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction(); + for (SurfaceControl leash : closeLeashes) { + startTransaction.setShadowRadius(leash, 0f); + } + + ValueAnimator closeFadeOutAnimator = createAnimator(); + closeFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT); + closeFadeOutAnimator.setDuration(mExitFadeOutDuration); + closeFadeOutAnimator.addUpdateListener( + animationUpdateListener(pipLeash).fadingOut().withMenu()); + for (SurfaceControl leash : closeLeashes) { + closeFadeOutAnimator.addUpdateListener(animationUpdateListener(leash).fadingOut()); + } + + closeFadeOutAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(@NonNull Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: close animation: start", TAG); + for (SurfaceControl leash : closeLeashes) { + startTransaction.setShadowRadius(leash, 0f); + } + startTransaction.apply(); + + mPipTransitionState.setTransitionState(EXITING_PIP); + sendOnPipTransitionStarted(TRANSITION_DIRECTION_REMOVE_STACK); + } + + @Override + public void onAnimationCancel(@NonNull Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: close animation: cancel", TAG); + sendOnPipTransitionCancelled(TRANSITION_DIRECTION_REMOVE_STACK); + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: close animation: end", TAG); + mTvPipMenuController.detach(); + finishCallback.onTransitionFinished(null /* wct */); + transaction.close(); + mPipTransitionState.setTransitionState(UNDEFINED); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK); + + mCurrentAnimator = null; + } + }); + + closeFadeOutAnimator.start(); + mCurrentAnimator = closeFadeOutAnimator; + } + + @Override + public void startEnterAnimation(@NonNull TransitionInfo.Change pipChange, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + // Keep track of the PIP task + mCurrentPipTaskToken = pipChange.getContainer(); + final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo(); + final SurfaceControl leash = pipChange.getLeash(); + + mTvPipMenuController.attach(leash); + setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams, + taskInfo.topActivityInfo); + + final Rect pipBounds = + mPipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas(); + mPipBoundsState.setBounds(pipBounds); + mTvPipMenuController.movePipMenu(null, pipBounds, 0f); + + final WindowContainerTransaction resizePipWct = new WindowContainerTransaction(); + resizePipWct.setWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED); + resizePipWct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED); + resizePipWct.setBounds(taskInfo.token, pipBounds); + + mSurfaceTransactionHelper + .resetScale(finishTransaction, leash, pipBounds) + .crop(finishTransaction, leash, pipBounds) + .shadow(finishTransaction, leash, false); + + final Rect currentBounds = pipChange.getStartAbsBounds(); + final Rect fadeOutCurrentBounds = scaledRect(currentBounds, ZOOM_ANIMATION_SCALE_FACTOR); + + final ValueAnimator enterFadeOutAnimator = createAnimator(); + enterFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT); + enterFadeOutAnimator.setDuration(mEnterFadeOutDuration); + enterFadeOutAnimator.addUpdateListener( + animationUpdateListener(leash) + .fadingOut() + .animateBounds(currentBounds, fadeOutCurrentBounds, currentBounds)); + + enterFadeOutAnimator.addListener(new AnimatorListenerAdapter() { + @SuppressLint("MissingPermission") + @Override + public void onAnimationEnd(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: enter fade out animation: end", TAG); + SurfaceControl.Transaction tx = mTransactionFactory.getTransaction(); + mSurfaceTransactionHelper + .resetScale(tx, leash, pipBounds) + .crop(tx, leash, pipBounds) + .shadow(tx, leash, false); + mShellTaskOrganizer.applyTransaction(resizePipWct); + tx.apply(); + } + }); + + final ValueAnimator enterFadeInAnimator = createAnimator(); + enterFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER); + enterFadeInAnimator.setDuration(mEnterFadeInDuration); + enterFadeInAnimator.addUpdateListener( + animationUpdateListener(leash) + .fadingIn() + .withMenu() + .atBounds(pipBounds)); + + final AnimatorSet animatorSet = new AnimatorSet(); + animatorSet + .play(enterFadeInAnimator) + .after(500) + .after(enterFadeOutAnimator); + + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: enter animation: start", TAG); + startTransaction.apply(); + mPipTransitionState.setTransitionState(ENTERING_PIP); + sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); + } + + @Override + public void onAnimationCancel(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: enter animation: cancel", TAG); + enterFadeInAnimator.setCurrentFraction(1f); + sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP); + } + + @Override + public void onAnimationEnd(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: enter animation: end", TAG); + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + wct.setBounds(taskInfo.token, pipBounds); + finishCallback.onTransitionFinished(wct); + + mPipTransitionState.setTransitionState(ENTERED_PIP); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + mCurrentAnimator = null; + } + }); + + animatorSet.start(); + mCurrentAnimator = animatorSet; } + private void startExitAnimation(@NonNull TaskInfo taskInfo, SurfaceControl leash, + Rect currentBounds, Rect destinationBounds, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + final Rect fadeInStartBounds = scaledRect(destinationBounds, ZOOM_ANIMATION_SCALE_FACTOR); + + final ValueAnimator exitFadeOutAnimator = createAnimator(); + exitFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT); + exitFadeOutAnimator.setDuration(mExitFadeOutDuration); + exitFadeOutAnimator.addUpdateListener( + animationUpdateListener(leash) + .fadingOut() + .withMenu() + .atBounds(currentBounds)); + exitFadeOutAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: exit fade out animation: end", TAG); + startTransaction.apply(); + mPipMenuController.detach(); + } + }); + + final ValueAnimator exitFadeInAnimator = createAnimator(); + exitFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER); + exitFadeInAnimator.setDuration(mExitFadeInDuration); + exitFadeInAnimator.addUpdateListener( + animationUpdateListener(leash) + .fadingIn() + .animateBounds(fadeInStartBounds, destinationBounds, destinationBounds)); + + final AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playSequentially( + exitFadeOutAnimator, + exitFadeInAnimator + ); + + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: exit animation: start", TAG); + mPipTransitionState.setTransitionState(EXITING_PIP); + sendOnPipTransitionStarted(TRANSITION_DIRECTION_LEAVE_PIP); + } + + @Override + public void onAnimationCancel(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: exit animation: cancel", TAG); + sendOnPipTransitionCancelled(TRANSITION_DIRECTION_LEAVE_PIP); + } + + @Override + public void onAnimationEnd(Animator animation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: exit animation: end", TAG); + mPipOrganizer.onExitPipFinished(taskInfo); + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + wct.setBounds(taskInfo.token, destinationBounds); + finishCallback.onTransitionFinished(wct); + + mPipTransitionState.setTransitionState(UNDEFINED); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_LEAVE_PIP); + + mCurrentAnimator = null; + } + }); + + animatorSet.start(); + mCurrentAnimator = animatorSet; + } + + @NonNull + private ValueAnimator createAnimator() { + final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); + return animator; + } + + @NonNull + private TvPipTransitionAnimatorUpdateListener animationUpdateListener( + @NonNull SurfaceControl leash) { + return new TvPipTransitionAnimatorUpdateListener(leash, mTvPipMenuController, + mTransactionFactory.getTransaction(), mSurfaceTransactionHelper); + } + + @NonNull + private static Rect scaledRect(@NonNull Rect rect, float scale) { + final Rect out = new Rect(rect); + out.inset((int) (rect.width() * (1 - scale) / 2), (int) (rect.height() * (1 - scale) / 2)); + return out; + } + + private boolean isCloseTransition(TransitionInfo info) { + final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info); + return currentPipTaskChange != null && info.getType() == TRANSIT_CLOSE; + } + + @Nullable + private TransitionInfo.Change findCurrentPipTaskChange(@NonNull TransitionInfo info) { + if (mCurrentPipTaskToken == null) { + return null; + } + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (mCurrentPipTaskToken.equals(change.getContainer())) { + return change; + } + } + return null; + } + + /** + * Whether we should handle the given {@link TransitionInfo} animation as entering PIP. + */ + private boolean isEnteringPip(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (isEnteringPip(change, info.getType())) return true; + } + return false; + } + + /** + * Whether a particular change is a window that is entering pip. + */ + @Override + public boolean isEnteringPip(@NonNull TransitionInfo.Change change, + @WindowManager.TransitionType int transitType) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED + && !Objects.equals(change.getContainer(), mCurrentPipTaskToken)) { + if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN + || transitType == TRANSIT_CHANGE) { + return true; + } + // Please file a bug to handle the unexpected transition type. + android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type=" + + transitTypeToString(transitType), new Throwable()); + } + return false; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG); + if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { + mCurrentAnimator.end(); + } + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + if (requestHasPipEnter(request)) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: handle PiP enter request", TAG); + WindowContainerTransaction wct = new WindowContainerTransaction(); + augmentRequest(transition, request, wct); + return wct; + } else if (request.getType() == TRANSIT_TO_BACK && request.getTriggerTask() != null + && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_PINNED) { + // if we receive a TRANSIT_TO_BACK type of request while in PiP + mPendingExitTransition = transition; + + // update the transition state to avoid {@link PipTaskOrganizer#onTaskVanished()} calls + mPipTransitionState.setTransitionState(EXITING_PIP); + + // return an empty WindowContainerTransaction so that we don't check other handlers + return new WindowContainerTransaction(); + } else { + return null; + } + } + + @Override + public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request, + @NonNull WindowContainerTransaction outWCT) { + if (!requestHasPipEnter(request)) { + throw new IllegalStateException("Called PiP augmentRequest when request has no PiP"); + } + outWCT.setActivityWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_UNDEFINED); + } + + /** + * Cancel any ongoing PiP transitions/animations. + */ + public void cancelAnimations() { + if (mPipAnimationController.isAnimating()) { + mPipAnimationController.getCurrentAnimator().cancel(); + mPipAnimationController.resetAnimatorState(); + } + if (mCurrentAnimator != null) { + mCurrentAnimator.cancel(); + } + } + + @Override + public void end() { + if (mCurrentAnimator != null) { + mCurrentAnimator.end(); + } + } + + private static class TvPipTransitionAnimatorUpdateListener implements + ValueAnimator.AnimatorUpdateListener { + private final SurfaceControl mLeash; + private final TvPipMenuController mTvPipMenuController; + private final SurfaceControl.Transaction mTransaction; + private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; + private final RectF mTmpRectF = new RectF(); + private final Rect mTmpRect = new Rect(); + + private float mStartAlpha = ALPHA_NO_CHANGE; + private float mEndAlpha = ALPHA_NO_CHANGE; + + @Nullable + private Rect mStartBounds; + @Nullable + private Rect mEndBounds; + private Rect mWindowContainerBounds; + private boolean mShowMenu; + + TvPipTransitionAnimatorUpdateListener(@NonNull SurfaceControl leash, + @NonNull TvPipMenuController tvPipMenuController, + @NonNull SurfaceControl.Transaction transaction, + @NonNull PipSurfaceTransactionHelper pipSurfaceTransactionHelper) { + mLeash = leash; + mTvPipMenuController = tvPipMenuController; + mTransaction = transaction; + mSurfaceTransactionHelper = pipSurfaceTransactionHelper; + } + + public TvPipTransitionAnimatorUpdateListener animateAlpha( + @FloatRange(from = 0.0, to = 1.0) float startAlpha, + @FloatRange(from = 0.0, to = 1.0) float endAlpha) { + mStartAlpha = startAlpha; + mEndAlpha = endAlpha; + return this; + } + + public TvPipTransitionAnimatorUpdateListener animateBounds(@NonNull Rect startBounds, + @NonNull Rect endBounds, @NonNull Rect windowContainerBounds) { + mStartBounds = startBounds; + mEndBounds = endBounds; + mWindowContainerBounds = windowContainerBounds; + return this; + } + + public TvPipTransitionAnimatorUpdateListener atBounds(@NonNull Rect bounds) { + return animateBounds(bounds, bounds, bounds); + } + + public TvPipTransitionAnimatorUpdateListener fadingOut() { + return animateAlpha(1f, 0f); + } + + public TvPipTransitionAnimatorUpdateListener fadingIn() { + return animateAlpha(0f, 1f); + } + + public TvPipTransitionAnimatorUpdateListener withMenu() { + mShowMenu = true; + return this; + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final float fraction = animation.getAnimatedFraction(); + final float alpha = lerp(mStartAlpha, mEndAlpha, fraction); + if (mStartBounds != null && mEndBounds != null) { + lerp(mStartBounds, mEndBounds, fraction, mTmpRectF); + applyAnimatedValue(alpha, mTmpRectF); + } else { + applyAnimatedValue(alpha, null); + } + } + + private void applyAnimatedValue(float alpha, @Nullable RectF bounds) { + Trace.beginSection("applyAnimatedValue"); + final SurfaceControl.Transaction tx = mTransaction; + + Trace.beginSection("leash scale and alpha"); + if (alpha != ALPHA_NO_CHANGE) { + mSurfaceTransactionHelper.alpha(tx, mLeash, alpha); + } + if (bounds != null) { + mSurfaceTransactionHelper.scale(tx, mLeash, mWindowContainerBounds, bounds); + } + mSurfaceTransactionHelper.shadow(tx, mLeash, false); + tx.show(mLeash); + Trace.endSection(); + + if (mShowMenu) { + Trace.beginSection("movePipMenu"); + if (bounds != null) { + mTmpRect.set((int) bounds.left, (int) bounds.top, (int) bounds.right, + (int) bounds.bottom); + mTvPipMenuController.movePipMenu(tx, mTmpRect, alpha); + } else { + mTvPipMenuController.movePipMenu(tx, null, alpha); + } + Trace.endSection(); + } else { + mTvPipMenuController.movePipMenu(tx, null, 0f); + } + + tx.apply(); + Trace.endSection(); + } + + private float lerp(float start, float end, float fraction) { + return start * (1 - fraction) + end * fraction; + } + + private void lerp(@NonNull Rect start, @NonNull Rect end, float fraction, + @NonNull RectF out) { + out.set( + start.left * (1 - fraction) + end.left * fraction, + start.top * (1 - fraction) + end.top * fraction, + start.right * (1 - fraction) + end.right * fraction, + start.bottom * (1 - fraction) + end.bottom * fraction); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 9bb383f0b61a..0448d94669ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -57,11 +57,6 @@ public class PipScheduler { @Nullable private SurfaceControl mPinnedTaskLeash; - // the leash of the original task of the PiP activity; - // used to synchronize app drawings in the multi-activity case - @Nullable - private SurfaceControl mOriginalTaskLeash; - /** * A temporary broadcast receiver to initiate exit PiP via expand. * This will later be modified to be triggered by the PiP menu. @@ -95,10 +90,6 @@ public class PipScheduler { mPinnedTaskLeash = pinnedTaskLeash; } - void setOriginalTaskLeash(SurfaceControl originalTaskLeash) { - mOriginalTaskLeash = originalTaskLeash; - } - void setPipTaskToken(@Nullable WindowContainerToken pipTaskToken) { mPipTaskToken = pipTaskToken; } @@ -133,6 +124,5 @@ public class PipScheduler { void onExitPip() { mPipTaskToken = null; mPinnedTaskLeash = null; - mOriginalTaskLeash = null; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 7d3bd658d126..6200ea583a48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -16,7 +16,6 @@ package com.android.wm.shell.pip2.phone; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.WindowManager.TRANSIT_OPEN; @@ -50,7 +49,7 @@ import com.android.wm.shell.transition.Transitions; public class PipTransition extends PipTransitionController { private static final String TAG = PipTransition.class.getSimpleName(); - private PipScheduler mPipScheduler; + private final PipScheduler mPipScheduler; @Nullable private WindowContainerToken mPipTaskToken; @Nullable @@ -168,14 +167,9 @@ public class PipTransition extends PipTransitionController { } mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and the relevant leashes + // cache the PiP task token and leash mPipScheduler.setPipTaskToken(mPipTaskToken); mPipScheduler.setPinnedTaskLeash(pipChange.getLeash()); - // check if we entered PiP from a multi-activity task and set the original task leash - final int lastParentTaskId = pipChange.getTaskInfo().lastParentTaskIdBeforePip; - final boolean isSingleActivity = lastParentTaskId == INVALID_TASK_ID; - mPipScheduler.setOriginalTaskLeash(isSingleActivity ? null : - findChangeByTaskId(info, lastParentTaskId).getLeash()); startTransaction.apply(); finishCallback.onTransitionFinished(null); @@ -201,17 +195,6 @@ public class PipTransition extends PipTransitionController { return null; } - @Nullable - private TransitionInfo.Change findChangeByTaskId(TransitionInfo info, int taskId) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() != null - && change.getTaskInfo().taskId == taskId) { - return change; - } - } - return null; - } - private void onExitPip() { mPipTaskToken = null; mPipScheduler.onExitPip(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 271a3b26305d..0f168c7b4ce6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -590,7 +590,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { cancel("transit_sleep"); return; } - if (mKeyguardLocked) { + if (mKeyguardLocked || (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: keyguard is locked", mInstanceId); // We will not accept new changes if we are swiping over the keyguard. @@ -627,7 +627,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { && mRecentsTask.equals(change.getContainer()); hasTaskChange = hasTaskChange || isRootTask; final boolean isLeafTask = leafTaskFilter.test(change); - if (TransitionUtil.isOpeningType(change.getMode())) { + if (TransitionUtil.isOpeningType(change.getMode()) + || TransitionUtil.isOrderOnly(change)) { if (isRecentsTask) { recentsOpening = change; } else if (isRootTask || isLeafTask) { @@ -821,8 +822,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } else if (!didMergeThings) { // Didn't recognize anything in incoming transition so don't merge it. Slog.w(TAG, "Don't know how to merge this transition, foundRecentsClosing=" - + foundRecentsClosing); - if (foundRecentsClosing) { + + foundRecentsClosing + " recentsTaskId=" + mRecentsTaskId); + if (foundRecentsClosing || mRecentsTaskId < 0) { mWillFinishToHome = false; cancel(false /* toHome */, false /* withScreenshots */, "didn't merge"); } 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 664d44910e72..56f1c784f3a7 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 @@ -25,6 +25,7 @@ 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.KEY_EXTRA_WIDGET_INTENT; 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; @@ -366,6 +367,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mStageCoordinator.getStageOfTask(taskId); } + /** + * @return {@code true} if we should create a left-right split, {@code false} if we should + * create a top-bottom split. + */ + public boolean isLeftRightSplit() { + return mStageCoordinator.isLeftRightSplit(); + } + /** Check split is foreground and task is under split or not by taskId. */ public boolean isTaskInSplitScreenForeground(int taskId) { return isTaskInSplitScreen(taskId) && isSplitScreenVisible(); @@ -711,10 +720,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, // recents that hasn't launched and is not being organized final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); + boolean setSecondIntentMultipleTask = false; if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { - fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + setSecondIntentMultipleTask = true; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else { if (mRecentTasksOptional.isPresent()) { @@ -729,6 +738,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, Toast.LENGTH_SHORT).show(); } } + if (options2 != null) { + Intent widgetIntent = options2.getParcelable(KEY_EXTRA_WIDGET_INTENT, Intent.class); + fillInIntent = resolveWidgetFillinIntent(widgetIntent, setSecondIntentMultipleTask); + } mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId, options2, splitPosition, snapPosition, remoteTransition, instanceId); } @@ -779,12 +792,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, ? ActivityOptions.fromBundle(options1) : ActivityOptions.makeBasic(); final ActivityOptions activityOptions2 = options2 != null ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic(); + boolean setSecondIntentMultipleTask = false; if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { fillInIntent1 = new Intent(); fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - fillInIntent2 = new Intent(); - fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + setSecondIntentMultipleTask = true; if (shortcutInfo1 != null) { activityOptions1.setApplyMultipleTaskFlagForShortcut(true); @@ -803,6 +816,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, Toast.LENGTH_SHORT).show(); } } + if (options2 != null) { + Intent widgetIntent = options2.getParcelable(KEY_EXTRA_WIDGET_INTENT, Intent.class); + fillInIntent2 = resolveWidgetFillinIntent(widgetIntent, setSecondIntentMultipleTask); + } mStageCoordinator.startIntents(pendingIntent1, fillInIntent1, shortcutInfo1, activityOptions1.toBundle(), pendingIntent2, fillInIntent2, shortcutInfo2, activityOptions2.toBundle(), splitPosition, snapPosition, remoteTransition, @@ -908,6 +925,34 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return false; } + /** + * Determines whether the widgetIntent needs to be modified if multiple tasks of its + * corresponding package/app are supported. There are 4 possible paths: + * <li> We select a widget for second app which is the same as the first app </li> + * <li> We select a widget for second app which is different from the first app </li> + * <li> No widgets involved, we select a second app that is the same as first app </li> + * <li> No widgets involved, we select a second app that is different from the first app + * (returns null) </li> + * + * @return an {@link Intent} with the appropriate {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} + * added on or not depending on {@param launchMultipleTasks}. + */ + @Nullable + private Intent resolveWidgetFillinIntent(@Nullable Intent widgetIntent, + boolean launchMultipleTasks) { + Intent fillInIntent2 = null; + if (launchMultipleTasks && widgetIntent != null) { + fillInIntent2 = widgetIntent; + fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } else if (widgetIntent != null) { + fillInIntent2 = widgetIntent; + } else if (launchMultipleTasks) { + fillInIntent2 = new Intent(); + fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + return fillInIntent2; + } + RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { if (ENABLE_SHELL_TRANSITIONS) return null; 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 7a4834cb5adb..be685b57f779 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 @@ -1301,7 +1301,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason); mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } void setSideStagePosition(@SplitPosition int sideStagePosition, @@ -1659,7 +1659,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } } @@ -1749,10 +1749,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (stage == STAGE_TYPE_MAIN) { mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } else { mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } if (present) { updateRecentTasksSplitPair(); @@ -2113,7 +2113,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } } } @@ -2205,8 +2205,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } - private boolean isLandscape() { - return mSplitLayout.isLandscape(); + /** + * @return {@code true} if we should create a left-right split, {@code false} if we should + * create a top-bottom split. + */ + boolean isLeftRightSplit() { + return mSplitLayout != null && mSplitLayout.isLeftRightSplit(); } /** @@ -3177,6 +3181,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); pw.println(innerPrefix + "isSplitActive=" + isSplitActive()); pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible()); + pw.println(innerPrefix + "isLeftRightSplit=" + mSplitLayout.isLeftRightSplit()); pw.println(innerPrefix + "MainStage"); pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition())); pw.println(childPrefix + "isActive=" + mMainStage.isActive()); @@ -3188,10 +3193,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.dump(pw, childPrefix); pw.println(innerPrefix + "SideStageListener"); mSideStageListener.dump(pw, childPrefix); - if (mMainStage.isActive()) { - pw.println(innerPrefix + "SplitLayout"); - mSplitLayout.dump(pw, childPrefix); - } + mSplitLayout.dump(pw, childPrefix); if (!mPausingTasks.isEmpty()) { pw.println(childPrefix + "mPausingTasks=" + mPausingTasks); } @@ -3243,7 +3245,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logExit(exitReason, SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */, SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */, - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } /** @@ -3256,7 +3258,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED, !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { 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 c2f15f6cba75..e6418f35a0b1 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 @@ -182,7 +182,7 @@ public class TaskSnapshotWindow { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "Removing taskSnapshot surface, mHasDrawn=%b", mHasDrawn); - mSession.remove(mWindow); + mSession.remove(mWindow.asBinder()); } catch (RemoteException e) { // nothing } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index ef8393c3b5b1..35a1fa0a92f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -151,7 +151,14 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, @Override public void setResizeBgColor(SurfaceControl.Transaction t, int bgColor) { - runOnViewThread(() -> setResizeBackgroundColor(t, bgColor)); + if (mHandler.getLooper().isCurrentThread()) { + // We can only use the transaction if it can updated synchronously, otherwise the tx + // will be applied immediately after but also used/updated on the view thread which + // will lead to a race and/or crash + runOnViewThread(() -> setResizeBackgroundColor(t, bgColor)); + } else { + runOnViewThread(() -> setResizeBackgroundColor(bgColor)); + } } /** 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 723a4a7ca664..193a4fb5b503 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 @@ -60,6 +60,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import android.animation.Animator; @@ -424,7 +425,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Don't animate anything that isn't independent. if (!TransitionInfo.isIndependent(change, info)) continue; - Animation a = loadAnimation(info, change, wallpaperTransit, isDreamTransition); + final int type = getTransitionTypeFromInfo(info); + Animation a = loadAnimation(type, info, change, wallpaperTransit, isDreamTransition); if (a != null) { if (isTask) { final boolean isTranslucent = (change.getFlags() & FLAG_TRANSLUCENT) != 0; @@ -660,12 +662,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @Nullable - private Animation loadAnimation(@NonNull TransitionInfo info, - @NonNull TransitionInfo.Change change, int wallpaperTransit, - boolean isDreamTransition) { + private Animation loadAnimation(@WindowManager.TransitionType int type, + @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, + int wallpaperTransit, boolean isDreamTransition) { Animation a; - final int type = info.getType(); final int flags = info.getFlags(); final int changeMode = change.getMode(); final int changeFlags = change.getFlags(); @@ -721,7 +722,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return null; } else { a = loadAttributeAnimation( - info, change, wallpaperTransit, mTransitionAnimation, isDreamTransition); + type, info, change, wallpaperTransit, mTransitionAnimation, isDreamTransition); } if (a != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index b528089d153e..473deba3b7d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -17,6 +17,7 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static com.android.wm.shell.transition.Transitions.TransitionObserver; @@ -56,14 +57,17 @@ public class HomeTransitionObserver implements TransitionObserver, @NonNull SurfaceControl.Transaction finishTransaction) { for (TransitionInfo.Change change : info.getChanges()) { final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo == null || taskInfo.taskId == -1) { + if (taskInfo == null + || taskInfo.taskId == -1 + || !taskInfo.isRunning) { continue; } final int mode = change.getMode(); + final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED); if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME - && TransitionUtil.isOpenOrCloseMode(mode)) { - notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode)); + && (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture)) { + notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode) || isBackGesture); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index d07d2b7b6db9..b012d359931a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -24,6 +24,7 @@ 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.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; +import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -45,6 +46,7 @@ import android.graphics.Rect; import android.graphics.Shader; import android.view.Surface; import android.view.SurfaceControl; +import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.Transformation; import android.window.ScreenCapture; @@ -61,10 +63,10 @@ public class TransitionAnimationHelper { /** Loads the animation that is defined through attribute id for the given transition. */ @Nullable - public static Animation loadAttributeAnimation(@NonNull TransitionInfo info, + public static Animation loadAttributeAnimation(@WindowManager.TransitionType int type, + @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, int wallpaperTransit, @NonNull TransitionAnimation transitionAnimation, boolean isDreamTransition) { - final int type = info.getType(); final int changeMode = change.getMode(); final int changeFlags = change.getFlags(); final boolean enter = TransitionUtil.isOpeningType(changeMode); @@ -186,6 +188,38 @@ public class TransitionAnimationHelper { return options.getCustomActivityTransition(isOpen); } + /** + * Gets the final transition type from {@link TransitionInfo} for determining the animation. + */ + public static int getTransitionTypeFromInfo(@NonNull TransitionInfo info) { + final int type = info.getType(); + // If the info transition type is opening transition, iterate its changes to see if it + // has any opening change, if none, returns TRANSIT_CLOSE type for closing animation. + if (type == TRANSIT_OPEN) { + boolean hasOpenTransit = false; + for (TransitionInfo.Change change : info.getChanges()) { + if ((change.getTaskInfo() != null || change.hasFlags(FLAG_IS_DISPLAY)) + && !TransitionUtil.isOrderOnly(change)) { + // This isn't an activity-level transition. + return type; + } + if (change.getTaskInfo() != null + && change.hasFlags(FLAG_IS_DISPLAY | FLAGS_IS_NON_APP_WINDOW)) { + // Ignore non-activity containers. + continue; + } + if (change.getMode() == TRANSIT_OPEN) { + hasOpenTransit = true; + break; + } + } + if (!hasOpenTransit) { + return TRANSIT_CLOSE; + } + } + return type; + } + static Animation loadCustomActivityTransition( @NonNull TransitionInfo.AnimationOptions.CustomActivityTransition transitionAnim, TransitionInfo.AnimationOptions options, boolean enter, 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 41ec33c9c762..b98762d5e104 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 @@ -654,12 +654,27 @@ public class Transitions implements RemoteCallable<Transitions>, info.setUnreleasedWarningCallSiteForAllSurfaces("Transitions.onTransitionReady"); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady (#%d) %s: %s", info.getDebugId(), transitionToken, info); - final int activeIdx = findByToken(mPendingTransitions, transitionToken); + int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { - throw new IllegalStateException("Got transitionReady for non-pending transition " + final ActiveTransition existing = getKnownTransition(transitionToken); + if (existing != null) { + Log.e(TAG, "Got duplicate transitionReady for " + transitionToken); + // The transition is already somewhere else in the pipeline, so just return here. + t.apply(); + existing.mFinishT.merge(finishT); + return; + } + // This usually means the system is in a bad state and may not recover; however, + // there's an incentive to propagate bad states rather than crash, so we're kinda + // required to do the same thing I guess. + Log.wtf(TAG, "Got transitionReady for non-pending transition " + transitionToken + ". expecting one of " + Arrays.toString(mPendingTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); + final ActiveTransition fallback = new ActiveTransition(); + fallback.mToken = transitionToken; + mPendingTransitions.add(fallback); + activeIdx = mPendingTransitions.size() - 1; } // Move from pending to ready final ActiveTransition active = mPendingTransitions.remove(activeIdx); @@ -1050,34 +1065,43 @@ public class Transitions implements RemoteCallable<Transitions>, processReadyQueue(track); } - private boolean isTransitionKnown(IBinder token) { + /** + * Checks to see if the transition specified by `token` is already known. If so, it will be + * returned. + */ + @Nullable + private ActiveTransition getKnownTransition(IBinder token) { for (int i = 0; i < mPendingTransitions.size(); ++i) { - if (mPendingTransitions.get(i).mToken == token) return true; + final ActiveTransition active = mPendingTransitions.get(i); + if (active.mToken == token) return active; } for (int i = 0; i < mReadyDuringSync.size(); ++i) { - if (mReadyDuringSync.get(i).mToken == token) return true; + final ActiveTransition active = mReadyDuringSync.get(i); + if (active.mToken == token) return active; } for (int t = 0; t < mTracks.size(); ++t) { final Track tr = mTracks.get(t); for (int i = 0; i < tr.mReadyTransitions.size(); ++i) { - if (tr.mReadyTransitions.get(i).mToken == token) return true; + final ActiveTransition active = tr.mReadyTransitions.get(i); + if (active.mToken == token) return active; } final ActiveTransition active = tr.mActiveTransition; if (active == null) continue; - if (active.mToken == token) return true; + if (active.mToken == token) return active; if (active.mMerged == null) continue; for (int m = 0; m < active.mMerged.size(); ++m) { - if (active.mMerged.get(m).mToken == token) return true; + final ActiveTransition merged = active.mMerged.get(m); + if (merged.mToken == token) return merged; } } - return false; + return null; } void requestStartTransition(@NonNull IBinder transitionToken, @Nullable TransitionRequestInfo request) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested (#%d): %s %s", request.getDebugId(), transitionToken, request); - if (isTransitionKnown(transitionToken)) { + if (getKnownTransition(transitionToken) != null) { throw new RuntimeException("Transition already started " + transitionToken); } final ActiveTransition active = new ActiveTransition(); @@ -1161,7 +1185,7 @@ public class Transitions implements RemoteCallable<Transitions>, */ private void finishForSync(ActiveTransition reason, int trackIdx, @Nullable ActiveTransition forceFinish) { - if (!isTransitionKnown(reason.mToken)) { + if (getKnownTransition(reason.mToken) == null) { Log.d(TAG, "finishForSleep: already played sync transition " + reason); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index aff35a347183..c12ac8b3772e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -149,7 +149,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, - mSurfaceControlTransactionSupplier); + mSurfaceControlTransactionSupplier, + mDisplayController); } final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 3add6f4175bc..ab29df1f780c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -22,6 +22,7 @@ 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.WindowInsets.Type.statusBars; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; 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; @@ -221,7 +222,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mRecentsTransitionHandler.addTransitionStateListener(new RecentsTransitionStateListener() { @Override public void onTransitionStarted(IBinder transition) { - onRecentsTransitionStarted(transition); + blockRelayoutOnTransitionStarted(transition); } }); mShellCommandHandler.addDumpCallback(this::dump, this); @@ -281,6 +282,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (decor != null) { decor.addTransitionPausingRelayout(transition); } + } else if (change.getMode() == WindowManager.TRANSIT_TO_FRONT + && ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) + && change.getTaskInfo() != null) { + blockRelayoutOnTransitionStarted(transition); } } @@ -358,7 +363,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } - private void onRecentsTransitionStarted(IBinder transition) { + private void blockRelayoutOnTransitionStarted(IBinder transition) { // Block relayout on window decorations originating from #onTaskInfoChanges until the // animation completes to avoid interfering with the transition animation. for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { @@ -428,7 +433,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (isTaskInSplitScreen(mTaskId)) { mSplitScreenController.moveTaskToFullscreen(mTaskId); } else { - mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId)); + mDesktopTasksController.ifPresent(c -> + c.moveToFullscreen(mTaskId, mWindowDecorByTaskId.get(mTaskId))); } } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); @@ -536,6 +542,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (mGestureDetector.onTouchEvent(e)) { return true; } + if (e.getActionMasked() == MotionEvent.ACTION_CANCEL) { + // If a motion event is cancelled, reset mShouldClick so a click is not accidentally + // performed. + mShouldClick = false; + } switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mDragPointerId = e.getPointerId(0); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index eba1a36ef29f..6ec91e0e28dd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -293,7 +293,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, - mSurfaceControlTransactionSupplier); + mSurfaceControlTransactionSupplier, + mDisplayController); } final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) @@ -547,8 +548,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private PointF offsetCaptionLocation(MotionEvent ev) { final PointF result = new PointF(ev.getX(), ev.getY()); - final Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId) - .positionInParent; + final ActivityManager.RunningTaskInfo taskInfo = + mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId); + if (taskInfo == null) return result; + final Point positionInParent = taskInfo.positionInParent; result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY); result.offset(-positionInParent.x, -positionInParent.y); return result; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java index 1669cf4a222c..8ce2d6d6d092 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java @@ -40,8 +40,9 @@ public interface DragPositioningCallback { * {@code 0} to indicate it's a move * @param x x coordinate in window decoration coordinate system where the drag starts * @param y y coordinate in window decoration coordinate system where the drag starts + * @return the starting task bounds */ - void onDragPositioningStart(@CtrlType int ctrlType, float x, float y); + Rect onDragPositioningStart(@CtrlType int ctrlType, float x, float y); /** * Called when the pointer moves during a drag-resize or drag-move. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 518f4b87e197..8511a21d4294 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -24,6 +24,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERL import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER; +import static com.android.input.flags.Flags.enablePointerChoreographer; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; @@ -49,7 +50,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManagerGlobal; -import com.android.internal.view.BaseIWindow; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; import java.util.function.Supplier; @@ -62,13 +64,16 @@ import java.util.function.Supplier; class DragResizeInputListener implements AutoCloseable { private static final String TAG = "DragResizeInputListener"; private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession(); + private final Context mContext; private final Handler mHandler; private final Choreographer mChoreographer; private final InputManager mInputManager; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; private final int mDisplayId; - private final BaseIWindow mFakeWindow; + + private final IBinder mClientToken; + private final IBinder mFocusGrantToken; private final SurfaceControl mDecorationSurface; private final InputChannel mInputChannel; @@ -76,8 +81,9 @@ class DragResizeInputListener implements AutoCloseable { private final DragPositioningCallback mCallback; private final SurfaceControl mInputSinkSurface; - private final BaseIWindow mFakeSinkWindow; + private final IBinder mSinkClientToken; private final InputChannel mSinkInputChannel; + private final DisplayController mDisplayController; private int mTaskWidth; private int mTaskHeight; @@ -92,6 +98,7 @@ class DragResizeInputListener implements AutoCloseable { private int mDragPointerId = -1; private DragDetector mDragDetector; + private final Region mTouchRegion = new Region(); DragResizeInputListener( Context context, @@ -102,25 +109,25 @@ class DragResizeInputListener implements AutoCloseable { SurfaceControl decorationSurface, DragPositioningCallback callback, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, - Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) { + Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, + DisplayController displayController) { mInputManager = context.getSystemService(InputManager.class); + mContext = context; mHandler = handler; mChoreographer = choreographer; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mDisplayId = displayId; mTaskCornerRadius = taskCornerRadius; mDecorationSurface = decorationSurface; - // Use a fake window as the backing surface is a container layer, and we don't want to - // create a buffer layer for it, so we can't use ViewRootImpl. - mFakeWindow = new BaseIWindow(); - mFakeWindow.setSession(mWindowSession); + mDisplayController = displayController; + mClientToken = new Binder(); mFocusGrantToken = new Binder(); mInputChannel = new InputChannel(); try { mWindowSession.grantInputChannel( mDisplayId, mDecorationSurface, - mFakeWindow.asBinder(), + mClientToken, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY, @@ -149,13 +156,13 @@ class DragResizeInputListener implements AutoCloseable { .setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER) .show(mInputSinkSurface) .apply(); - mFakeSinkWindow = new BaseIWindow(); + mSinkClientToken = new Binder(); mSinkInputChannel = new InputChannel(); try { mWindowSession.grantInputChannel( mDisplayId, mInputSinkSurface, - mFakeSinkWindow.asBinder(), + mSinkClientToken, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */, @@ -195,34 +202,34 @@ class DragResizeInputListener implements AutoCloseable { mCornerSize = cornerSize; mDragDetector.setTouchSlop(touchSlop); - Region touchRegion = new Region(); + mTouchRegion.setEmpty(); final Rect topInputBounds = new Rect( -mResizeHandleThickness, -mResizeHandleThickness, mTaskWidth + mResizeHandleThickness, 0); - touchRegion.union(topInputBounds); + mTouchRegion.union(topInputBounds); final Rect leftInputBounds = new Rect( -mResizeHandleThickness, 0, 0, mTaskHeight); - touchRegion.union(leftInputBounds); + mTouchRegion.union(leftInputBounds); final Rect rightInputBounds = new Rect( mTaskWidth, 0, mTaskWidth + mResizeHandleThickness, mTaskHeight); - touchRegion.union(rightInputBounds); + mTouchRegion.union(rightInputBounds); final Rect bottomInputBounds = new Rect( -mResizeHandleThickness, mTaskHeight, mTaskWidth + mResizeHandleThickness, mTaskHeight + mResizeHandleThickness); - touchRegion.union(bottomInputBounds); + mTouchRegion.union(bottomInputBounds); // Set up touch areas in each corner. int cornerRadius = mCornerSize / 2; @@ -231,28 +238,28 @@ class DragResizeInputListener implements AutoCloseable { -cornerRadius, cornerRadius, cornerRadius); - touchRegion.union(mLeftTopCornerBounds); + mTouchRegion.union(mLeftTopCornerBounds); mRightTopCornerBounds = new Rect( mTaskWidth - cornerRadius, -cornerRadius, mTaskWidth + cornerRadius, cornerRadius); - touchRegion.union(mRightTopCornerBounds); + mTouchRegion.union(mRightTopCornerBounds); mLeftBottomCornerBounds = new Rect( -cornerRadius, mTaskHeight - cornerRadius, cornerRadius, mTaskHeight + cornerRadius); - touchRegion.union(mLeftBottomCornerBounds); + mTouchRegion.union(mLeftBottomCornerBounds); mRightBottomCornerBounds = new Rect( mTaskWidth - cornerRadius, mTaskHeight - cornerRadius, mTaskWidth + cornerRadius, mTaskHeight + cornerRadius); - touchRegion.union(mRightBottomCornerBounds); + mTouchRegion.union(mRightBottomCornerBounds); try { mWindowSession.updateInputChannel( @@ -262,7 +269,7 @@ class DragResizeInputListener implements AutoCloseable { FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY, INPUT_FEATURE_SPY, - touchRegion); + mTouchRegion); } catch (RemoteException e) { e.rethrowFromSystemServer(); } @@ -281,19 +288,8 @@ class DragResizeInputListener implements AutoCloseable { // issue. However, were there touchscreen-only a region out of the task bounds, mouse // gestures will become no-op in that region, even though the mouse gestures may appear to // be performed on the input window behind the resize handle. - touchRegion.op(0, 0, mTaskWidth, mTaskHeight, Region.Op.DIFFERENCE); - try { - mWindowSession.updateInputChannel( - mSinkInputChannel.getToken(), - mDisplayId, - mInputSinkSurface, - FLAG_NOT_FOCUSABLE, - 0 /* privateFlags */, - INPUT_FEATURE_NO_INPUT_CHANNEL, - touchRegion); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } + mTouchRegion.op(0, 0, mTaskWidth, mTaskHeight, Region.Op.DIFFERENCE); + updateSinkInputChannel(mTouchRegion); return true; } @@ -309,19 +305,34 @@ class DragResizeInputListener implements AutoCloseable { return region; } + private void updateSinkInputChannel(Region region) { + try { + mWindowSession.updateInputChannel( + mSinkInputChannel.getToken(), + mDisplayId, + mInputSinkSurface, + FLAG_NOT_FOCUSABLE, + 0 /* privateFlags */, + INPUT_FEATURE_NO_INPUT_CHANNEL, + region); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + @Override public void close() { mInputEventReceiver.dispose(); mInputChannel.dispose(); try { - mWindowSession.remove(mFakeWindow); + mWindowSession.remove(mClientToken); } catch (RemoteException e) { e.rethrowFromSystemServer(); } mSinkInputChannel.dispose(); try { - mWindowSession.remove(mFakeSinkWindow); + mWindowSession.remove(mSinkClientToken); } catch (RemoteException e) { e.rethrowFromSystemServer(); } @@ -337,6 +348,7 @@ class DragResizeInputListener implements AutoCloseable { private boolean mConsumeBatchEventScheduled; private boolean mShouldHandleEvents; private int mLastCursorType = PointerIcon.TYPE_DEFAULT; + private Rect mDragStartTaskBounds; private TaskResizeInputEventReceiver( InputChannel inputChannel, Handler handler, Choreographer choreographer) { @@ -398,12 +410,15 @@ class DragResizeInputListener implements AutoCloseable { } if (mShouldHandleEvents) { mInputManager.pilferPointers(mInputChannel.getToken()); - mDragPointerId = e.getPointerId(0); float rawX = e.getRawX(0); float rawY = e.getRawY(0); int ctrlType = calculateCtrlType(isTouch, x, y); - mCallback.onDragPositioningStart(ctrlType, rawX, rawY); + mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, + rawX, rawY); + // Increase the input sink region to cover the whole screen; this is to + // prevent input and focus from going to other tasks during a drag resize. + updateInputSinkRegionForDrag(mDragStartTaskBounds); result = true; } break; @@ -415,7 +430,8 @@ class DragResizeInputListener implements AutoCloseable { int dragPointerIndex = e.findPointerIndex(mDragPointerId); float rawX = e.getRawX(dragPointerIndex); float rawY = e.getRawY(dragPointerIndex); - mCallback.onDragPositioningMove(rawX, rawY); + final Rect taskBounds = mCallback.onDragPositioningMove(rawX, rawY); + updateInputSinkRegionForDrag(taskBounds); result = true; break; } @@ -423,8 +439,13 @@ class DragResizeInputListener implements AutoCloseable { case MotionEvent.ACTION_CANCEL: { if (mShouldHandleEvents) { int dragPointerIndex = e.findPointerIndex(mDragPointerId); - mCallback.onDragPositioningEnd( + final Rect taskBounds = mCallback.onDragPositioningEnd( e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); + // If taskBounds has changed, setGeometry will be called and update the + // sink region. Otherwise, we should revert it here. + if (taskBounds.equals(mDragStartTaskBounds)) { + updateSinkInputChannel(mTouchRegion); + } } mShouldHandleEvents = false; mDragPointerId = -1; @@ -433,7 +454,9 @@ class DragResizeInputListener implements AutoCloseable { } case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: { - updateCursorType(e.getXCursorPosition(), e.getYCursorPosition()); + updateCursorType(e.getDisplayId(), e.getDeviceId(), + e.getPointerId(/*pointerIndex=*/0), e.getXCursorPosition(), + e.getYCursorPosition()); result = true; break; } @@ -444,6 +467,18 @@ class DragResizeInputListener implements AutoCloseable { return result; } + private void updateInputSinkRegionForDrag(Rect taskBounds) { + final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); + final Region dragTouchRegion = new Region(-taskBounds.left, + -taskBounds.top, + -taskBounds.left + layout.width(), + -taskBounds.top + layout.height()); + // Remove the localized task bounds from the touch region. + taskBounds.offsetTo(0, 0); + dragTouchRegion.op(taskBounds, Region.Op.DIFFERENCE); + updateSinkInputChannel(dragTouchRegion); + } + private boolean isInCornerBounds(float xf, float yf) { return calculateCornersCtrlType(xf, yf) != 0; } @@ -549,7 +584,8 @@ class DragResizeInputListener implements AutoCloseable { return 0; } - private void updateCursorType(float x, float y) { + private void updateCursorType(int displayId, int deviceId, int pointerId, float x, + float y) { @DragPositioningCallback.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y); int cursorType = PointerIcon.TYPE_DEFAULT; @@ -581,9 +617,14 @@ class DragResizeInputListener implements AutoCloseable { // where views in the task can receive input events because we can't set touch regions // of input sinks to have rounded corners. if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) { - mInputManager.setPointerIconType(cursorType); + if (enablePointerChoreographer()) { + mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType), + displayId, deviceId, pointerId, mInputChannel.getToken()); + } else { + mInputManager.setPointerIconType(cursorType); + } mLastCursorType = cursorType; } } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index dadd264596fb..3a1ea0e201b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -68,7 +68,7 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { } @Override - public void onDragPositioningStart(int ctrlType, float x, float y) { + public Rect onDragPositioningStart(int ctrlType, float x, float y) { mCtrlType = ctrlType; mTaskBoundsAtDragStart.set( mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); @@ -87,6 +87,7 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId()) .getStableBounds(mStableBounds); } + return new Rect(mRepositionTaskBounds); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 852c037baad5..4b55a0caaca5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -85,7 +85,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, } @Override - public void onDragPositioningStart(int ctrlType, float x, float y) { + public Rect onDragPositioningStart(int ctrlType, float x, float y) { mCtrlType = ctrlType; mTaskBoundsAtDragStart.set( mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); @@ -107,6 +107,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mDisplayController.getDisplayLayout(mDesktopWindowDecoration.mDisplay.getDisplayId()) .getStableBounds(mStableBounds); } + return new Rect(mRepositionTaskBounds); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt index b739ad3793e4..589a8134c2d3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt @@ -1,14 +1,23 @@ package com.android.wm.shell.windowdecor.viewholder +import android.annotation.ColorInt import android.app.ActivityManager.RunningTaskInfo import android.content.res.ColorStateList +import android.content.res.Configuration import android.graphics.Bitmap -import android.graphics.drawable.GradientDrawable +import android.graphics.Color import android.view.View import android.view.View.OnLongClickListener import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView +import androidx.core.content.withStyledAttributes +import com.android.internal.R.attr.materialColorOnSecondaryContainer +import com.android.internal.R.attr.materialColorOnSurface +import com.android.internal.R.attr.materialColorSecondaryContainer +import com.android.internal.R.attr.materialColorSurfaceContainerHigh +import com.android.internal.R.attr.materialColorSurfaceContainerLow +import com.android.internal.R.attr.materialColorSurfaceDim import com.android.wm.shell.R /** @@ -49,57 +58,82 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( } override fun bindData(taskInfo: RunningTaskInfo) { - val captionDrawable = captionView.background as GradientDrawable - taskInfo.taskDescription?.statusBarColor?.let { - captionDrawable.setColor(it) - } - - closeWindowButton.imageTintList = ColorStateList.valueOf( - getCaptionCloseButtonColor(taskInfo)) - maximizeWindowButton.imageTintList = ColorStateList.valueOf( - getCaptionMaximizeButtonColor(taskInfo)) - expandMenuButton.imageTintList = ColorStateList.valueOf( - getCaptionExpandButtonColor(taskInfo)) - appNameTextView.setTextColor(getCaptionAppNameTextColor(taskInfo)) + captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo)) + val color = getAppNameAndButtonColor(taskInfo) + val alpha = Color.alpha(color) + closeWindowButton.imageTintList = ColorStateList.valueOf(color) + maximizeWindowButton.imageTintList = ColorStateList.valueOf(color) + expandMenuButton.imageTintList = ColorStateList.valueOf(color) + appNameTextView.setTextColor(color) + appIconImageView.imageAlpha = alpha + maximizeWindowButton.imageAlpha = alpha + closeWindowButton.imageAlpha = alpha + expandMenuButton.imageAlpha = alpha } override fun onHandleMenuOpened() {} override fun onHandleMenuClosed() {} - private fun getCaptionAppNameTextColor(taskInfo: RunningTaskInfo): Int { - return if (shouldUseLightCaptionColors(taskInfo)) { - context.getColor(R.color.desktop_mode_caption_app_name_light) - } else { - context.getColor(R.color.desktop_mode_caption_app_name_dark) + @ColorInt + private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int { + val materialColorAttr: Int = + if (isDarkMode()) { + if (!taskInfo.isFocused) { + materialColorSurfaceContainerHigh + } else { + materialColorSurfaceDim + } + } else { + if (!taskInfo.isFocused) { + materialColorSurfaceContainerLow + } else { + materialColorSecondaryContainer + } } - } - - private fun getCaptionCloseButtonColor(taskInfo: RunningTaskInfo): Int { - return if (shouldUseLightCaptionColors(taskInfo)) { - context.getColor(R.color.desktop_mode_caption_close_button_light) - } else { - context.getColor(R.color.desktop_mode_caption_close_button_dark) + context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) { + return getColor(0, 0) } + return 0 } - private fun getCaptionMaximizeButtonColor(taskInfo: RunningTaskInfo): Int { - return if (shouldUseLightCaptionColors(taskInfo)) { - context.getColor(R.color.desktop_mode_caption_maximize_button_light) - } else { - context.getColor(R.color.desktop_mode_caption_maximize_button_dark) + @ColorInt + private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int { + val materialColorAttr = when { + isDarkMode() -> materialColorOnSurface + else -> materialColorOnSecondaryContainer } + val appDetailsOpacity = when { + isDarkMode() && !taskInfo.isFocused -> DARK_THEME_UNFOCUSED_OPACITY + !isDarkMode() && !taskInfo.isFocused -> LIGHT_THEME_UNFOCUSED_OPACITY + else -> FOCUSED_OPACITY + } + context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) { + val color = getColor(0, 0) + return if (appDetailsOpacity == FOCUSED_OPACITY) { + color + } else { + Color.argb( + appDetailsOpacity, + Color.red(color), + Color.green(color), + Color.blue(color) + ) + } + } + return 0 } - private fun getCaptionExpandButtonColor(taskInfo: RunningTaskInfo): Int { - return if (shouldUseLightCaptionColors(taskInfo)) { - context.getColor(R.color.desktop_mode_caption_expand_button_light) - } else { - context.getColor(R.color.desktop_mode_caption_expand_button_dark) - } + private fun isDarkMode(): Boolean { + return context.resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES } companion object { private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" + private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% + private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% + private const val FOCUSED_OPACITY = 255 } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt index b1fb0f184bff..4930cb721336 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt @@ -2,9 +2,11 @@ package com.android.wm.shell.windowdecor.viewholder import android.animation.ObjectAnimator import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.res.ColorStateList -import android.graphics.drawable.GradientDrawable +import android.graphics.Color import android.view.View +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.widget.ImageButton import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators @@ -34,10 +36,8 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( override fun bindData(taskInfo: RunningTaskInfo) { taskInfo.taskDescription?.statusBarColor?.let { captionColor -> - val captionDrawable = captionView.background as GradientDrawable - captionDrawable.setColor(captionColor) + captionView.setBackgroundColor(captionColor) } - captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) } @@ -57,6 +57,22 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( } } + /** + * Whether the caption items should use the 'light' color variant so that there's good contrast + * with the caption background color. + */ + private fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean { + return taskInfo.taskDescription + ?.let { taskDescription -> + if (Color.alpha(taskDescription.statusBarColor) != 0 && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { + Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5 + } else { + taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 + } + } ?: false + } + /** Animate appearance/disappearance of caption handle as the handle menu is animated. */ private fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) { val animator = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt index 8b405f02ef29..690b4e4be122 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt @@ -1,11 +1,8 @@ package com.android.wm.shell.windowdecor.viewholder import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context -import android.graphics.Color import android.view.View -import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS /** * Encapsulates the root [View] of a window decoration and its children to facilitate looking up @@ -20,22 +17,6 @@ internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) { */ abstract fun bindData(taskInfo: RunningTaskInfo) - /** - * Whether the caption items should use the 'light' color variant so that there's good contrast - * with the caption background color. - */ - protected fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean { - return taskInfo.taskDescription - ?.let { taskDescription -> - if (Color.alpha(taskDescription.statusBarColor) != 0 && - taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { - Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5 - } else { - taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0 - } - } ?: false - } - /** Callback when the handle menu is opened. */ abstract fun onHandleMenuOpened() diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 366f7b1e678f..4abaf5bd4a38 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -52,7 +52,7 @@ java_library { } java_defaults { - name: "WMShellFlickerTestsDefaultWithoutTemplate", + name: "WMShellFlickerTestsDefault", platform_apis: true, certificate: "platform", optimize: { @@ -75,16 +75,9 @@ java_defaults { ], data: [ ":FlickerTestApp", - "trace_config/*", ], } -java_defaults { - name: "WMShellFlickerTestsDefault", - defaults: ["WMShellFlickerTestsDefaultWithoutTemplate"], - test_config_template: "AndroidTestTemplate.xml", -} - java_library { name: "WMShellFlickerTestsBase", defaults: ["WMShellFlickerTestsDefault"], diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml deleted file mode 100644 index b00d88e61e13..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml +++ /dev/null @@ -1,112 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 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. - --> -<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}"> - <option name="test-tag" value="FlickerTests"/> - <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> - <option name="isolated-storage" value="false"/> - - <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> - <!-- keeps the screen on during tests --> - <option name="screen-always-on" value="on"/> - <!-- prevents the phone from restarting --> - <option name="force-skip-system-props" value="true"/> - <!-- set WM tracing verbose level to all --> - <option name="run-command" value="cmd window tracing level all"/> - <!-- set WM tracing to frame (avoid incomplete states) --> - <option name="run-command" value="cmd window tracing frame"/> - <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> - <option name="run-command" value="pm disable com.google.android.internal.betterbug"/> - <!-- ensure lock screen mode is swipe --> - <option name="run-command" value="locksettings set-disabled false"/> - <!-- restart launcher to activate TAPL --> - <option name="run-command" - value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/> - <!-- Increase trace size: 20mb for WM and 80mb for SF --> - <option name="run-command" value="cmd window tracing size 20480"/> - <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> - <!-- b/307664397 - Ensure camera has the correct permissions and doesn't show a dialog --> - <option name="run-command" - value="pm grant com.google.android.GoogleCamera android.permission.ACCESS_FINE_LOCATION"/> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="test-user-token" value="%TEST_USER%"/> - <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> - <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> - <option name="run-command" value="settings put system show_touches 1"/> - <option name="run-command" value="settings put system pointer_location 1"/> - <option name="teardown-command" - value="settings delete secure show_ime_with_hard_keyboard"/> - <option name="teardown-command" value="settings delete system show_touches"/> - <option name="teardown-command" value="settings delete system pointer_location"/> - <option name="teardown-command" - value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> - <option name="cleanup-apks" value="true"/> - <option name="test-file-name" value="{MODULE}.apk"/> - <option name="test-file-name" value="FlickerTestApp.apk"/> - </target_preparer> - <!-- Enable mocking GPS location by the test app --> - <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="run-command" - value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location allow"/> - <option name="teardown-command" - value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location deny"/> - </target_preparer> - - <!-- Needed for pushing the trace config file --> - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> - <option name="push-file" - key="trace_config.textproto" - value="/data/misc/perfetto-traces/trace_config.textproto" - /> - <!--Install the content provider automatically when we push some file in sdcard folder.--> - <!--Needed to avoid the installation during the test suite.--> - <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/> - </target_preparer> - <test class="com.android.tradefed.testtype.AndroidJUnitTest"> - <option name="package" value="{PACKAGE}"/> - <option name="shell-timeout" value="6600s"/> - <option name="test-timeout" value="6000s"/> - <option name="hidden-api-checks" value="false"/> - <option name="device-listeners" value="android.device.collectors.PerfettoListener"/> - <!-- PerfettoListener related arguments --> - <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/> - <option name="instrumentation-arg" - key="perfetto_config_file" - value="trace_config.textproto" - /> - <option name="instrumentation-arg" key="per_run" value="true"/> - </test> - <!-- Needed for pulling the collected trace config on to the host --> - <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> - <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> - <option name="collect-on-run-ended-only" value="true"/> - <option name="clean-up" value="true"/> - </metrics_collector> -</configuration> diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp index bae701f2cbeb..e151ab2c5878 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp @@ -36,6 +36,8 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker", instrumentation_target_package: "com.android.wm.shell.flicker", + test_config_template: "AndroidTestTemplate.xml", srcs: [":WMShellFlickerTestsAppCompat-src"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt index 744e8c2eb06f..181474fa0590 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt @@ -57,25 +57,18 @@ class LetterboxRule( resetLetterboxStyle() _letterboxStyle = mapLetterboxStyle() val isLetterboxEducationEnabled = _letterboxStyle.getValue("Is education enabled") - var hasLetterboxEducationStateChanged = false if ("$withLetterboxEducationEnabled" != isLetterboxEducationEnabled) { - hasLetterboxEducationStateChanged = true execAdb("wm set-letterbox-style --isEducationEnabled " + withLetterboxEducationEnabled) } - return try { - object : Statement() { - @Throws(Throwable::class) - override fun evaluate() { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + try { base!!.evaluate() + } finally { + resetLetterboxStyle() } } - } finally { - if (hasLetterboxEducationStateChanged) { - execAdb( - "wm set-letterbox-style --isEducationEnabled " + isLetterboxEducationEnabled - ) - } - resetLetterboxStyle() } } diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto index 406ada97a07d..5a017ad21044 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto @@ -63,11 +63,7 @@ data_sources: { atrace_categories: "sched_process_exit" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker" atrace_apps: "com.android.wm.shell.flicker.other" - atrace_apps: "com.android.wm.shell.flicker.bubbles" - atrace_apps: "com.android.wm.shell.flicker.pip" - atrace_apps: "com.android.wm.shell.flicker.splitscreen" atrace_apps: "com.google.android.apps.nexuslauncher" } } diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp b/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp index c4e9a8479563..f0b4f1faad46 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp @@ -29,6 +29,8 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.bubbles", instrumentation_target_package: "com.android.wm.shell.flicker.bubbles", + test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*.kt"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto index 406ada97a07d..15998311e43a 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto +++ b/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto @@ -63,11 +63,7 @@ data_sources: { atrace_categories: "sched_process_exit" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker" - atrace_apps: "com.android.wm.shell.flicker.other" atrace_apps: "com.android.wm.shell.flicker.bubbles" - atrace_apps: "com.android.wm.shell.flicker.pip" - atrace_apps: "com.android.wm.shell.flicker.splitscreen" atrace_apps: "com.google.android.apps.nexuslauncher" } } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp index 386983ce6aae..e61f7629f4fd 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp @@ -62,11 +62,13 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip", instrumentation_target_package: "com.android.wm.shell.flicker.pip", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPip1-src", ":WMShellFlickerTestsPipCommon-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -75,11 +77,13 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip", instrumentation_target_package: "com.android.wm.shell.flicker.pip", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPip2-src", ":WMShellFlickerTestsPipCommon-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -88,6 +92,7 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip", instrumentation_target_package: "com.android.wm.shell.flicker.pip", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPip3-src", ":WMShellFlickerTestsPipCommon-src", @@ -98,6 +103,7 @@ android_test { ":WMShellFlickerTestsPipApps-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -106,19 +112,22 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip.apps", instrumentation_target_package: "com.android.wm.shell.flicker.pip.apps", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPipApps-src", ":WMShellFlickerTestsPipCommon-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { name: "WMShellFlickerTestsPipAppsCSuite", - defaults: ["WMShellFlickerTestsDefaultWithoutTemplate"], + defaults: ["WMShellFlickerTestsDefault"], additional_manifests: ["AndroidManifest.xml"], package_name: "com.android.wm.shell.flicker.pip.apps", instrumentation_target_package: "com.android.wm.shell.flicker.pip.apps", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPipApps-src", ":WMShellFlickerTestsPipCommon-src", @@ -128,9 +137,11 @@ android_test { "device-tests", "csuite", ], + data: ["trace_config/*"], } csuite_test { name: "csuite-1p3p-pip-flickers", + test_plan_include: "csuitePlan.xml", test_config_template: "csuiteDefaultTemplate.xml", } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index 89ecc29d977b..f5a8655b81f0 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -45,12 +45,15 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put global package_verifier_user_consent -1"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> + <option name="teardown-command" + value="settings put global package_verifier_user_consent 1"/> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true"/> @@ -76,6 +79,17 @@ value="appops set com.android.shell android:mock_location deny"/> </target_preparer> + <!-- Use app crawler to log into Netflix --> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" + value="am start -n com.netflix.mediaclient/com.netflix.mediaclient.ui.login.LoginActivity"/> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="set-option" value="package-name:com.netflix.mediaclient"/> + <option name="set-option" value="ui-automator-mode:true"/> + <option name="class" value="com.android.csuite.tests.AppCrawlTest" /> + </test> + <!-- Needed for pushing the trace config file --> <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml new file mode 100644 index 000000000000..a2fc6b45c2ad --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml @@ -0,0 +1,3 @@ +<configuration description="Flicker tests C-Suite Crawler Test Plan"> + <target_preparer class="com.android.csuite.core.AppCrawlTesterHostPreparer"/> +</configuration>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt index 42191d1c5feb..182a9089d040 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt @@ -31,10 +31,18 @@ import org.junit.runners.Parameterized abstract class AppsEnterPipTransition(flicker: LegacyFlickerTest) : EnterPipTransition(flicker) { protected abstract val standardAppHelper: StandardAppHelper + protected abstract val permissions: Array<String> + @FlickerBuilderProvider override fun buildFlicker(): FlickerBuilder { return FlickerBuilder(instrumentation).apply { instrumentation.uiAutomation.adoptShellPermissionIdentity() + for (permission in permissions) { + instrumentation.uiAutomation.grantRuntimePermission( + standardAppHelper.packageName, + permission + ) + } setup { flicker.scenario.setIsTablet(tapl.isTablet) } transition() } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt index 4da52ef1272c..d06cf775ca60 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.pip.apps +import android.Manifest import android.content.Context import android.location.Criteria import android.location.Location @@ -64,6 +65,9 @@ import org.junit.runners.Parameterized open class MapsEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransition(flicker) { override val standardAppHelper: MapsAppHelper = MapsAppHelper(instrumentation) + override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.ACCESS_FINE_LOCATION) + val locationManager: LocationManager = instrumentation.context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val mainHandler = Handler(Looper.getMainLooper()) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt index 5498e8c4f970..32f12592135d 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.pip.apps +import android.Manifest import android.platform.test.annotations.Postsubmit import android.tools.common.NavBar import android.tools.common.Rotation @@ -62,6 +63,8 @@ import org.junit.runners.Parameterized open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransition(flicker) { override val standardAppHelper: NetflixAppHelper = NetflixAppHelper(instrumentation) + override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS) + override val defaultEnterPip: FlickerBuilder.() -> Unit = { setup { standardAppHelper.launchViaIntent( diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt index d8afc25caf71..509b32c11afe 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.pip.apps +import android.Manifest import android.platform.test.annotations.Postsubmit import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.apphelpers.YouTubeAppHelper @@ -58,6 +59,8 @@ import org.junit.runners.Parameterized open class YouTubeEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransition(flicker) { override val standardAppHelper: YouTubeAppHelper = YouTubeAppHelper(instrumentation) + override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS) + override val defaultEnterPip: FlickerBuilder.() -> Unit = { setup { standardAppHelper.launchViaIntent( diff --git a/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto index 406ada97a07d..fc15ff9b9af8 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto +++ b/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto @@ -63,11 +63,7 @@ data_sources: { atrace_categories: "sched_process_exit" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker" - atrace_apps: "com.android.wm.shell.flicker.other" - atrace_apps: "com.android.wm.shell.flicker.bubbles" atrace_apps: "com.android.wm.shell.flicker.pip" - atrace_apps: "com.android.wm.shell.flicker.splitscreen" atrace_apps: "com.google.android.apps.nexuslauncher" } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/Android.bp b/libs/WindowManager/Shell/tests/flicker/service/Android.bp index 9b8cd94d56b2..4f1a68a1a74e 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/service/Android.bp @@ -52,8 +52,10 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.service", instrumentation_target_package: "com.android.wm.shell.flicker.service", + test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*.kt"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -62,6 +64,8 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.service", instrumentation_target_package: "com.android.wm.shell.flicker.service", + test_config_template: "AndroidTestTemplate.xml", srcs: [":WMShellFlickerServicePlatinumTests-src"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt index 80ab24ddf9ef..824e45403d2a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -64,4 +66,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt index d7b306c3be23..03170a326890 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt @@ -57,10 +57,13 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + + tapl.enableBlockTimeout(true) } @Test open fun enterSplitScreenByDragFromAllApps() { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .openAllApps() .getAppIcon(secondaryApp.appName) @@ -72,5 +75,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun teardown() { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) + tapl.enableBlockTimeout(false) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt index cc982d1ba860..c52ada3b312f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.flicker.rules.ChangeDisplayOrientationRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -29,6 +30,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -75,4 +77,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { secondaryApp.exit(wmHelper) sendNotificationApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt index fa12bb869467..479d01ddaeb9 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.flicker.rules.ChangeDisplayOrientationRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -29,6 +30,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -57,10 +59,13 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + + tapl.enableBlockTimeout(true) } @Test open fun enterSplitScreenByDragFromShortcut() { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .openDeepShortcutMenu() @@ -81,5 +86,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun teardwon() { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) + tapl.enableBlockTimeout(false) + } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt index 2592fd40d902..625c56bc4a4c 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,6 +29,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -52,6 +54,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + tapl.enableBlockTimeout(true) + tapl.goHome() SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) primaryApp.launchViaIntent(wmHelper) @@ -59,6 +63,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Test open fun enterSplitScreenByDragFromTaskbar() { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .dragToSplitscreen(secondaryApp.packageName, primaryApp.packageName) @@ -69,5 +74,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun teardown() { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) + tapl.enableBlockTimeout(false) + } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt index 983653b9b5ca..f1a011c0d191 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -70,4 +72,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt index 068171d2e129..c9b1c916ff4b 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt @@ -20,6 +20,7 @@ import android.app.Instrumentation import android.graphics.Point import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.helpers.WindowUtils import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -29,6 +30,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -149,4 +151,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { val LARGE_SCREEN_DP_THRESHOLD = 600 return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt index 64b75c5fd967..72f2db3380dd 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -67,4 +69,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt index 179501089168..511de4fd8b90 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -66,4 +68,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt index 7065846dc653..558d2bf1f349 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -68,4 +70,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt index 251cb50de017..ecd68295df9a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -69,4 +71,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { thirdApp.exit(wmHelper) fourthApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt index a9933bbe09fc..f50d5c7df8d7 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -66,4 +68,8 @@ abstract class UnlockKeyguardToSplitScreen { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto index 406ada97a07d..9f2e49755fec 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto +++ b/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto @@ -63,11 +63,7 @@ data_sources: { atrace_categories: "sched_process_exit" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker" - atrace_apps: "com.android.wm.shell.flicker.other" - atrace_apps: "com.android.wm.shell.flicker.bubbles" - atrace_apps: "com.android.wm.shell.flicker.pip" - atrace_apps: "com.android.wm.shell.flicker.splitscreen" + atrace_apps: "com.android.wm.shell.flicker.service" atrace_apps: "com.google.android.apps.nexuslauncher" } } diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp index 4629c5318366..f813b0d3b0b7 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp @@ -54,11 +54,13 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.splitscreen", instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsSplitScreenBase-src", ":WMShellFlickerTestsSplitScreenGroup1-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -74,4 +76,5 @@ android_test { ":WMShellFlickerTestsSplitScreenGroup1-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml index fdda5974d1f9..05f937ab6795 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml @@ -95,6 +95,8 @@ <option name="pull-pattern-keys" value="perfetto_file_path"/> <option name="directory-keys" value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> + <option name="directory-keys" + value="/data/user/0/com.android.wm.shell.flicker.service/files"/> <option name="collect-on-run-ended-only" value="true"/> <option name="clean-up" value="true"/> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt index 394864ad9d4d..5c43cbdb3832 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt @@ -23,6 +23,7 @@ import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -42,8 +43,10 @@ abstract class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: setup { tapl.goHome() primaryApp.launchViaIntent(wmHelper) + tapl.enableBlockTimeout(true) } transitions { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .openAllApps() .getAppIcon(secondaryApp.appName) @@ -57,6 +60,11 @@ abstract class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: Assume.assumeTrue(tapl.isTablet) } + @After + fun after() { + tapl.enableBlockTimeout(false) + } + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt index 3b3be84f9841..15ad0c12c49a 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt @@ -23,6 +23,7 @@ import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -42,13 +43,20 @@ abstract class EnterSplitScreenByDragFromShortcutBenchmark( Assume.assumeTrue(tapl.isTablet) } + @After + fun after() { + tapl.enableBlockTimeout(false) + } + protected val thisTransition: FlickerBuilder.() -> Unit = { setup { tapl.goHome() SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) primaryApp.launchViaIntent(wmHelper) + tapl.enableBlockTimeout(true) } transitions { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .openDeepShortcutMenu() diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt index eff355987cc0..ca8adb1fcb38 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt @@ -23,6 +23,7 @@ import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -44,6 +45,7 @@ abstract class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: primaryApp.launchViaIntent(wmHelper) } transitions { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .dragToSplitscreen(secondaryApp.packageName, primaryApp.packageName) @@ -54,6 +56,12 @@ abstract class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: @Before fun before() { Assume.assumeTrue(tapl.isTablet) + tapl.enableBlockTimeout(true) + } + + @After + fun after() { + tapl.enableBlockTimeout(false) } companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto index 406ada97a07d..67316d2d7c0f 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto @@ -63,10 +63,7 @@ data_sources: { atrace_categories: "sched_process_exit" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker" - atrace_apps: "com.android.wm.shell.flicker.other" - atrace_apps: "com.android.wm.shell.flicker.bubbles" - atrace_apps: "com.android.wm.shell.flicker.pip" + atrace_apps: "com.android.wm.shell.flicker.service" atrace_apps: "com.android.wm.shell.flicker.splitscreen" atrace_apps: "com.google.android.apps.nexuslauncher" } diff --git a/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto deleted file mode 100644 index 406ada97a07d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (C) 2023 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. - -# proto-message: TraceConfig - -# Enable periodic flushing of the trace buffer into the output file. -write_into_file: true - -# Writes the userspace buffer into the file every 1s. -file_write_period_ms: 2500 - -# See b/126487238 - we need to guarantee ordering of events. -flush_period_ms: 30000 - -# The trace buffers needs to be big enough to hold |file_write_period_ms| of -# trace data. The trace buffer sizing depends on the number of trace categories -# enabled and the device activity. - -# RSS events -buffers: { - size_kb: 63488 - fill_policy: RING_BUFFER -} - -data_sources { - config { - name: "linux.process_stats" - target_buffer: 0 - # polled per-process memory counters and process/thread names. - # If you don't want the polled counters, remove the "process_stats_config" - # section, but keep the data source itself as it still provides on-demand - # thread/process naming for ftrace data below. - process_stats_config { - scan_all_processes_on_start: true - } - } -} - -data_sources: { - config { - name: "linux.ftrace" - ftrace_config { - ftrace_events: "ftrace/print" - ftrace_events: "task/task_newtask" - ftrace_events: "task/task_rename" - atrace_categories: "ss" - atrace_categories: "wm" - atrace_categories: "am" - atrace_categories: "aidl" - atrace_categories: "input" - atrace_categories: "binder_driver" - atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker.testapp" - atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker" - atrace_apps: "com.android.wm.shell.flicker.other" - atrace_apps: "com.android.wm.shell.flicker.bubbles" - atrace_apps: "com.android.wm.shell.flicker.pip" - atrace_apps: "com.android.wm.shell.flicker.splitscreen" - atrace_apps: "com.google.android.apps.nexuslauncher" - } - } -} - diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestHandler.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestHandler.java new file mode 100644 index 000000000000..b91d6f90ed8e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 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.os.Handler; +import android.os.Looper; +import android.os.Message; + + +/** + * Basic test handler that immediately executes anything that is posted on it. + */ +public class TestHandler extends Handler { + public TestHandler(Looper looper) { + super(looper); + } + + @Override + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + dispatchMessage(msg); + return true; + } +} 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 26c73946c1c6..dab762f233e2 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 @@ -192,7 +192,7 @@ public class BubbleDataTest extends ShellTestCase { mMainExecutor); mPositioner = new TestableBubblePositioner(mContext, - mock(WindowManager.class)); + mContext.getSystemService(WindowManager.class)); mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, mEducationController, mMainExecutor); @@ -221,6 +221,22 @@ public class BubbleDataTest extends ShellTestCase { } @Test + public void testAddAppBubble_setsTime() { + // Setup + mBubbleData.setListener(mListener); + + // Test + assertThat(mAppBubble.getLastActivity()).isEqualTo(0); + setCurrentTime(1000); + mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/, + false /* showInShade */); + + // Verify + assertThat(mBubbleData.getBubbleInStackWithKey(mAppBubble.getKey())).isEqualTo(mAppBubble); + assertThat(mAppBubble.getLastActivity()).isEqualTo(1000); + } + + @Test public void testRemoveBubble() { // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); @@ -1162,7 +1178,7 @@ public class BubbleDataTest extends ShellTestCase { } @Test - public void test_removeAppBubble_skipsOverflow() { + public void test_removeAppBubble_overflows() { String appBubbleKey = mAppBubble.getKey(); mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/, false /* showInShade */); @@ -1170,7 +1186,7 @@ public class BubbleDataTest extends ShellTestCase { mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_USER_GESTURE); - assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull(); + assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isEqualTo(mAppBubble); assertThat(mBubbleData.getBubbleInStackWithKey(appBubbleKey)).isNull(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java index cb29a21e2f5b..f5b0174642d1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java @@ -53,7 +53,8 @@ public class BubbleOverflowTest extends ShellTestCase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); + mPositioner = new TestableBubblePositioner(mContext, + mContext.getSystemService(WindowManager.class)); when(mBubbleController.getPositioner()).thenReturn(mPositioner); when(mBubbleController.getStackView()).thenReturn(mock(BubbleStackView.class)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java index 287a97c9b5b0..6ebee730756e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java @@ -16,33 +16,22 @@ package com.android.wm.shell.bubbles; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.view.View.LAYOUT_DIRECTION_LTR; -import static android.view.View.LAYOUT_DIRECTION_RTL; +import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; import android.content.Intent; -import android.content.res.Configuration; +import android.content.pm.ShortcutInfo; import android.graphics.Insets; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.os.UserHandle; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableResources; -import android.util.DisplayMetrics; -import android.view.WindowInsets; import android.view.WindowManager; -import android.view.WindowMetrics; import androidx.test.filters.SmallTest; @@ -52,36 +41,20 @@ import com.android.wm.shell.ShellTestCase; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests operations and the resulting state managed by {@link BubblePositioner}. */ @SmallTest @RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) public class BubblePositionerTest extends ShellTestCase { - private static final int MIN_WIDTH_FOR_TABLET = 600; - private BubblePositioner mPositioner; - private Configuration mConfiguration; - - @Mock - private WindowManager mWindowManager; - @Mock - private WindowMetrics mWindowMetrics; @Before public void setUp() { - MockitoAnnotations.initMocks(this); - - mConfiguration = spy(new Configuration()); - TestableResources testableResources = mContext.getOrCreateTestableResources(); - testableResources.overrideConfiguration(mConfiguration); - - mPositioner = new BubblePositioner(mContext, mWindowManager); + WindowManager windowManager = mContext.getSystemService(WindowManager.class); + mPositioner = new BubblePositioner(mContext, windowManager); } @Test @@ -91,11 +64,11 @@ public class BubblePositionerTest extends ShellTestCase { Rect availableRect = new Rect(screenBounds); availableRect.inset(insets); - new WindowManagerConfig() + DeviceConfig deviceConfig = new ConfigBuilder() .setInsets(insets) .setScreenBounds(screenBounds) - .setUpConfig(); - mPositioner.update(); + .build(); + mPositioner.update(deviceConfig); assertThat(mPositioner.getAvailableRect()).isEqualTo(availableRect); assertThat(mPositioner.isLandscape()).isFalse(); @@ -105,16 +78,16 @@ public class BubblePositionerTest extends ShellTestCase { @Test public void testShowBubblesVertically_phonePortrait() { - new WindowManagerConfig().setOrientation(ORIENTATION_PORTRAIT).setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().build(); + mPositioner.update(deviceConfig); assertThat(mPositioner.showBubblesVertically()).isFalse(); } @Test public void testShowBubblesVertically_phoneLandscape() { - new WindowManagerConfig().setOrientation(ORIENTATION_LANDSCAPE).setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLandscape().build(); + mPositioner.update(deviceConfig); assertThat(mPositioner.isLandscape()).isTrue(); assertThat(mPositioner.showBubblesVertically()).isTrue(); @@ -122,8 +95,8 @@ public class BubblePositionerTest extends ShellTestCase { @Test public void testShowBubblesVertically_tablet() { - new WindowManagerConfig().setLargeScreen().setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build(); + mPositioner.update(deviceConfig); assertThat(mPositioner.showBubblesVertically()).isTrue(); } @@ -131,8 +104,8 @@ public class BubblePositionerTest extends ShellTestCase { /** If a resting position hasn't been set, calling it will return the default position. */ @Test public void testGetRestingPosition_returnsDefaultPosition() { - new WindowManagerConfig().setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().build(); + mPositioner.update(deviceConfig); PointF restingPosition = mPositioner.getRestingPosition(); PointF defaultPosition = mPositioner.getDefaultStartPosition(); @@ -143,8 +116,8 @@ public class BubblePositionerTest extends ShellTestCase { /** If a resting position has been set, it'll return that instead of the default position. */ @Test public void testGetRestingPosition_returnsRestingPosition() { - new WindowManagerConfig().setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().build(); + mPositioner.update(deviceConfig); PointF restingPosition = new PointF(100, 100); mPositioner.setRestingPosition(restingPosition); @@ -155,8 +128,8 @@ public class BubblePositionerTest extends ShellTestCase { /** Test that the default resting position on phone is in upper left. */ @Test public void testGetRestingPosition_bubble_onPhone() { - new WindowManagerConfig().setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().build(); + mPositioner.update(deviceConfig); RectF allowableStackRegion = mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); @@ -168,8 +141,8 @@ public class BubblePositionerTest extends ShellTestCase { @Test public void testGetRestingPosition_bubble_onPhone_RTL() { - new WindowManagerConfig().setLayoutDirection(LAYOUT_DIRECTION_RTL).setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setRtl().build(); + mPositioner.update(deviceConfig); RectF allowableStackRegion = mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); @@ -182,8 +155,8 @@ public class BubblePositionerTest extends ShellTestCase { /** Test that the default resting position on tablet is middle left. */ @Test public void testGetRestingPosition_chatBubble_onTablet() { - new WindowManagerConfig().setLargeScreen().setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build(); + mPositioner.update(deviceConfig); RectF allowableStackRegion = mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); @@ -195,9 +168,8 @@ public class BubblePositionerTest extends ShellTestCase { @Test public void testGetRestingPosition_chatBubble_onTablet_RTL() { - new WindowManagerConfig().setLargeScreen().setLayoutDirection( - LAYOUT_DIRECTION_RTL).setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build(); + mPositioner.update(deviceConfig); RectF allowableStackRegion = mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); @@ -210,8 +182,8 @@ public class BubblePositionerTest extends ShellTestCase { /** Test that the default resting position on tablet is middle right. */ @Test public void testGetDefaultPosition_appBubble_onTablet() { - new WindowManagerConfig().setLargeScreen().setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build(); + mPositioner.update(deviceConfig); RectF allowableStackRegion = mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); @@ -223,9 +195,8 @@ public class BubblePositionerTest extends ShellTestCase { @Test public void testGetRestingPosition_appBubble_onTablet_RTL() { - new WindowManagerConfig().setLargeScreen().setLayoutDirection( - LAYOUT_DIRECTION_RTL).setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build(); + mPositioner.update(deviceConfig); RectF allowableStackRegion = mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */); @@ -237,9 +208,8 @@ public class BubblePositionerTest extends ShellTestCase { @Test public void testHasUserModifiedDefaultPosition_false() { - new WindowManagerConfig().setLargeScreen().setLayoutDirection( - LAYOUT_DIRECTION_RTL).setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build(); + mPositioner.update(deviceConfig); assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse(); @@ -250,9 +220,8 @@ public class BubblePositionerTest extends ShellTestCase { @Test public void testHasUserModifiedDefaultPosition_true() { - new WindowManagerConfig().setLargeScreen().setLayoutDirection( - LAYOUT_DIRECTION_RTL).setUpConfig(); - mPositioner.update(); + DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build(); + mPositioner.update(deviceConfig); assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse(); @@ -262,24 +231,300 @@ public class BubblePositionerTest extends ShellTestCase { } @Test - public void testExpandedViewHeight_onLargeTablet() { + public void testGetExpandedViewHeight_max() { Insets insets = Insets.of(10, 20, 5, 15); Rect screenBounds = new Rect(0, 0, 1800, 2600); - new WindowManagerConfig() + DeviceConfig deviceConfig = new ConfigBuilder() .setLargeScreen() .setInsets(insets) .setScreenBounds(screenBounds) - .setUpConfig(); - mPositioner.update(); + .build(); + mPositioner.update(deviceConfig); Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT); + } + + @Test + public void testGetExpandedViewHeight_customHeight_valid() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + final int minHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_expanded_default_height); + Bubble bubble = new Bubble("key", + mock(ShortcutInfo.class), + minHeight + 100 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor(), + mock(Bubbles.BubbleMetadataFlagListener.class)); + + // Ensure the height is the same as the desired value + assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo( + bubble.getDesiredHeight(mContext)); + } + + + @Test + public void testGetExpandedViewHeight_customHeight_tooSmall() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Bubble bubble = new Bubble("key", + mock(ShortcutInfo.class), + 10 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor(), + mock(Bubbles.BubbleMetadataFlagListener.class)); + + // Ensure the height is the same as the minimum value + final int minHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_expanded_default_height); + assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight); + } + + @Test + public void testGetMaxExpandedViewHeight_onLargeTablet() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + int manageButtonHeight = mContext.getResources().getDimensionPixelSize(R.dimen.bubble_manage_button_height); - float expectedHeight = 1800 - 2 * 20 - manageButtonHeight; - assertThat(mPositioner.getExpandedViewHeight(bubble)).isWithin(0.1f).of(expectedHeight); + int pointerWidth = mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_pointer_width); + int expandedViewPadding = mContext.getResources().getDimensionPixelSize(R + .dimen.bubble_expanded_view_padding); + float expectedHeight = 1800 - 2 * 20 - manageButtonHeight - pointerWidth + - expandedViewPadding * 2; + assertThat(((float) mPositioner.getMaxExpandedViewHeight(false /* isOverflow */))) + .isWithin(0.1f).of(expectedHeight); + } + + @Test + public void testAreBubblesBottomAligned_largeScreen_true() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + assertThat(mPositioner.areBubblesBottomAligned()).isTrue(); + } + + @Test + public void testAreBubblesBottomAligned_largeScreen_false() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setLandscape() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + assertThat(mPositioner.areBubblesBottomAligned()).isFalse(); + } + + @Test + public void testAreBubblesBottomAligned_smallTablet_false() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setSmallTablet() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + assertThat(mPositioner.areBubblesBottomAligned()).isFalse(); + } + + @Test + public void testAreBubblesBottomAligned_phone_false() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + assertThat(mPositioner.areBubblesBottomAligned()).isFalse(); + } + + @Test + public void testExpandedViewY_phoneLandscape() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLandscape() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); + Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + + // This bubble will have max height so it'll always be top aligned + assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(mPositioner.getExpandedViewYTopAligned()); + } + + @Test + public void testExpandedViewY_phonePortrait() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); + Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + + // Always top aligned in phone portrait + assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(mPositioner.getExpandedViewYTopAligned()); + } + + @Test + public void testExpandedViewY_smallTabletLandscape() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setSmallTablet() + .setLandscape() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); + Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + + // This bubble will have max height which is always top aligned on small tablets + assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(mPositioner.getExpandedViewYTopAligned()); + } + + @Test + public void testExpandedViewY_smallTabletPortrait() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setSmallTablet() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); + Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + + // This bubble will have max height which is always top aligned on small tablets + assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(mPositioner.getExpandedViewYTopAligned()); + } + + @Test + public void testExpandedViewY_largeScreenLandscape() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setLandscape() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); + Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + + // This bubble will have max height which is always top aligned on landscape, large tablet + assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(mPositioner.getExpandedViewYTopAligned()); + } + + @Test + public void testExpandedViewY_largeScreenPortrait() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); + Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + + int manageButtonHeight = + mContext.getResources().getDimensionPixelSize(R.dimen.bubble_manage_button_height); + int manageButtonPlusMargin = manageButtonHeight + 2 + * mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_manage_button_margin); + int pointerWidth = mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_pointer_width); + + final float expectedExpandedViewY = mPositioner.getAvailableRect().bottom + - manageButtonPlusMargin + - mPositioner.getExpandedViewHeightForLargeScreen() + - pointerWidth; + + // Bubbles are bottom aligned on portrait, large tablet + assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(expectedExpandedViewY); } /** @@ -311,58 +556,47 @@ public class BubblePositionerTest extends ShellTestCase { * Sets up window manager to return config values based on what you need for the test. * By default it sets up a portrait phone without any insets. */ - private class WindowManagerConfig { + private static class ConfigBuilder { private Rect mScreenBounds = new Rect(0, 0, 1000, 2000); private boolean mIsLargeScreen = false; - private int mOrientation = ORIENTATION_PORTRAIT; - private int mLayoutDirection = LAYOUT_DIRECTION_LTR; + private boolean mIsSmallTablet = false; + private boolean mIsLandscape = false; + private boolean mIsRtl = false; private Insets mInsets = Insets.of(0, 0, 0, 0); - public WindowManagerConfig setScreenBounds(Rect screenBounds) { + public ConfigBuilder setScreenBounds(Rect screenBounds) { mScreenBounds = screenBounds; return this; } - public WindowManagerConfig setLargeScreen() { + public ConfigBuilder setLargeScreen() { mIsLargeScreen = true; return this; } - public WindowManagerConfig setOrientation(int orientation) { - mOrientation = orientation; + public ConfigBuilder setSmallTablet() { + mIsSmallTablet = true; return this; } - public WindowManagerConfig setLayoutDirection(int layoutDirection) { - mLayoutDirection = layoutDirection; + public ConfigBuilder setLandscape() { + mIsLandscape = true; return this; } - public WindowManagerConfig setInsets(Insets insets) { - mInsets = insets; + public ConfigBuilder setRtl() { + mIsRtl = true; return this; } - public void setUpConfig() { - mConfiguration.smallestScreenWidthDp = mIsLargeScreen - ? MIN_WIDTH_FOR_TABLET - : MIN_WIDTH_FOR_TABLET - 1; - mConfiguration.orientation = mOrientation; - mConfiguration.screenWidthDp = pxToDp(mScreenBounds.width()); - mConfiguration.screenHeightDp = pxToDp(mScreenBounds.height()); - - when(mConfiguration.getLayoutDirection()).thenReturn(mLayoutDirection); - WindowInsets windowInsets = mock(WindowInsets.class); - when(windowInsets.getInsetsIgnoringVisibility(anyInt())).thenReturn(mInsets); - when(mWindowMetrics.getWindowInsets()).thenReturn(windowInsets); - when(mWindowMetrics.getBounds()).thenReturn(mScreenBounds); - when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + public ConfigBuilder setInsets(Insets insets) { + mInsets = insets; + return this; } - private int pxToDp(float px) { - int dpi = mContext.getResources().getDisplayMetrics().densityDpi; - float dp = px / ((float) dpi / DisplayMetrics.DENSITY_DEFAULT); - return (int) dp; + private DeviceConfig build() { + return new DeviceConfig(mIsLargeScreen, mIsSmallTablet, mIsLandscape, mIsRtl, + mScreenBounds, mInsets); } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java index 44ff35466ae2..c4b9c9ba43f1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java @@ -55,8 +55,6 @@ public class BubblesNavBarMotionEventHandlerTest extends ShellTestCase { private BubblesNavBarMotionEventHandler mMotionEventHandler; @Mock - private WindowManager mWindowManager; - @Mock private Runnable mInterceptTouchRunnable; @Mock private MotionEventListener mMotionEventListener; @@ -66,7 +64,7 @@ public class BubblesNavBarMotionEventHandlerTest extends ShellTestCase { public void setUp() { MockitoAnnotations.initMocks(this); TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(), - mWindowManager); + getContext().getSystemService(WindowManager.class)); mMotionEventHandler = new BubblesNavBarMotionEventHandler(getContext(), positioner, mInterceptTouchRunnable, mMotionEventListener); mMotionEventTime = SystemClock.uptimeMillis(); 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 335222e98c6c..c1ff260836b8 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 @@ -66,7 +66,8 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC public void setUp() throws Exception { super.setUp(); - mPositioner = new BubblePositioner(getContext(), mock(WindowManager.class)); + mPositioner = new BubblePositioner(getContext(), + getContext().getSystemService(WindowManager.class)); mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, Insets.of(0, 0, 0, 0), new Rect(0, 0, mDisplayWidth, mDisplayHeight)); @@ -105,7 +106,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC verify(afterExpand).run(); Runnable afterCollapse = mock(Runnable.class); - mExpandedController.collapseBackToStack(mExpansionPoint, afterCollapse); + mExpandedController.collapseBackToStack(mExpansionPoint, false, afterCollapse); waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java index 991913afbb90..f6609872852f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java @@ -50,9 +50,6 @@ public class ExpandedViewAnimationControllerTest extends ShellTestCase { private ExpandedViewAnimationController mController; @Mock - private WindowManager mWindowManager; - - @Mock private BubbleExpandedView mMockExpandedView; @Before @@ -60,7 +57,7 @@ public class ExpandedViewAnimationControllerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); TestableBubblePositioner positioner = new TestableBubblePositioner(getContext(), - mWindowManager); + getContext().getSystemService(WindowManager.class)); mController = new ExpandedViewAnimationControllerImpl(getContext(), positioner); mController.setExpandedView(mMockExpandedView); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java index 31fafcaab7cc..0c22908be9eb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java @@ -313,7 +313,8 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase bubbleCountSupplier, onBubbleAnimatedOutAction, onStackAnimationFinished, - new TestableBubblePositioner(mContext, mock(WindowManager.class))); + new TestableBubblePositioner(mContext, + mContext.getSystemService(WindowManager.class))); } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index fde6acb9bfe5..94c862bd7a4f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -63,6 +63,7 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS +import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_DESKTOP_MODE import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.google.common.truth.Truth.assertThat @@ -392,8 +393,8 @@ class DesktopTasksControllerTest : ShellTestCase() { fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToFullscreen(task) - val wct = getLatestWct(type = TRANSIT_CHANGE) + controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration) + val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) } @@ -402,15 +403,15 @@ class DesktopTasksControllerTest : ShellTestCase() { fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() { val task = setUpFreeformTask() task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM - controller.moveToFullscreen(task) - val wct = getLatestWct(type = TRANSIT_CHANGE) + controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration) + val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) } @Test fun moveToFullscreen_nonExistentTask_doesNothing() { - controller.moveToFullscreen(999) + controller.moveToFullscreen(999, desktopModeWindowDecoration) verifyWCTNotExecuted() } @@ -419,9 +420,9 @@ class DesktopTasksControllerTest : ShellTestCase() { val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) - controller.moveToFullscreen(taskDefaultDisplay) + controller.moveToFullscreen(taskDefaultDisplay.taskId, desktopModeWindowDecoration) - with(getLatestWct(type = TRANSIT_CHANGE)) { + with(getLatestExitDesktopWct()) { assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder()) } @@ -808,6 +809,17 @@ class DesktopTasksControllerTest : ShellTestCase() { return arg.value } + private fun getLatestExitDesktopWct(): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (ENABLE_SHELL_TRANSITIONS) { + verify(exitDesktopTransitionHandler) + .startTransition(eq(TRANSIT_EXIT_DESKTOP_MODE), arg.capture(), any(), any()) + } else { + verify(shellTaskOrganizer).applyTransaction(arg.capture()) + } + return arg.value + } + private fun verifyWCTNotExecuted() { if (ENABLE_SHELL_TRANSITIONS) { verify(transitions, never()).startTransition(anyInt(), any(), isNull()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index a5629c8f8f15..3bc90ade898e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -13,23 +13,26 @@ import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl import android.window.TransitionInfo import android.window.TransitionInfo.FLAG_IS_WALLPAPER +import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest -import com.android.server.testutils.any -import com.android.server.testutils.mock import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP +import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import java.util.function.Supplier +import junit.framework.Assert.assertFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions @@ -113,6 +116,40 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + fun startDragToDesktop_aborted_finishDropped() { + val task = createTask() + val dragAnimator = mock<MoveToDesktopAnimator>() + // Simulate transition is started. + val transition = startDragToDesktopTransition(task, dragAnimator) + // But the transition was aborted. + handler.onTransitionConsumed(transition, aborted = true, mock()) + + // Attempt to finish the failed drag start. + handler.finishDragToDesktopTransition(WindowContainerTransaction()) + + // Should not be attempted and state should be reset. + verify(transitions, never()) + .startTransition(eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), any()) + assertFalse(handler.inProgress) + } + + @Test + fun startDragToDesktop_aborted_cancelDropped() { + val task = createTask() + val dragAnimator = mock<MoveToDesktopAnimator>() + // Simulate transition is started. + val transition = startDragToDesktopTransition(task, dragAnimator) + // But the transition was aborted. + handler.onTransitionConsumed(transition, aborted = true, mock()) + + // Attempt to finish the failed drag start. + handler.cancelDragToDesktopTransition() + + // Should not be attempted and state should be reset. + assertFalse(handler.inProgress) + } + + @Test fun cancelDragToDesktop_startWasReady_cancel() { val task = createTask() val dragAnimator = mock<MoveToDesktopAnimator>() 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 527dc0149716..1b347e01888e 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 @@ -204,6 +204,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { @Test public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() { + doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mHomeTask); DragSession dragSession = new DragSession(mContext, mActivityTaskManager, mLandscapeDisplayLayout, mActivityClipData); @@ -219,6 +220,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { @Test public void testDragAppOverFullscreenApp_expectSplitScreenTargets() { + doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mContext, mActivityTaskManager, mLandscapeDisplayLayout, mActivityClipData); @@ -239,6 +241,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { @Test public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { + doReturn(false).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mContext, mActivityTaskManager, mPortraitDisplayLayout, mActivityClipData); 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 4e2b7f6d16b2..800f9e4e5371 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 @@ -66,6 +66,7 @@ import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -283,7 +284,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) .round(any(), any(), anyBoolean()); doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) - .scale(any(), any(), any(), any(), anyFloat()); + .scale(any(), any(), any(), ArgumentMatchers.<Rect>any(), anyFloat()); doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) .alpha(any(), any(), anyFloat()); doNothing().when(mMockPipSurfaceTransactionHelper).onDensityOrFontScaleChanged(any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenUtilsTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenUtilsTests.java new file mode 100644 index 000000000000..30847d3ac192 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenUtilsTests.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 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 org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.res.Configuration; +import android.graphics.Rect; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.split.SplitScreenUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + + +/** Tests for {@link com.android.wm.shell.common.split.SplitScreenUtils} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitScreenUtilsTests extends ShellTestCase { + + @Test + public void testIsLeftRightSplit() { + Configuration portraitTablet = new Configuration(); + portraitTablet.smallestScreenWidthDp = 720; + portraitTablet.windowConfiguration.setMaxBounds(new Rect(0, 0, 500, 1000)); + Configuration landscapeTablet = new Configuration(); + landscapeTablet.smallestScreenWidthDp = 720; + landscapeTablet.windowConfiguration.setMaxBounds(new Rect(0, 0, 1000, 500)); + Configuration portraitPhone = new Configuration(); + portraitPhone.smallestScreenWidthDp = 420; + portraitPhone.windowConfiguration.setMaxBounds(new Rect(0, 0, 500, 1000)); + Configuration landscapePhone = new Configuration(); + landscapePhone.smallestScreenWidthDp = 420; + landscapePhone.windowConfiguration.setMaxBounds(new Rect(0, 0, 1000, 500)); + + // Allow L/R split in portrait = false + assertTrue(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + landscapeTablet)); + assertTrue(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + landscapePhone)); + assertFalse(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + portraitTablet)); + assertFalse(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + portraitPhone)); + + // Allow L/R split in portrait = true, only affects large screens + assertFalse(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + landscapeTablet)); + assertTrue(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + landscapePhone)); + assertTrue(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + portraitTablet)); + assertFalse(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + portraitPhone)); + } +} 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 fff65f364121..d819261ecba2 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 @@ -140,7 +140,7 @@ public class StageCoordinatorTests extends ShellTestCase { when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds); - when(mSplitLayout.isLandscape()).thenReturn(false); + when(mSplitLayout.isLeftRightSplit()).thenReturn(false); when(mSplitLayout.applyTaskChanges(any(), any(), any())).thenReturn(true); when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 4afb29ecd98c..d7c46104b6b1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -40,6 +40,7 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.Context; +import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; @@ -58,6 +59,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestHandler; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable; @@ -92,8 +94,7 @@ public class TaskViewTest extends ShellTestCase { Transitions mTransitions; @Mock Looper mViewLooper; - @Mock - Handler mViewHandler; + TestHandler mViewHandler; SurfaceSession mSession; SurfaceControl mLeash; @@ -112,7 +113,7 @@ public class TaskViewTest extends ShellTestCase { mContext = getContext(); doReturn(true).when(mViewLooper).isCurrentThread(); - doReturn(mViewLooper).when(mViewHandler).getLooper(); + mViewHandler = spy(new TestHandler(mViewLooper)); mTaskInfo = new ActivityManager.RunningTaskInfo(); mTaskInfo.token = mToken; @@ -668,4 +669,24 @@ public class TaskViewTest extends ShellTestCase { mTaskViewTaskController.onTaskInfoChanged(mTaskInfo); verify(mViewHandler).post(any()); } + + @Test + public void testSetResizeBgOnSameUiThread_expectUsesTransaction() { + SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); + mTaskView = spy(mTaskView); + mTaskView.setResizeBgColor(tx, Color.BLUE); + verify(mViewHandler, never()).post(any()); + verify(mTaskView, never()).setResizeBackgroundColor(eq(Color.BLUE)); + verify(mTaskView).setResizeBackgroundColor(eq(tx), eq(Color.BLUE)); + } + + @Test + public void testSetResizeBgOnDifferentUiThread_expectDoesNotUseTransaction() { + doReturn(false).when(mViewLooper).isCurrentThread(); + SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); + mTaskView = spy(mTaskView); + mTaskView.setResizeBgColor(tx, Color.BLUE); + verify(mViewHandler).post(any()); + verify(mTaskView).setResizeBackgroundColor(eq(Color.BLUE)); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index ea7c0d9c264e..50802c3759c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -18,8 +18,10 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -99,7 +101,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { when(change.getTaskInfo()).thenReturn(taskInfo); when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); - setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true); mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), info, @@ -117,7 +119,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { when(change.getTaskInfo()).thenReturn(taskInfo); when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); - setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_TO_BACK); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_TO_BACK, true); mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), info, @@ -135,7 +137,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { when(change.getTaskInfo()).thenReturn(taskInfo); when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); - setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_UNDEFINED, TRANSIT_TO_BACK); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_UNDEFINED, TRANSIT_TO_BACK, true); mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), info, @@ -145,15 +147,53 @@ public class HomeTransitionObserverTest extends ShellTestCase { verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean()); } + @Test + public void testNonRunningHomeActivityDoesNotTriggerCallback() throws RemoteException { + TransitionInfo info = mock(TransitionInfo.class); + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); + when(change.getTaskInfo()).thenReturn(taskInfo); + when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); + + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_UNDEFINED, TRANSIT_TO_BACK, false); + + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + + verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean()); + } + + @Test + public void testHomeActivityWithBackGestureNotifiesHomeIsVisible() throws RemoteException { + TransitionInfo info = mock(TransitionInfo.class); + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); + when(change.getTaskInfo()).thenReturn(taskInfo); + when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); + + when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE, true); + + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + + verify(mListener, times(1)).onHomeVisibilityChanged(true); + } + /** * Helper class to initialize variables for the rest. */ private void setupTransitionInfo(ActivityManager.RunningTaskInfo taskInfo, TransitionInfo.Change change, @ActivityType int activityType, - @TransitionMode int mode) { + @TransitionMode int mode, + boolean isRunning) { when(taskInfo.getActivityType()).thenReturn(activityType); when(change.getMode()).thenReturn(mode); + taskInfo.isRunning = isRunning; } - } 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 4e300d9a7e69..01c9bd0cb9f7 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 @@ -40,6 +40,8 @@ import static android.window.TransitionInfo.FLAG_SYNC; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -93,6 +95,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.R; +import com.android.internal.policy.TransitionAnimation; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; @@ -1463,6 +1467,43 @@ public class ShellTransitionTests extends ShellTestCase { assertEquals(0, mDefaultHandler.activeCount()); } + @Test + public void testCloseTransitAnimationWhenClosingChangesExists() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + final TransitionAnimation transitionAnimation = new TransitionAnimation(mContext, false, + Transitions.TAG); + spyOn(transitionAnimation); + + // Creating a transition by the app hooking the back key event to start the + // previous activity with FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + // flags in order to clear the top activity and bring the exist previous activity to front. + // Expects the activity transition should playing the close animation instead the initiated + // open animation made by startActivity. + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_CLOSE).addChange(TRANSIT_TO_FRONT).build(); + transitions.onTransitionReady(transitToken, info, new StubTransaction(), + new StubTransaction()); + + final int type = getTransitionTypeFromInfo(info); + assertEquals(TRANSIT_CLOSE, type); + + TransitionAnimationHelper.loadAttributeAnimation(type, info, info.getChanges().get(0), 0, + transitionAnimation, false); + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityCloseExitAnimation), anyBoolean()); + + TransitionAnimationHelper.loadAttributeAnimation(type, info, info.getChanges().get(1), 0, + transitionAnimation, false); + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityCloseEnterAnimation), anyBoolean()); + } + class ChangeBuilder { final TransitionInfo.Change mChange; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 57aa47e85556..883c24e78076 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.Context import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay @@ -39,7 +40,8 @@ import android.view.SurfaceControl import android.view.SurfaceView import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars -import androidx.core.content.getSystemService +import android.view.WindowManager +import android.window.TransitionInfo import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer @@ -291,6 +293,30 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + fun testRelayoutBlockedDuringKeyguardTransition() { + val transition = mock(IBinder::class.java) + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + val transitionInfo = mock(TransitionInfo::class.java) + val transitionChange = mock(TransitionInfo.Change::class.java) + val taskInfo = mock(RunningTaskInfo()::class.java) + + // Replicate a keyguard going away transition for a task + whenever(transitionInfo.getFlags()) + .thenReturn(WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY) + whenever(transitionChange.getMode()).thenReturn(WindowManager.TRANSIT_TO_FRONT) + whenever(transitionChange.getTaskInfo()).thenReturn(taskInfo) + + // Make sure a window decorations exists first by launching a freeform task. + onTaskOpening(task) + // OnTransition ready is called when a keyguard going away transition happens + desktopModeWindowDecorViewModel + .onTransitionReady(transition, transitionInfo, transitionChange) + + verify(decoration).incrementRelayoutBlock() + verify(decoration).addTransitionPausingRelayout(transition) + } + @Test fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) @@ -401,7 +427,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private fun createVirtualDisplay(): VirtualDisplay? { val surfaceView = SurfaceView(mContext) - return mContext.getSystemService<DisplayManager>()?.createVirtualDisplay( + val dm = mContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + return dm.createVirtualDisplay( "testEventReceiversOnMultipleDisplays", /*width=*/ 400, /*height=*/ 400, diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 47a7f3579764..2f28363aedc7 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -63,15 +63,21 @@ cc_library { "AssetsProvider.cpp", "AttributeResolution.cpp", "BigBuffer.cpp", + "BigBufferStream.cpp", "ChunkIterator.cpp", "ConfigDescription.cpp", + "FileStream.cpp", "Idmap.cpp", "LoadedArsc.cpp", "Locale.cpp", "LocaleData.cpp", "misc.cpp", + "NinePatch.cpp", "ObbFile.cpp", "PosixUtils.cpp", + "Png.cpp", + "PngChunkFilter.cpp", + "PngCrunch.cpp", "ResourceTimer.cpp", "ResourceTypes.cpp", "ResourceUtils.cpp", @@ -84,7 +90,10 @@ cc_library { ], export_include_dirs: ["include"], export_shared_lib_headers: ["libz"], - static_libs: ["libincfs-utils"], + static_libs: [ + "libincfs-utils", + "libpng", + ], whole_static_libs: [ "libandroidfw_pathutils", "libincfs-utils", @@ -198,9 +207,11 @@ cc_test { "tests/ConfigDescription_test.cpp", "tests/ConfigLocale_test.cpp", "tests/DynamicRefTable_test.cpp", + "tests/FileStream_test.cpp", "tests/Idmap_test.cpp", "tests/LoadedArsc_test.cpp", "tests/Locale_test.cpp", + "tests/NinePatch_test.cpp", "tests/ResourceTimer_test.cpp", "tests/ResourceUtils_test.cpp", "tests/ResTable_test.cpp", diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index d056248273b2..8748dab581bb 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -603,7 +603,7 @@ std::unique_ptr<Asset> AssetManager2::OpenNonAsset(const std::string& filename, std::unique_ptr<Asset> asset = assets->GetAssetsProvider()->Open(filename, mode); if (asset) { if (out_cookie != nullptr) { - *out_cookie = i; + *out_cookie = i - 1; } return asset; } diff --git a/libs/androidfw/BigBufferStream.cpp b/libs/androidfw/BigBufferStream.cpp new file mode 100644 index 000000000000..f18199cfa52b --- /dev/null +++ b/libs/androidfw/BigBufferStream.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2017 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. + */ + +#include "androidfw/BigBufferStream.h" + +#include <algorithm> + +namespace android { + +// +// BigBufferInputStream +// + +bool BigBufferInputStream::Next(const void** data, size_t* size) { + if (iter_ == buffer_->end()) { + return false; + } + + if (offset_ == iter_->size) { + ++iter_; + if (iter_ == buffer_->end()) { + return false; + } + offset_ = 0; + } + + *data = iter_->buffer.get() + offset_; + *size = iter_->size - offset_; + bytes_read_ += iter_->size - offset_; + offset_ = iter_->size; + return true; +} + +void BigBufferInputStream::BackUp(size_t count) { + if (count > offset_) { + bytes_read_ -= offset_; + offset_ = 0; + } else { + offset_ -= count; + bytes_read_ -= count; + } +} + +bool BigBufferInputStream::CanRewind() const { + return true; +} + +bool BigBufferInputStream::Rewind() { + iter_ = buffer_->begin(); + offset_ = 0; + bytes_read_ = 0; + return true; +} + +size_t BigBufferInputStream::ByteCount() const { + return bytes_read_; +} + +bool BigBufferInputStream::HadError() const { + return false; +} + +size_t BigBufferInputStream::TotalSize() const { + return buffer_->size(); +} + +bool BigBufferInputStream::ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) { + if (byte_count == 0) { + return true; + } + if (offset < 0) { + return false; + } + if (offset > std::numeric_limits<off64_t>::max() - byte_count) { + return false; + } + if (offset + byte_count > buffer_->size()) { + return false; + } + auto p = reinterpret_cast<uint8_t*>(data); + for (auto iter = buffer_->begin(); iter != buffer_->end() && byte_count > 0; ++iter) { + if (offset < iter->size) { + size_t to_read = std::min(byte_count, (size_t)(iter->size - offset)); + memcpy(p, iter->buffer.get() + offset, to_read); + byte_count -= to_read; + p += to_read; + offset = 0; + } else { + offset -= iter->size; + } + } + return byte_count == 0; +} + +// +// BigBufferOutputStream +// + +bool BigBufferOutputStream::Next(void** data, size_t* size) { + *data = buffer_->NextBlock(size); + return true; +} + +void BigBufferOutputStream::BackUp(size_t count) { + buffer_->BackUp(count); +} + +size_t BigBufferOutputStream::ByteCount() const { + return buffer_->size(); +} + +bool BigBufferOutputStream::HadError() const { + return false; +} + +} // namespace android diff --git a/libs/androidfw/FileStream.cpp b/libs/androidfw/FileStream.cpp new file mode 100644 index 000000000000..e8989490fe2c --- /dev/null +++ b/libs/androidfw/FileStream.cpp @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2017 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. + */ + +#include "androidfw/FileStream.h" + +#include <errno.h> // for errno +#include <fcntl.h> // for O_RDONLY +#include <unistd.h> // for read + +#include "android-base/errors.h" +#include "android-base/file.h" // for O_BINARY +#include "android-base/logging.h" +#include "android-base/macros.h" +#include "android-base/utf8.h" + +#if defined(_WIN32) +// This is only needed for O_CLOEXEC. +#include <windows.h> +#define O_CLOEXEC O_NOINHERIT +#endif + +using ::android::base::SystemErrorCodeToString; +using ::android::base::unique_fd; + +namespace android { + +FileInputStream::FileInputStream(const std::string& path, size_t buffer_capacity) + : should_close_(true), buffer_capacity_(buffer_capacity) { + int mode = O_RDONLY | O_CLOEXEC | O_BINARY; + fd_ = TEMP_FAILURE_RETRY(::android::base::utf8::open(path.c_str(), mode)); + if (fd_ == -1) { + error_ = SystemErrorCodeToString(errno); + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +FileInputStream::FileInputStream(int fd, size_t buffer_capacity) + : fd_(fd), should_close_(true), buffer_capacity_(buffer_capacity) { + if (fd_ < 0) { + error_ = "Bad File Descriptor"; + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +FileInputStream::FileInputStream(android::base::borrowed_fd fd, size_t buffer_capacity) + : fd_(fd.get()), should_close_(false), buffer_capacity_(buffer_capacity) { + + if (fd_ < 0) { + error_ = "Bad File Descriptor"; + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + + +bool FileInputStream::Next(const void** data, size_t* size) { + if (HadError()) { + return false; + } + + // Deal with any remaining bytes after BackUp was called. + if (buffer_offset_ != buffer_size_) { + *data = buffer_.get() + buffer_offset_; + *size = buffer_size_ - buffer_offset_; + total_byte_count_ += buffer_size_ - buffer_offset_; + buffer_offset_ = buffer_size_; + return true; + } + + ssize_t n = TEMP_FAILURE_RETRY(read(fd_, buffer_.get(), buffer_capacity_)); + if (n < 0) { + error_ = SystemErrorCodeToString(errno); + if (fd_ != -1) { + if (should_close_) { + close(fd_); + } + fd_ = -1; + } + buffer_.reset(); + return false; + } + + buffer_size_ = static_cast<size_t>(n); + buffer_offset_ = buffer_size_; + total_byte_count_ += buffer_size_; + + *data = buffer_.get(); + *size = buffer_size_; + return buffer_size_ != 0u; +} + +void FileInputStream::BackUp(size_t count) { + if (count > buffer_offset_) { + count = buffer_offset_; + } + buffer_offset_ -= count; + total_byte_count_ -= count; +} + +size_t FileInputStream::ByteCount() const { + return total_byte_count_; +} + +bool FileInputStream::HadError() const { + return fd_ == -1; +} + +std::string FileInputStream::GetError() const { + return error_; +} + +bool FileInputStream::ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) { + return base::ReadFullyAtOffset(fd_, data, byte_count, offset); +} + +FileOutputStream::FileOutputStream(const std::string& path, size_t buffer_capacity) + : buffer_capacity_(buffer_capacity) { + int mode = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_BINARY; + owned_fd_.reset(TEMP_FAILURE_RETRY(::android::base::utf8::open(path.c_str(), mode, 0666))); + fd_ = owned_fd_.get(); + if (fd_ < 0) { + error_ = SystemErrorCodeToString(errno); + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +FileOutputStream::FileOutputStream(unique_fd fd, size_t buffer_capacity) + : FileOutputStream(fd.get(), buffer_capacity) { + owned_fd_ = std::move(fd); +} + +FileOutputStream::FileOutputStream(int fd, size_t buffer_capacity) + : fd_(fd), buffer_capacity_(buffer_capacity) { + if (fd_ < 0) { + error_ = "Bad File Descriptor"; + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +FileOutputStream::~FileOutputStream() { + // Flush the buffer. + Flush(); +} + +bool FileOutputStream::Next(void** data, size_t* size) { + if (HadError()) { + return false; + } + + if (buffer_offset_ == buffer_capacity_) { + if (!FlushImpl()) { + return false; + } + } + + const size_t buffer_size = buffer_capacity_ - buffer_offset_; + *data = buffer_.get() + buffer_offset_; + *size = buffer_size; + total_byte_count_ += buffer_size; + buffer_offset_ = buffer_capacity_; + return true; +} + +void FileOutputStream::BackUp(size_t count) { + if (count > buffer_offset_) { + count = buffer_offset_; + } + buffer_offset_ -= count; + total_byte_count_ -= count; +} + +size_t FileOutputStream::ByteCount() const { + return total_byte_count_; +} + +bool FileOutputStream::Flush() { + if (!HadError()) { + return FlushImpl(); + } + return false; +} + +bool FileOutputStream::FlushImpl() { + ssize_t n = TEMP_FAILURE_RETRY(write(fd_, buffer_.get(), buffer_offset_)); + if (n < 0) { + error_ = SystemErrorCodeToString(errno); + owned_fd_.reset(); + fd_ = -1; + buffer_.reset(); + return false; + } + + buffer_offset_ = 0u; + return true; +} + +bool FileOutputStream::HadError() const { + return fd_ == -1; +} + +std::string FileOutputStream::GetError() const { + return error_; +} + +} // namespace android diff --git a/libs/androidfw/NinePatch.cpp b/libs/androidfw/NinePatch.cpp new file mode 100644 index 000000000000..1fdbebfb6daa --- /dev/null +++ b/libs/androidfw/NinePatch.cpp @@ -0,0 +1,682 @@ +/* + * Copyright (C) 2016 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. + */ + +#include <sstream> +#include <string> +#include <vector> + +#include "androidfw/Image.h" +#include "androidfw/ResourceTypes.h" +#include "androidfw/StringPiece.h" + +using android::StringPiece; + +namespace android { + +// Colors in the format 0xAARRGGBB (the way 9-patch expects it). +constexpr static const uint32_t kColorOpaqueWhite = 0xffffffffu; +constexpr static const uint32_t kColorOpaqueBlack = 0xff000000u; +constexpr static const uint32_t kColorOpaqueRed = 0xffff0000u; + +constexpr static const uint32_t kPrimaryColor = kColorOpaqueBlack; +constexpr static const uint32_t kSecondaryColor = kColorOpaqueRed; + +/** + * Returns the alpha value encoded in the 0xAARRGBB encoded pixel. + */ +static uint32_t get_alpha(uint32_t color); + +/** + * Determines whether a color on an ImageLine is valid. + * A 9patch image may use a transparent color as neutral, + * or a fully opaque white color as neutral, based on the + * pixel color at (0,0) of the image. One or the other is fine, + * but we need to ensure consistency throughout the image. + */ +class ColorValidator { + public: + virtual ~ColorValidator() = default; + + /** + * Returns true if the color specified is a neutral color + * (no padding, stretching, or optical bounds). + */ + virtual bool IsNeutralColor(uint32_t color) const = 0; + + /** + * Returns true if the color is either a neutral color + * or one denoting padding, stretching, or optical bounds. + */ + bool IsValidColor(uint32_t color) const { + switch (color) { + case kPrimaryColor: + case kSecondaryColor: + return true; + } + return IsNeutralColor(color); + } +}; + +// Walks an ImageLine and records Ranges of primary and secondary colors. +// The primary color is black and is used to denote a padding or stretching +// range, +// depending on which border we're iterating over. +// The secondary color is red and is used to denote optical bounds. +// +// An ImageLine is a templated-interface that would look something like this if +// it +// were polymorphic: +// +// class ImageLine { +// public: +// virtual int32_t GetLength() const = 0; +// virtual uint32_t GetColor(int32_t idx) const = 0; +// }; +// +template <typename ImageLine> +static bool FillRanges(const ImageLine* image_line, const ColorValidator* color_validator, + std::vector<Range>* primary_ranges, std::vector<Range>* secondary_ranges, + std::string* out_err) { + const int32_t length = image_line->GetLength(); + + uint32_t last_color = 0xffffffffu; + for (int32_t idx = 1; idx < length - 1; idx++) { + const uint32_t color = image_line->GetColor(idx); + if (!color_validator->IsValidColor(color)) { + *out_err = "found an invalid color"; + return false; + } + + if (color != last_color) { + // We are ending a range. Which range? + // note: encode the x offset without the final 1 pixel border. + if (last_color == kPrimaryColor) { + primary_ranges->back().end = idx - 1; + } else if (last_color == kSecondaryColor) { + secondary_ranges->back().end = idx - 1; + } + + // We are starting a range. Which range? + // note: encode the x offset without the final 1 pixel border. + if (color == kPrimaryColor) { + primary_ranges->push_back(Range(idx - 1, length - 2)); + } else if (color == kSecondaryColor) { + secondary_ranges->push_back(Range(idx - 1, length - 2)); + } + last_color = color; + } + } + return true; +} + +/** + * Iterates over a row in an image. Implements the templated ImageLine + * interface. + */ +class HorizontalImageLine { + public: + explicit HorizontalImageLine(uint8_t** rows, int32_t xoffset, int32_t yoffset, int32_t length) + : rows_(rows), xoffset_(xoffset), yoffset_(yoffset), length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_] + (idx + xoffset_) * 4); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, length_; + + DISALLOW_COPY_AND_ASSIGN(HorizontalImageLine); +}; + +/** + * Iterates over a column in an image. Implements the templated ImageLine + * interface. + */ +class VerticalImageLine { + public: + explicit VerticalImageLine(uint8_t** rows, int32_t xoffset, int32_t yoffset, int32_t length) + : rows_(rows), xoffset_(xoffset), yoffset_(yoffset), length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_ + idx] + (xoffset_ * 4)); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, length_; + + DISALLOW_COPY_AND_ASSIGN(VerticalImageLine); +}; + +class DiagonalImageLine { + public: + explicit DiagonalImageLine(uint8_t** rows, int32_t xoffset, int32_t yoffset, int32_t xstep, + int32_t ystep, int32_t length) + : rows_(rows), + xoffset_(xoffset), + yoffset_(yoffset), + xstep_(xstep), + ystep_(ystep), + length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_ + (idx * ystep_)] + ((idx + xoffset_) * xstep_) * 4); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, xstep_, ystep_, length_; + + DISALLOW_COPY_AND_ASSIGN(DiagonalImageLine); +}; + +class TransparentNeutralColorValidator : public ColorValidator { + public: + bool IsNeutralColor(uint32_t color) const override { + return get_alpha(color) == 0; + } +}; + +class WhiteNeutralColorValidator : public ColorValidator { + public: + bool IsNeutralColor(uint32_t color) const override { + return color == kColorOpaqueWhite; + } +}; + +inline static uint32_t get_alpha(uint32_t color) { + return (color & 0xff000000u) >> 24; +} + +static bool PopulateBounds(const std::vector<Range>& padding, + const std::vector<Range>& layout_bounds, + const std::vector<Range>& stretch_regions, const int32_t length, + int32_t* padding_start, int32_t* padding_end, int32_t* layout_start, + int32_t* layout_end, StringPiece edge_name, std::string* out_err) { + if (padding.size() > 1) { + std::stringstream err_stream; + err_stream << "too many padding sections on " << edge_name << " border"; + *out_err = err_stream.str(); + return false; + } + + *padding_start = 0; + *padding_end = 0; + if (!padding.empty()) { + const Range& range = padding.front(); + *padding_start = range.start; + *padding_end = length - range.end; + } else if (!stretch_regions.empty()) { + // No padding was defined. Compute the padding from the first and last + // stretch regions. + *padding_start = stretch_regions.front().start; + *padding_end = length - stretch_regions.back().end; + } + + if (layout_bounds.size() > 2) { + std::stringstream err_stream; + err_stream << "too many layout bounds sections on " << edge_name << " border"; + *out_err = err_stream.str(); + return false; + } + + *layout_start = 0; + *layout_end = 0; + if (layout_bounds.size() >= 1) { + const Range& range = layout_bounds.front(); + // If there is only one layout bound segment, it might not start at 0, but + // then it should + // end at length. + if (range.start != 0 && range.end != length) { + std::stringstream err_stream; + err_stream << "layout bounds on " << edge_name << " border must start at edge"; + *out_err = err_stream.str(); + return false; + } + *layout_start = range.end; + + if (layout_bounds.size() >= 2) { + const Range& range = layout_bounds.back(); + if (range.end != length) { + std::stringstream err_stream; + err_stream << "layout bounds on " << edge_name << " border must start at edge"; + *out_err = err_stream.str(); + return false; + } + *layout_end = length - range.start; + } + } + return true; +} + +static int32_t CalculateSegmentCount(const std::vector<Range>& stretch_regions, int32_t length) { + if (stretch_regions.size() == 0) { + return 0; + } + + const bool start_is_fixed = stretch_regions.front().start != 0; + const bool end_is_fixed = stretch_regions.back().end != length; + int32_t modifier = 0; + if (start_is_fixed && end_is_fixed) { + modifier = 1; + } else if (!start_is_fixed && !end_is_fixed) { + modifier = -1; + } + return static_cast<int32_t>(stretch_regions.size()) * 2 + modifier; +} + +static uint32_t GetRegionColor(uint8_t** rows, const Bounds& region) { + // Sample the first pixel to compare against. + const uint32_t expected_color = NinePatch::PackRGBA(rows[region.top] + region.left * 4); + for (int32_t y = region.top; y < region.bottom; y++) { + const uint8_t* row = rows[y]; + for (int32_t x = region.left; x < region.right; x++) { + const uint32_t color = NinePatch::PackRGBA(row + x * 4); + if (get_alpha(color) == 0) { + // The color is transparent. + // If the expectedColor is not transparent, NO_COLOR. + if (get_alpha(expected_color) != 0) { + return android::Res_png_9patch::NO_COLOR; + } + } else if (color != expected_color) { + return android::Res_png_9patch::NO_COLOR; + } + } + } + + if (get_alpha(expected_color) == 0) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + return expected_color; +} + +// Fills out_colors with each 9-patch section's color. If the whole section is +// transparent, +// it gets the special TRANSPARENT color. If the whole section is the same +// color, it is assigned +// that color. Otherwise it gets the special NO_COLOR color. +// +// Note that the rows contain the 9-patch 1px border, and the indices in the +// stretch regions are +// already offset to exclude the border. This means that each time the rows are +// accessed, +// the indices must be offset by 1. +// +// width and height also include the 9-patch 1px border. +static void CalculateRegionColors(uint8_t** rows, + const std::vector<Range>& horizontal_stretch_regions, + const std::vector<Range>& vertical_stretch_regions, + const int32_t width, const int32_t height, + std::vector<uint32_t>* out_colors) { + int32_t next_top = 0; + Bounds bounds; + auto row_iter = vertical_stretch_regions.begin(); + while (next_top != height) { + if (row_iter != vertical_stretch_regions.end()) { + if (next_top != row_iter->start) { + // This is a fixed segment. + // Offset the bounds by 1 to accommodate the border. + bounds.top = next_top + 1; + bounds.bottom = row_iter->start + 1; + next_top = row_iter->start; + } else { + // This is a stretchy segment. + // Offset the bounds by 1 to accommodate the border. + bounds.top = row_iter->start + 1; + bounds.bottom = row_iter->end + 1; + next_top = row_iter->end; + ++row_iter; + } + } else { + // This is the end, fixed section. + // Offset the bounds by 1 to accommodate the border. + bounds.top = next_top + 1; + bounds.bottom = height + 1; + next_top = height; + } + + int32_t next_left = 0; + auto col_iter = horizontal_stretch_regions.begin(); + while (next_left != width) { + if (col_iter != horizontal_stretch_regions.end()) { + if (next_left != col_iter->start) { + // This is a fixed segment. + // Offset the bounds by 1 to accommodate the border. + bounds.left = next_left + 1; + bounds.right = col_iter->start + 1; + next_left = col_iter->start; + } else { + // This is a stretchy segment. + // Offset the bounds by 1 to accommodate the border. + bounds.left = col_iter->start + 1; + bounds.right = col_iter->end + 1; + next_left = col_iter->end; + ++col_iter; + } + } else { + // This is the end, fixed section. + // Offset the bounds by 1 to accommodate the border. + bounds.left = next_left + 1; + bounds.right = width + 1; + next_left = width; + } + out_colors->push_back(GetRegionColor(rows, bounds)); + } + } +} + +// Calculates the insets of a row/column of pixels based on where the largest +// alpha value begins +// (on both sides). +template <typename ImageLine> +static void FindOutlineInsets(const ImageLine* image_line, int32_t* out_start, int32_t* out_end) { + *out_start = 0; + *out_end = 0; + + const int32_t length = image_line->GetLength(); + if (length < 3) { + return; + } + + // If the length is odd, we want both sides to process the center pixel, + // so we use two different midpoints (to account for < and <= in the different + // loops). + const int32_t mid2 = length / 2; + const int32_t mid1 = mid2 + (length % 2); + + uint32_t max_alpha = 0; + for (int32_t i = 0; i < mid1 && max_alpha != 0xff; i++) { + uint32_t alpha = get_alpha(image_line->GetColor(i)); + if (alpha > max_alpha) { + max_alpha = alpha; + *out_start = i; + } + } + + max_alpha = 0; + for (int32_t i = length - 1; i >= mid2 && max_alpha != 0xff; i--) { + uint32_t alpha = get_alpha(image_line->GetColor(i)); + if (alpha > max_alpha) { + max_alpha = alpha; + *out_end = length - (i + 1); + } + } + return; +} + +template <typename ImageLine> +static uint32_t FindMaxAlpha(const ImageLine* image_line) { + const int32_t length = image_line->GetLength(); + uint32_t max_alpha = 0; + for (int32_t idx = 0; idx < length && max_alpha != 0xff; idx++) { + uint32_t alpha = get_alpha(image_line->GetColor(idx)); + if (alpha > max_alpha) { + max_alpha = alpha; + } + } + return max_alpha; +} + +// Pack the pixels in as 0xAARRGGBB (as 9-patch expects it). +uint32_t NinePatch::PackRGBA(const uint8_t* pixel) { + return (pixel[3] << 24) | (pixel[0] << 16) | (pixel[1] << 8) | pixel[2]; +} + +std::unique_ptr<NinePatch> NinePatch::Create(uint8_t** rows, const int32_t width, + const int32_t height, std::string* out_err) { + if (width < 3 || height < 3) { + *out_err = "image must be at least 3x3 (1x1 image with 1 pixel border)"; + return {}; + } + + std::vector<Range> horizontal_padding; + std::vector<Range> horizontal_layout_bounds; + std::vector<Range> vertical_padding; + std::vector<Range> vertical_layout_bounds; + std::vector<Range> unexpected_ranges; + std::unique_ptr<ColorValidator> color_validator; + + if (rows[0][3] == 0) { + color_validator = std::make_unique<TransparentNeutralColorValidator>(); + } else if (PackRGBA(rows[0]) == kColorOpaqueWhite) { + color_validator = std::make_unique<WhiteNeutralColorValidator>(); + } else { + *out_err = "top-left corner pixel must be either opaque white or transparent"; + return {}; + } + + // Private constructor, can't use make_unique. + auto nine_patch = std::unique_ptr<NinePatch>(new NinePatch()); + + HorizontalImageLine top_row(rows, 0, 0, width); + if (!FillRanges(&top_row, color_validator.get(), &nine_patch->horizontal_stretch_regions, + &unexpected_ranges, out_err)) { + return {}; + } + + if (!unexpected_ranges.empty()) { + const Range& range = unexpected_ranges[0]; + std::stringstream err_stream; + err_stream << "found unexpected optical bounds (red pixel) on top border " + << "at x=" << range.start + 1; + *out_err = err_stream.str(); + return {}; + } + + VerticalImageLine left_col(rows, 0, 0, height); + if (!FillRanges(&left_col, color_validator.get(), &nine_patch->vertical_stretch_regions, + &unexpected_ranges, out_err)) { + return {}; + } + + if (!unexpected_ranges.empty()) { + const Range& range = unexpected_ranges[0]; + std::stringstream err_stream; + err_stream << "found unexpected optical bounds (red pixel) on left border " + << "at y=" << range.start + 1; + return {}; + } + + HorizontalImageLine bottom_row(rows, 0, height - 1, width); + if (!FillRanges(&bottom_row, color_validator.get(), &horizontal_padding, + &horizontal_layout_bounds, out_err)) { + return {}; + } + + if (!PopulateBounds(horizontal_padding, horizontal_layout_bounds, + nine_patch->horizontal_stretch_regions, width - 2, &nine_patch->padding.left, + &nine_patch->padding.right, &nine_patch->layout_bounds.left, + &nine_patch->layout_bounds.right, "bottom", out_err)) { + return {}; + } + + VerticalImageLine right_col(rows, width - 1, 0, height); + if (!FillRanges(&right_col, color_validator.get(), &vertical_padding, &vertical_layout_bounds, + out_err)) { + return {}; + } + + if (!PopulateBounds(vertical_padding, vertical_layout_bounds, + nine_patch->vertical_stretch_regions, height - 2, &nine_patch->padding.top, + &nine_patch->padding.bottom, &nine_patch->layout_bounds.top, + &nine_patch->layout_bounds.bottom, "right", out_err)) { + return {}; + } + + // Fill the region colors of the 9-patch. + const int32_t num_rows = CalculateSegmentCount(nine_patch->horizontal_stretch_regions, width - 2); + const int32_t num_cols = CalculateSegmentCount(nine_patch->vertical_stretch_regions, height - 2); + if ((int64_t)num_rows * (int64_t)num_cols > 0x7f) { + *out_err = "too many regions in 9-patch"; + return {}; + } + + nine_patch->region_colors.reserve(num_rows * num_cols); + CalculateRegionColors(rows, nine_patch->horizontal_stretch_regions, + nine_patch->vertical_stretch_regions, width - 2, height - 2, + &nine_patch->region_colors); + + // Compute the outline based on opacity. + + // Find left and right extent of 9-patch content on center row. + HorizontalImageLine mid_row(rows, 1, height / 2, width - 2); + FindOutlineInsets(&mid_row, &nine_patch->outline.left, &nine_patch->outline.right); + + // Find top and bottom extent of 9-patch content on center column. + VerticalImageLine mid_col(rows, width / 2, 1, height - 2); + FindOutlineInsets(&mid_col, &nine_patch->outline.top, &nine_patch->outline.bottom); + + const int32_t outline_width = (width - 2) - nine_patch->outline.left - nine_patch->outline.right; + const int32_t outline_height = + (height - 2) - nine_patch->outline.top - nine_patch->outline.bottom; + + // Find the largest alpha value within the outline area. + HorizontalImageLine outline_mid_row(rows, 1 + nine_patch->outline.left, + 1 + nine_patch->outline.top + (outline_height / 2), + outline_width); + VerticalImageLine outline_mid_col(rows, 1 + nine_patch->outline.left + (outline_width / 2), + 1 + nine_patch->outline.top, outline_height); + nine_patch->outline_alpha = + std::max(FindMaxAlpha(&outline_mid_row), FindMaxAlpha(&outline_mid_col)); + + // Assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center. + DiagonalImageLine diagonal(rows, 1 + nine_patch->outline.left, 1 + nine_patch->outline.top, 1, 1, + std::min(outline_width, outline_height)); + int32_t top_left, bottom_right; + FindOutlineInsets(&diagonal, &top_left, &bottom_right); + + /* Determine source radius based upon inset: + * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r + * sqrt(2) * r = sqrt(2) * i + r + * (sqrt(2) - 1) * r = sqrt(2) * i + * r = sqrt(2) / (sqrt(2) - 1) * i + */ + nine_patch->outline_radius = 3.4142f * top_left; + return nine_patch; +} + +std::unique_ptr<uint8_t[]> NinePatch::SerializeBase(size_t* outLen) const { + android::Res_png_9patch data; + data.numXDivs = static_cast<uint8_t>(horizontal_stretch_regions.size()) * 2; + data.numYDivs = static_cast<uint8_t>(vertical_stretch_regions.size()) * 2; + data.numColors = static_cast<uint8_t>(region_colors.size()); + data.paddingLeft = padding.left; + data.paddingRight = padding.right; + data.paddingTop = padding.top; + data.paddingBottom = padding.bottom; + + auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[data.serializedSize()]); + android::Res_png_9patch::serialize(data, (const int32_t*)horizontal_stretch_regions.data(), + (const int32_t*)vertical_stretch_regions.data(), + region_colors.data(), buffer.get()); + // Convert to file endianness. + reinterpret_cast<android::Res_png_9patch*>(buffer.get())->deviceToFile(); + + *outLen = data.serializedSize(); + return buffer; +} + +std::unique_ptr<uint8_t[]> NinePatch::SerializeLayoutBounds(size_t* out_len) const { + size_t chunk_len = sizeof(uint32_t) * 4; + auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[chunk_len]); + uint8_t* cursor = buffer.get(); + + memcpy(cursor, &layout_bounds.left, sizeof(layout_bounds.left)); + cursor += sizeof(layout_bounds.left); + + memcpy(cursor, &layout_bounds.top, sizeof(layout_bounds.top)); + cursor += sizeof(layout_bounds.top); + + memcpy(cursor, &layout_bounds.right, sizeof(layout_bounds.right)); + cursor += sizeof(layout_bounds.right); + + memcpy(cursor, &layout_bounds.bottom, sizeof(layout_bounds.bottom)); + cursor += sizeof(layout_bounds.bottom); + + *out_len = chunk_len; + return buffer; +} + +std::unique_ptr<uint8_t[]> NinePatch::SerializeRoundedRectOutline(size_t* out_len) const { + size_t chunk_len = sizeof(uint32_t) * 6; + auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[chunk_len]); + uint8_t* cursor = buffer.get(); + + memcpy(cursor, &outline.left, sizeof(outline.left)); + cursor += sizeof(outline.left); + + memcpy(cursor, &outline.top, sizeof(outline.top)); + cursor += sizeof(outline.top); + + memcpy(cursor, &outline.right, sizeof(outline.right)); + cursor += sizeof(outline.right); + + memcpy(cursor, &outline.bottom, sizeof(outline.bottom)); + cursor += sizeof(outline.bottom); + + *((float*)cursor) = outline_radius; + cursor += sizeof(outline_radius); + + *((uint32_t*)cursor) = outline_alpha; + + *out_len = chunk_len; + return buffer; +} + +::std::ostream& operator<<(::std::ostream& out, const Range& range) { + return out << "[" << range.start << ", " << range.end << ")"; +} + +::std::ostream& operator<<(::std::ostream& out, const Bounds& bounds) { + return out << "l=" << bounds.left << " t=" << bounds.top << " r=" << bounds.right + << " b=" << bounds.bottom; +} + +template <typename T> +std::ostream& operator<<(std::ostream& os, const std::vector<T>& v) { + for (int i = 0; i < v.size(); ++i) { + os << v[i]; + if (i != v.size() - 1) os << " "; + } + return os; +} + +::std::ostream& operator<<(::std::ostream& out, const NinePatch& nine_patch) { + return out << "horizontalStretch:" << nine_patch.horizontal_stretch_regions + << " verticalStretch:" << nine_patch.vertical_stretch_regions + << " padding: " << nine_patch.padding << ", bounds: " << nine_patch.layout_bounds + << ", outline: " << nine_patch.outline << " rad=" << nine_patch.outline_radius + << " alpha=" << nine_patch.outline_alpha; +} + +} // namespace android diff --git a/libs/androidfw/Png.cpp b/libs/androidfw/Png.cpp new file mode 100644 index 000000000000..fb45cd9b49d0 --- /dev/null +++ b/libs/androidfw/Png.cpp @@ -0,0 +1,1259 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "androidfw/Png.h" + +#include <png.h> +#include <zlib.h> + +#include <iostream> +#include <sstream> +#include <string> +#include <vector> + +#include "android-base/strings.h" +#include "androidfw/BigBuffer.h" +#include "androidfw/ResourceTypes.h" +#include "androidfw/Source.h" + +namespace android { + +constexpr bool kDebug = false; + +struct PngInfo { + ~PngInfo() { + for (png_bytep row : rows) { + if (row != nullptr) { + delete[] row; + } + } + + delete[] xDivs; + delete[] yDivs; + } + + void* serialize9Patch() { + void* serialized = Res_png_9patch::serialize(info9Patch, xDivs, yDivs, colors.data()); + reinterpret_cast<Res_png_9patch*>(serialized)->deviceToFile(); + return serialized; + } + + uint32_t width = 0; + uint32_t height = 0; + std::vector<png_bytep> rows; + + bool is9Patch = false; + Res_png_9patch info9Patch; + int32_t* xDivs = nullptr; + int32_t* yDivs = nullptr; + std::vector<uint32_t> colors; + + // Layout padding. + bool haveLayoutBounds = false; + int32_t layoutBoundsLeft; + int32_t layoutBoundsTop; + int32_t layoutBoundsRight; + int32_t layoutBoundsBottom; + + // Round rect outline description. + int32_t outlineInsetsLeft; + int32_t outlineInsetsTop; + int32_t outlineInsetsRight; + int32_t outlineInsetsBottom; + float outlineRadius; + uint8_t outlineAlpha; +}; + +static void readDataFromStream(png_structp readPtr, png_bytep data, png_size_t length) { + std::istream* input = reinterpret_cast<std::istream*>(png_get_io_ptr(readPtr)); + if (!input->read(reinterpret_cast<char*>(data), length)) { + png_error(readPtr, strerror(errno)); + } +} + +static void writeDataToStream(png_structp writePtr, png_bytep data, png_size_t length) { + BigBuffer* outBuffer = reinterpret_cast<BigBuffer*>(png_get_io_ptr(writePtr)); + png_bytep buf = outBuffer->NextBlock<png_byte>(length); + memcpy(buf, data, length); +} + +static void flushDataToStream(png_structp /*writePtr*/) { +} + +static void logWarning(png_structp readPtr, png_const_charp warningMessage) { + IDiagnostics* diag = reinterpret_cast<IDiagnostics*>(png_get_error_ptr(readPtr)); + diag->Warn(DiagMessage() << warningMessage); +} + +static bool readPng(IDiagnostics* diag, png_structp readPtr, png_infop infoPtr, PngInfo* outInfo) { + if (setjmp(png_jmpbuf(readPtr))) { + diag->Error(DiagMessage() << "failed reading png"); + return false; + } + + png_set_sig_bytes(readPtr, kPngSignatureSize); + png_read_info(readPtr, infoPtr); + + int colorType, bitDepth, interlaceType, compressionType; + png_get_IHDR(readPtr, infoPtr, &outInfo->width, &outInfo->height, &bitDepth, &colorType, + &interlaceType, &compressionType, nullptr); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(readPtr); + } + + if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) { + png_set_expand_gray_1_2_4_to_8(readPtr); + } + + if (png_get_valid(readPtr, infoPtr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(readPtr); + } + + if (bitDepth == 16) { + png_set_strip_16(readPtr); + } + + if (!(colorType & PNG_COLOR_MASK_ALPHA)) { + png_set_add_alpha(readPtr, 0xFF, PNG_FILLER_AFTER); + } + + if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(readPtr); + } + + png_set_interlace_handling(readPtr); + png_read_update_info(readPtr, infoPtr); + + const uint32_t rowBytes = png_get_rowbytes(readPtr, infoPtr); + outInfo->rows.resize(outInfo->height); + for (size_t i = 0; i < outInfo->height; i++) { + outInfo->rows[i] = new png_byte[rowBytes]; + } + + png_read_image(readPtr, outInfo->rows.data()); + png_read_end(readPtr, infoPtr); + return true; +} + +static void checkNinePatchSerialization(Res_png_9patch* inPatch, void* data) { + size_t patchSize = inPatch->serializedSize(); + void* newData = malloc(patchSize); + memcpy(newData, data, patchSize); + Res_png_9patch* outPatch = inPatch->deserialize(newData); + outPatch->fileToDevice(); + // deserialization is done in place, so outPatch == newData + assert(outPatch == newData); + assert(outPatch->numXDivs == inPatch->numXDivs); + assert(outPatch->numYDivs == inPatch->numYDivs); + assert(outPatch->paddingLeft == inPatch->paddingLeft); + assert(outPatch->paddingRight == inPatch->paddingRight); + assert(outPatch->paddingTop == inPatch->paddingTop); + assert(outPatch->paddingBottom == inPatch->paddingBottom); + /* for (int i = 0; i < outPatch->numXDivs; i++) { + assert(outPatch->getXDivs()[i] == inPatch->getXDivs()[i]); + } + for (int i = 0; i < outPatch->numYDivs; i++) { + assert(outPatch->getYDivs()[i] == inPatch->getYDivs()[i]); + } + for (int i = 0; i < outPatch->numColors; i++) { + assert(outPatch->getColors()[i] == inPatch->getColors()[i]); + }*/ + free(newData); +} + +/*static void dump_image(int w, int h, const png_byte* const* rows, int +color_type) { + int i, j, rr, gg, bb, aa; + + int bpp; + if (color_type == PNG_COLOR_TYPE_PALETTE || color_type == +PNG_COLOR_TYPE_GRAY) { + bpp = 1; + } else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + bpp = 2; + } else if (color_type == PNG_COLOR_TYPE_RGB || color_type == +PNG_COLOR_TYPE_RGB_ALPHA) { + // We use a padding byte even when there is no alpha + bpp = 4; + } else { + printf("Unknown color type %d.\n", color_type); + } + + for (j = 0; j < h; j++) { + const png_byte* row = rows[j]; + for (i = 0; i < w; i++) { + rr = row[0]; + gg = row[1]; + bb = row[2]; + aa = row[3]; + row += bpp; + + if (i == 0) { + printf("Row %d:", j); + } + switch (bpp) { + case 1: + printf(" (%d)", rr); + break; + case 2: + printf(" (%d %d", rr, gg); + break; + case 3: + printf(" (%d %d %d)", rr, gg, bb); + break; + case 4: + printf(" (%d %d %d %d)", rr, gg, bb, aa); + break; + } + if (i == (w - 1)) { + printf("\n"); + } + } + } +}*/ + +#ifdef MAX +#undef MAX +#endif +#ifdef ABS +#undef ABS +#endif + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define ABS(a) ((a) < 0 ? -(a) : (a)) + +static void analyze_image(IDiagnostics* diag, const PngInfo& imageInfo, int grayscaleTolerance, + png_colorp rgbPalette, png_bytep alphaPalette, int* paletteEntries, + bool* hasTransparency, int* colorType, png_bytepp outRows) { + int w = imageInfo.width; + int h = imageInfo.height; + int i, j, rr, gg, bb, aa, idx; + uint32_t colors[256], col; + int num_colors = 0; + int maxGrayDeviation = 0; + + bool isOpaque = true; + bool isPalette = true; + bool isGrayscale = true; + + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors + + if (kDebug) { + printf("Initial image data:\n"); + // dump_image(w, h, imageInfo.rows.data(), PNG_COLOR_TYPE_RGB_ALPHA); + } + + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + int odev = maxGrayDeviation; + maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation); + if (maxGrayDeviation > odev) { + if (kDebug) { + printf("New max dev. = %d at pixel (%d, %d) = (%d %d %d %d)\n", maxGrayDeviation, i, j, + rr, gg, bb, aa); + } + } + + // Check if image is really grayscale + if (isGrayscale) { + if (rr != gg || rr != bb) { + if (kDebug) { + printf("Found a non-gray pixel at %d, %d = (%d %d %d %d)\n", i, j, rr, gg, bb, aa); + } + isGrayscale = false; + } + } + + // Check if image is really opaque + if (isOpaque) { + if (aa != 0xff) { + if (kDebug) { + printf("Found a non-opaque pixel at %d, %d = (%d %d %d %d)\n", i, j, rr, gg, bb, aa); + } + isOpaque = false; + } + } + + // Check if image is really <= 256 colors + if (isPalette) { + col = (uint32_t)((rr << 24) | (gg << 16) | (bb << 8) | aa); + bool match = false; + for (idx = 0; idx < num_colors; idx++) { + if (colors[idx] == col) { + match = true; + break; + } + } + + // Write the palette index for the pixel to outRows optimistically + // We might overwrite it later if we decide to encode as gray or + // gray + alpha + *out++ = idx; + if (!match) { + if (num_colors == 256) { + if (kDebug) { + printf("Found 257th color at %d, %d\n", i, j); + } + isPalette = false; + } else { + colors[num_colors++] = col; + } + } + } + } + } + + *paletteEntries = 0; + *hasTransparency = !isOpaque; + int bpp = isOpaque ? 3 : 4; + int paletteSize = w * h + bpp * num_colors; + + if (kDebug) { + printf("isGrayscale = %s\n", isGrayscale ? "true" : "false"); + printf("isOpaque = %s\n", isOpaque ? "true" : "false"); + printf("isPalette = %s\n", isPalette ? "true" : "false"); + printf("Size w/ palette = %d, gray+alpha = %d, rgb(a) = %d\n", paletteSize, 2 * w * h, + bpp * w * h); + printf("Max gray deviation = %d, tolerance = %d\n", maxGrayDeviation, grayscaleTolerance); + } + + // Choose the best color type for the image. + // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel + // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct + // combinations + // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA + // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is + // sufficiently + // small, otherwise use COLOR_TYPE_RGB{_ALPHA} + if (isGrayscale) { + if (isOpaque) { + *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel + } else { + // Use a simple heuristic to determine whether using a palette will + // save space versus using gray + alpha for each pixel. + // This doesn't take into account chunk overhead, filtering, LZ + // compression, etc. + if (isPalette && (paletteSize < 2 * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color + } else { + *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel + } + } + } else if (isPalette && (paletteSize < bpp * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; + } else { + if (maxGrayDeviation <= grayscaleTolerance) { + diag->Note(DiagMessage() << "forcing image to gray (max deviation = " << maxGrayDeviation + << ")"); + *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; + } else { + *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + } + } + + // Perform postprocessing of the image or palette data based on the final + // color type chosen + + if (*colorType == PNG_COLOR_TYPE_PALETTE) { + // Create separate RGB and Alpha palettes and set the number of colors + *paletteEntries = num_colors; + + // Create the RGB and alpha palettes + for (int idx = 0; idx < num_colors; idx++) { + col = colors[idx]; + rgbPalette[idx].red = (png_byte)((col >> 24) & 0xff); + rgbPalette[idx].green = (png_byte)((col >> 16) & 0xff); + rgbPalette[idx].blue = (png_byte)((col >> 8) & 0xff); + alphaPalette[idx] = (png_byte)(col & 0xff); + } + } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + // If the image is gray or gray + alpha, compact the pixels into outRows + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + if (isGrayscale) { + *out++ = rr; + } else { + *out++ = (png_byte)(rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + if (!isOpaque) { + *out++ = aa; + } + } + } + } +} + +static bool writePng(IDiagnostics* diag, png_structp writePtr, png_infop infoPtr, PngInfo* info, + int grayScaleTolerance) { + if (setjmp(png_jmpbuf(writePtr))) { + diag->Error(DiagMessage() << "failed to write png"); + return false; + } + + uint32_t width, height; + int colorType, bitDepth, interlaceType, compressionType; + + png_unknown_chunk unknowns[3]; + unknowns[0].data = nullptr; + unknowns[1].data = nullptr; + unknowns[2].data = nullptr; + + png_bytepp outRows = (png_bytepp)malloc((int)info->height * sizeof(png_bytep)); + if (outRows == (png_bytepp)0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + for (uint32_t i = 0; i < info->height; i++) { + outRows[i] = (png_bytep)malloc(2 * (int)info->width); + if (outRows[i] == (png_bytep)0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + } + + png_set_compression_level(writePtr, Z_BEST_COMPRESSION); + + if (kDebug) { + diag->Note(DiagMessage() << "writing image: w = " << info->width << ", h = " << info->height); + } + + png_color rgbPalette[256]; + png_byte alphaPalette[256]; + bool hasTransparency; + int paletteEntries; + + analyze_image(diag, *info, grayScaleTolerance, rgbPalette, alphaPalette, &paletteEntries, + &hasTransparency, &colorType, outRows); + + // If the image is a 9-patch, we need to preserve it as a ARGB file to make + // sure the pixels will not be pre-dithered/clamped until we decide they are + if (info->is9Patch && (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_GRAY || + colorType == PNG_COLOR_TYPE_PALETTE)) { + colorType = PNG_COLOR_TYPE_RGB_ALPHA; + } + + if (kDebug) { + switch (colorType) { + case PNG_COLOR_TYPE_PALETTE: + diag->Note(DiagMessage() << "has " << paletteEntries << " colors" + << (hasTransparency ? " (with alpha)" : "") + << ", using PNG_COLOR_TYPE_PALLETTE"); + break; + case PNG_COLOR_TYPE_GRAY: + diag->Note(DiagMessage() << "is opaque gray, using PNG_COLOR_TYPE_GRAY"); + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + diag->Note(DiagMessage() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA"); + break; + case PNG_COLOR_TYPE_RGB: + diag->Note(DiagMessage() << "is opaque RGB, using PNG_COLOR_TYPE_RGB"); + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + diag->Note(DiagMessage() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA"); + break; + } + } + + png_set_IHDR(writePtr, infoPtr, info->width, info->height, 8, colorType, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_PLTE(writePtr, infoPtr, rgbPalette, paletteEntries); + if (hasTransparency) { + png_set_tRNS(writePtr, infoPtr, alphaPalette, paletteEntries, (png_color_16p)0); + } + png_set_filter(writePtr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(writePtr, 0, PNG_ALL_FILTERS); + } + + if (info->is9Patch) { + int chunkCount = 2 + (info->haveLayoutBounds ? 1 : 0); + int pIndex = info->haveLayoutBounds ? 2 : 1; + int bIndex = 1; + int oIndex = 0; + + // Chunks ordered thusly because older platforms depend on the base 9 patch + // data being last + png_bytep chunkNames = + info->haveLayoutBounds ? (png_bytep) "npOl\0npLb\0npTc\0" : (png_bytep) "npOl\0npTc"; + + // base 9 patch data + if (kDebug) { + diag->Note(DiagMessage() << "adding 9-patch info.."); + } + memcpy((char*)unknowns[pIndex].name, "npTc", 5); + unknowns[pIndex].data = (png_byte*)info->serialize9Patch(); + unknowns[pIndex].size = info->info9Patch.serializedSize(); + // TODO: remove the check below when everything works + checkNinePatchSerialization(&info->info9Patch, unknowns[pIndex].data); + + // automatically generated 9 patch outline data + int chunkSize = sizeof(png_uint_32) * 6; + memcpy((char*)unknowns[oIndex].name, "npOl", 5); + unknowns[oIndex].data = (png_byte*)calloc(chunkSize, 1); + png_byte outputData[chunkSize]; + memcpy(&outputData, &info->outlineInsetsLeft, 4 * sizeof(png_uint_32)); + ((float*)outputData)[4] = info->outlineRadius; + ((png_uint_32*)outputData)[5] = info->outlineAlpha; + memcpy(unknowns[oIndex].data, &outputData, chunkSize); + unknowns[oIndex].size = chunkSize; + + // optional optical inset / layout bounds data + if (info->haveLayoutBounds) { + int chunkSize = sizeof(png_uint_32) * 4; + memcpy((char*)unknowns[bIndex].name, "npLb", 5); + unknowns[bIndex].data = (png_byte*)calloc(chunkSize, 1); + memcpy(unknowns[bIndex].data, &info->layoutBoundsLeft, chunkSize); + unknowns[bIndex].size = chunkSize; + } + + for (int i = 0; i < chunkCount; i++) { + unknowns[i].location = PNG_HAVE_PLTE; + } + png_set_keep_unknown_chunks(writePtr, PNG_HANDLE_CHUNK_ALWAYS, chunkNames, chunkCount); + png_set_unknown_chunks(writePtr, infoPtr, unknowns, chunkCount); + +#if PNG_LIBPNG_VER < 10600 + // Deal with unknown chunk location bug in 1.5.x and earlier. + png_set_unknown_chunk_location(writePtr, infoPtr, 0, PNG_HAVE_PLTE); + if (info->haveLayoutBounds) { + png_set_unknown_chunk_location(writePtr, infoPtr, 1, PNG_HAVE_PLTE); + } +#endif + } + + png_write_info(writePtr, infoPtr); + + png_bytepp rows; + if (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_RGB_ALPHA) { + if (colorType == PNG_COLOR_TYPE_RGB) { + png_set_filler(writePtr, 0, PNG_FILLER_AFTER); + } + rows = info->rows.data(); + } else { + rows = outRows; + } + png_write_image(writePtr, rows); + + if (kDebug) { + printf("Final image data:\n"); + // dump_image(info->width, info->height, rows, colorType); + } + + png_write_end(writePtr, infoPtr); + + for (uint32_t i = 0; i < info->height; i++) { + free(outRows[i]); + } + free(outRows); + free(unknowns[0].data); + free(unknowns[1].data); + free(unknowns[2].data); + + png_get_IHDR(writePtr, infoPtr, &width, &height, &bitDepth, &colorType, &interlaceType, + &compressionType, nullptr); + + if (kDebug) { + diag->Note(DiagMessage() << "image written: w = " << width << ", h = " << height + << ", d = " << bitDepth << ", colors = " << colorType + << ", inter = " << interlaceType << ", comp = " << compressionType); + } + return true; +} + +constexpr uint32_t kColorWhite = 0xffffffffu; +constexpr uint32_t kColorTick = 0xff000000u; +constexpr uint32_t kColorLayoutBoundsTick = 0xff0000ffu; + +enum class TickType { kNone, kTick, kLayoutBounds, kBoth }; + +static TickType tickType(png_bytep p, bool transparent, const char** outError) { + png_uint_32 color = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + + if (transparent) { + if (p[3] == 0) { + return TickType::kNone; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + if (color == kColorTick) { + return TickType::kTick; + } + + // Error cases + if (p[3] != 0xff) { + *outError = + "Frame pixels must be either solid or transparent " + "(not intermediate alphas)"; + return TickType::kNone; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in transparent frame must be black or red"; + } + return TickType::kTick; + } + + if (p[3] != 0xFF) { + *outError = "White frame must be a solid color (no alpha)"; + } + if (color == kColorWhite) { + return TickType::kNone; + } + if (color == kColorTick) { + return TickType::kTick; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in white frame must be black or red"; + return TickType::kNone; + } + return TickType::kTick; +} + +enum class TickState { kStart, kInside1, kOutside1 }; + +static bool getHorizontalTicks(png_bytep row, int width, bool transparent, bool required, + int32_t* outLeft, int32_t* outRight, const char** outError, + uint8_t* outDivs, bool multipleAllowed) { + *outLeft = *outRight = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < width - 1; i++) { + if (tickType(row + i * 4, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || (state == TickState::kOutside1 && multipleAllowed)) { + *outLeft = i - 1; + *outRight = width - 2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outLeft = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outRight = i - 1; + outRight += 2; + outLeft += 2; + state = TickState::kOutside1; + } + } else { + *outLeft = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outLeft = -1; + return false; + } + return true; +} + +static bool getVerticalTicks(png_bytepp rows, int offset, int height, bool transparent, + bool required, int32_t* outTop, int32_t* outBottom, + const char** outError, uint8_t* outDivs, bool multipleAllowed) { + *outTop = *outBottom = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < height - 1; i++) { + if (tickType(rows[i] + offset, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || (state == TickState::kOutside1 && multipleAllowed)) { + *outTop = i - 1; + *outBottom = height - 2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outTop = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outBottom = i - 1; + outTop += 2; + outBottom += 2; + state = TickState::kOutside1; + } + } else { + *outTop = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outTop = -1; + return false; + } + return true; +} + +static bool getHorizontalLayoutBoundsTicks(png_bytep row, int width, bool transparent, + bool /* required */, int32_t* outLeft, int32_t* outRight, + const char** outError) { + *outLeft = *outRight = 0; + + // Look for left tick + if (tickType(row + 4, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < width - 1) { + (*outLeft)++; + i++; + if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for right tick + if (tickType(row + (width - 2) * 4, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = width - 2; + while (i > 1) { + (*outRight)++; + i--; + if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static bool getVerticalLayoutBoundsTicks(png_bytepp rows, int offset, int height, bool transparent, + bool /* required */, int32_t* outTop, int32_t* outBottom, + const char** outError) { + *outTop = *outBottom = 0; + + // Look for top tick + if (tickType(rows[1] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < height - 1) { + (*outTop)++; + i++; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for bottom tick + if (tickType(rows[height - 2] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = height - 2; + while (i > 1) { + (*outBottom)++; + i--; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static void findMaxOpacity(png_bytepp rows, int startX, int startY, int endX, int endY, int dX, + int dY, int* outInset) { + uint8_t maxOpacity = 0; + int inset = 0; + *outInset = 0; + for (int x = startX, y = startY; x != endX && y != endY; x += dX, y += dY, inset++) { + png_byte* color = rows[y] + x * 4; + uint8_t opacity = color[3]; + if (opacity > maxOpacity) { + maxOpacity = opacity; + *outInset = inset; + } + if (opacity == 0xff) return; + } +} + +static uint8_t maxAlphaOverRow(png_bytep row, int startX, int endX) { + uint8_t maxAlpha = 0; + for (int x = startX; x < endX; x++) { + uint8_t alpha = (row + x * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static uint8_t maxAlphaOverCol(png_bytepp rows, int offsetX, int startY, int endY) { + uint8_t maxAlpha = 0; + for (int y = startY; y < endY; y++) { + uint8_t alpha = (rows[y] + offsetX * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static void getOutline(PngInfo* image) { + int midX = image->width / 2; + int midY = image->height / 2; + int endX = image->width - 2; + int endY = image->height - 2; + + // find left and right extent of nine patch content on center row + if (image->width > 4) { + findMaxOpacity(image->rows.data(), 1, midY, midX, -1, 1, 0, &image->outlineInsetsLeft); + findMaxOpacity(image->rows.data(), endX, midY, midX, -1, -1, 0, &image->outlineInsetsRight); + } else { + image->outlineInsetsLeft = 0; + image->outlineInsetsRight = 0; + } + + // find top and bottom extent of nine patch content on center column + if (image->height > 4) { + findMaxOpacity(image->rows.data(), midX, 1, -1, midY, 0, 1, &image->outlineInsetsTop); + findMaxOpacity(image->rows.data(), midX, endY, -1, midY, 0, -1, &image->outlineInsetsBottom); + } else { + image->outlineInsetsTop = 0; + image->outlineInsetsBottom = 0; + } + + int innerStartX = 1 + image->outlineInsetsLeft; + int innerStartY = 1 + image->outlineInsetsTop; + int innerEndX = endX - image->outlineInsetsRight; + int innerEndY = endY - image->outlineInsetsBottom; + int innerMidX = (innerEndX + innerStartX) / 2; + int innerMidY = (innerEndY + innerStartY) / 2; + + // assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center + image->outlineAlpha = + std::max(maxAlphaOverRow(image->rows[innerMidY], innerStartX, innerEndX), + maxAlphaOverCol(image->rows.data(), innerMidX, innerStartY, innerStartY)); + + int diagonalInset = 0; + findMaxOpacity(image->rows.data(), innerStartX, innerStartY, innerMidX, innerMidY, 1, 1, + &diagonalInset); + + /* Determine source radius based upon inset: + * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r + * sqrt(2) * r = sqrt(2) * i + r + * (sqrt(2) - 1) * r = sqrt(2) * i + * r = sqrt(2) / (sqrt(2) - 1) * i + */ + image->outlineRadius = 3.4142f * diagonalInset; + + if (kDebug) { + printf("outline insets %d %d %d %d, rad %f, alpha %x\n", image->outlineInsetsLeft, + image->outlineInsetsTop, image->outlineInsetsRight, image->outlineInsetsBottom, + image->outlineRadius, image->outlineAlpha); + } +} + +static uint32_t getColor(png_bytepp rows, int left, int top, int right, int bottom) { + png_bytep color = rows[top] + left * 4; + + if (left > right || top > bottom) { + return Res_png_9patch::TRANSPARENT_COLOR; + } + + while (top <= bottom) { + for (int i = left; i <= right; i++) { + png_bytep p = rows[top] + i * 4; + if (color[3] == 0) { + if (p[3] != 0) { + return Res_png_9patch::NO_COLOR; + } + } else if (p[0] != color[0] || p[1] != color[1] || p[2] != color[2] || p[3] != color[3]) { + return Res_png_9patch::NO_COLOR; + } + } + top++; + } + + if (color[3] == 0) { + return Res_png_9patch::TRANSPARENT_COLOR; + } + return (color[3] << 24) | (color[0] << 16) | (color[1] << 8) | color[2]; +} + +static bool do9Patch(PngInfo* image, std::string* outError) { + image->is9Patch = true; + + int W = image->width; + int H = image->height; + int i, j; + + const int maxSizeXDivs = W * sizeof(int32_t); + const int maxSizeYDivs = H * sizeof(int32_t); + int32_t* xDivs = image->xDivs = new int32_t[W]; + int32_t* yDivs = image->yDivs = new int32_t[H]; + uint8_t numXDivs = 0; + uint8_t numYDivs = 0; + + int8_t numColors; + int numRows; + int numCols; + int top; + int left; + int right; + int bottom; + memset(xDivs, -1, maxSizeXDivs); + memset(yDivs, -1, maxSizeYDivs); + image->info9Patch.paddingLeft = image->info9Patch.paddingRight = -1; + image->info9Patch.paddingTop = image->info9Patch.paddingBottom = -1; + image->layoutBoundsLeft = image->layoutBoundsRight = 0; + image->layoutBoundsTop = image->layoutBoundsBottom = 0; + + png_bytep p = image->rows[0]; + bool transparent = p[3] == 0; + bool hasColor = false; + + const char* errorMsg = nullptr; + int errorPixel = -1; + const char* errorEdge = nullptr; + + int colorIndex = 0; + std::vector<png_bytep> newRows; + + // Validate size... + if (W < 3 || H < 3) { + errorMsg = "Image must be at least 3x3 (1x1 without frame) pixels"; + goto getout; + } + + // Validate frame... + if (!transparent && (p[0] != 0xFF || p[1] != 0xFF || p[2] != 0xFF || p[3] != 0xFF)) { + errorMsg = "Must have one-pixel frame that is either transparent or white"; + goto getout; + } + + // Find left and right of sizing areas... + if (!getHorizontalTicks(p, W, transparent, true, &xDivs[0], &xDivs[1], &errorMsg, &numXDivs, + true)) { + errorPixel = xDivs[0]; + errorEdge = "top"; + goto getout; + } + + // Find top and bottom of sizing areas... + if (!getVerticalTicks(image->rows.data(), 0, H, transparent, true, &yDivs[0], &yDivs[1], + &errorMsg, &numYDivs, true)) { + errorPixel = yDivs[0]; + errorEdge = "left"; + goto getout; + } + + // Copy patch size data into image... + image->info9Patch.numXDivs = numXDivs; + image->info9Patch.numYDivs = numYDivs; + + // Find left and right of padding area... + if (!getHorizontalTicks(image->rows[H - 1], W, transparent, false, &image->info9Patch.paddingLeft, + &image->info9Patch.paddingRight, &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingLeft; + errorEdge = "bottom"; + goto getout; + } + + // Find top and bottom of padding area... + if (!getVerticalTicks(image->rows.data(), (W - 1) * 4, H, transparent, false, + &image->info9Patch.paddingTop, &image->info9Patch.paddingBottom, &errorMsg, + nullptr, false)) { + errorPixel = image->info9Patch.paddingTop; + errorEdge = "right"; + goto getout; + } + + // Find left and right of layout padding... + getHorizontalLayoutBoundsTicks(image->rows[H - 1], W, transparent, false, + &image->layoutBoundsLeft, &image->layoutBoundsRight, &errorMsg); + + getVerticalLayoutBoundsTicks(image->rows.data(), (W - 1) * 4, H, transparent, false, + &image->layoutBoundsTop, &image->layoutBoundsBottom, &errorMsg); + + image->haveLayoutBounds = image->layoutBoundsLeft != 0 || image->layoutBoundsRight != 0 || + image->layoutBoundsTop != 0 || image->layoutBoundsBottom != 0; + + if (image->haveLayoutBounds) { + if (kDebug) { + printf("layoutBounds=%d %d %d %d\n", image->layoutBoundsLeft, image->layoutBoundsTop, + image->layoutBoundsRight, image->layoutBoundsBottom); + } + } + + // use opacity of pixels to estimate the round rect outline + getOutline(image); + + // If padding is not yet specified, take values from size. + if (image->info9Patch.paddingLeft < 0) { + image->info9Patch.paddingLeft = xDivs[0]; + image->info9Patch.paddingRight = W - 2 - xDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingRight = W - 2 - image->info9Patch.paddingRight; + } + if (image->info9Patch.paddingTop < 0) { + image->info9Patch.paddingTop = yDivs[0]; + image->info9Patch.paddingBottom = H - 2 - yDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingBottom = H - 2 - image->info9Patch.paddingBottom; + } + + /* if (kDebug) { + printf("Size ticks for %s: x0=%d, x1=%d, y0=%d, y1=%d\n", imageName, + xDivs[0], xDivs[1], + yDivs[0], yDivs[1]); + printf("padding ticks for %s: l=%d, r=%d, t=%d, b=%d\n", imageName, + image->info9Patch.paddingLeft, image->info9Patch.paddingRight, + image->info9Patch.paddingTop, + image->info9Patch.paddingBottom); + }*/ + + // Remove frame from image. + newRows.resize(H - 2); + for (i = 0; i < H - 2; i++) { + newRows[i] = image->rows[i + 1]; + memmove(newRows[i], newRows[i] + 4, (W - 2) * 4); + } + image->rows.swap(newRows); + + image->width -= 2; + W = image->width; + image->height -= 2; + H = image->height; + + // Figure out the number of rows and columns in the N-patch + numCols = numXDivs + 1; + if (xDivs[0] == 0) { // Column 1 is strechable + numCols--; + } + if (xDivs[numXDivs - 1] == W) { + numCols--; + } + numRows = numYDivs + 1; + if (yDivs[0] == 0) { // Row 1 is strechable + numRows--; + } + if (yDivs[numYDivs - 1] == H) { + numRows--; + } + + // Make sure the amount of rows and columns will fit in the number of + // colors we can use in the 9-patch format. + if (numRows * numCols > 0x7F) { + errorMsg = "Too many rows and columns in 9-patch perimeter"; + goto getout; + } + + numColors = numRows * numCols; + image->info9Patch.numColors = numColors; + image->colors.resize(numColors); + + // Fill in color information for each patch. + + uint32_t c; + top = 0; + + // The first row always starts with the top being at y=0 and the bottom + // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case + // the first row is stretchable along the Y axis, otherwise it is fixed. + // The last row always ends with the bottom being bitmap.height and the top + // being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) or + // yDivs[numYDivs-1]. In the former case the last row is stretchable along + // the Y axis, otherwise it is fixed. + // + // The first and last columns are similarly treated with respect to the X + // axis. + // + // The above is to help explain some of the special casing that goes on the + // code below. + + // The initial yDiv and whether the first row is considered stretchable or + // not depends on whether yDiv[0] was zero or not. + for (j = (yDivs[0] == 0 ? 1 : 0); j <= numYDivs && top < H; j++) { + if (j == numYDivs) { + bottom = H; + } else { + bottom = yDivs[j]; + } + left = 0; + // The initial xDiv and whether the first column is considered + // stretchable or not depends on whether xDiv[0] was zero or not. + for (i = xDivs[0] == 0 ? 1 : 0; i <= numXDivs && left < W; i++) { + if (i == numXDivs) { + right = W; + } else { + right = xDivs[i]; + } + c = getColor(image->rows.data(), left, top, right - 1, bottom - 1); + image->colors[colorIndex++] = c; + if (kDebug) { + if (c != Res_png_9patch::NO_COLOR) { + hasColor = true; + } + } + left = right; + } + top = bottom; + } + + assert(colorIndex == numColors); + + if (kDebug && hasColor) { + for (i = 0; i < numColors; i++) { + if (i == 0) printf("Colors:\n"); + printf(" #%08x", image->colors[i]); + if (i == numColors - 1) printf("\n"); + } + } +getout: + if (errorMsg) { + std::stringstream err; + err << "9-patch malformed: " << errorMsg; + if (errorEdge) { + err << "." << std::endl; + if (errorPixel >= 0) { + err << "Found at pixel #" << errorPixel << " along " << errorEdge << " edge"; + } else { + err << "Found along " << errorEdge << " edge"; + } + } + *outError = err.str(); + return false; + } + return true; +} + +bool Png::process(const Source& source, std::istream* input, BigBuffer* outBuffer, + const PngOptions& options) { + png_byte signature[kPngSignatureSize]; + + // Read the PNG signature first. + if (!input->read(reinterpret_cast<char*>(signature), kPngSignatureSize)) { + mDiag->Error(DiagMessage() << strerror(errno)); + return false; + } + + // If the PNG signature doesn't match, bail early. + if (png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { + mDiag->Error(DiagMessage() << "not a valid png file"); + return false; + } + + bool result = false; + png_structp readPtr = nullptr; + png_infop infoPtr = nullptr; + png_structp writePtr = nullptr; + png_infop writeInfoPtr = nullptr; + PngInfo pngInfo = {}; + + readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!readPtr) { + mDiag->Error(DiagMessage() << "failed to allocate read ptr"); + goto bail; + } + + infoPtr = png_create_info_struct(readPtr); + if (!infoPtr) { + mDiag->Error(DiagMessage() << "failed to allocate info ptr"); + goto bail; + } + + png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(mDiag), nullptr, logWarning); + + // Set the read function to read from std::istream. + png_set_read_fn(readPtr, (png_voidp)input, readDataFromStream); + + if (!readPng(mDiag, readPtr, infoPtr, &pngInfo)) { + goto bail; + } + + if (android::base::EndsWith(source.path, ".9.png")) { + std::string errorMsg; + if (!do9Patch(&pngInfo, &errorMsg)) { + mDiag->Error(DiagMessage() << errorMsg); + goto bail; + } + } + + writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!writePtr) { + mDiag->Error(DiagMessage() << "failed to allocate write ptr"); + goto bail; + } + + writeInfoPtr = png_create_info_struct(writePtr); + if (!writeInfoPtr) { + mDiag->Error(DiagMessage() << "failed to allocate write info ptr"); + goto bail; + } + + png_set_error_fn(writePtr, nullptr, nullptr, logWarning); + + // Set the write function to write to std::ostream. + png_set_write_fn(writePtr, (png_voidp)outBuffer, writeDataToStream, flushDataToStream); + + if (!writePng(mDiag, writePtr, writeInfoPtr, &pngInfo, options.grayscale_tolerance)) { + goto bail; + } + + result = true; +bail: + if (readPtr) { + png_destroy_read_struct(&readPtr, &infoPtr, nullptr); + } + + if (writePtr) { + png_destroy_write_struct(&writePtr, &writeInfoPtr); + } + return result; +} + +} // namespace android diff --git a/libs/androidfw/PngChunkFilter.cpp b/libs/androidfw/PngChunkFilter.cpp new file mode 100644 index 000000000000..331b94803e93 --- /dev/null +++ b/libs/androidfw/PngChunkFilter.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2016 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. + */ + +#include "android-base/stringprintf.h" +#include "android-base/strings.h" +#include "androidfw/Png.h" +#include "androidfw/Streams.h" +#include "androidfw/StringPiece.h" + +using ::android::base::StringPrintf; + +namespace android { + +static constexpr const char* kPngSignature = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"; + +// Useful helper function that encodes individual bytes into a uint32 +// at compile time. +constexpr uint32_t u32(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + return (((uint32_t)a) << 24) | (((uint32_t)b) << 16) | (((uint32_t)c) << 8) | ((uint32_t)d); +} + +// Allow list of PNG chunk types that we want to keep in the resulting PNG. +enum PngChunkTypes { + kPngChunkIHDR = u32(73, 72, 68, 82), + kPngChunkIDAT = u32(73, 68, 65, 84), + kPngChunkIEND = u32(73, 69, 78, 68), + kPngChunkPLTE = u32(80, 76, 84, 69), + kPngChunktRNS = u32(116, 82, 78, 83), + kPngChunksRGB = u32(115, 82, 71, 66), +}; + +static uint32_t Peek32LE(const char* data) { + uint32_t word = ((uint32_t)data[0]) & 0x000000ff; + word <<= 8; + word |= ((uint32_t)data[1]) & 0x000000ff; + word <<= 8; + word |= ((uint32_t)data[2]) & 0x000000ff; + word <<= 8; + word |= ((uint32_t)data[3]) & 0x000000ff; + return word; +} + +static bool IsPngChunkAllowed(uint32_t type) { + switch (type) { + case kPngChunkIHDR: + case kPngChunkIDAT: + case kPngChunkIEND: + case kPngChunkPLTE: + case kPngChunktRNS: + case kPngChunksRGB: + return true; + default: + return false; + } +} + +PngChunkFilter::PngChunkFilter(StringPiece data) : data_(data) { + if (android::base::StartsWith(data_, kPngSignature)) { + window_start_ = 0; + window_end_ = kPngSignatureSize; + } else { + error_msg_ = "file does not start with PNG signature"; + } +} + +bool PngChunkFilter::ConsumeWindow(const void** buffer, size_t* len) { + if (window_start_ != window_end_) { + // We have bytes to give from our window. + const size_t bytes_read = window_end_ - window_start_; + *buffer = data_.data() + window_start_; + *len = bytes_read; + window_start_ = window_end_; + return true; + } + return false; +} + +bool PngChunkFilter::Next(const void** buffer, size_t* len) { + if (HadError()) { + return false; + } + + // In case BackUp was called, we must consume the window. + if (ConsumeWindow(buffer, len)) { + return true; + } + + // Advance the window as far as possible (until we meet a chunk that + // we want to strip). + while (window_end_ < data_.size()) { + // Chunk length (4 bytes) + type (4 bytes) + crc32 (4 bytes) = 12 bytes. + const size_t kMinChunkHeaderSize = 3 * sizeof(uint32_t); + + // Is there enough room for a chunk header? + if (data_.size() - window_end_ < kMinChunkHeaderSize) { + error_msg_ = StringPrintf("Not enough space for a PNG chunk @ byte %zu/%zu", window_end_, + data_.size()); + return false; + } + + // Verify the chunk length. + const uint32_t chunk_len = Peek32LE(data_.data() + window_end_); + if ((size_t)chunk_len > data_.size() - window_end_ - kMinChunkHeaderSize) { + // Overflow. + const uint32_t chunk_type = Peek32LE(data_.data() + window_end_ + sizeof(uint32_t)); + error_msg_ = StringPrintf( + "PNG chunk type %08x is too large: chunk length is %zu but chunk " + "starts at byte %zu/%zu", + chunk_type, (size_t)chunk_len, window_end_ + kMinChunkHeaderSize, data_.size()); + return false; + } + + // Do we strip this chunk? + const uint32_t chunk_type = Peek32LE(data_.data() + window_end_ + sizeof(uint32_t)); + if (IsPngChunkAllowed(chunk_type)) { + // Advance the window to include this chunk. + window_end_ += kMinChunkHeaderSize + chunk_len; + + // Special case the IEND chunk, which MUST appear last and libpng stops parsing once it hits + // such a chunk (let's do the same). + if (chunk_type == kPngChunkIEND) { + // Truncate the data to the end of this chunk. There may be garbage trailing after + // (b/38169876) + data_ = data_.substr(0, window_end_); + break; + } + + } else { + // We want to strip this chunk. If we accumulated a window, + // we must return the window now. + if (window_start_ != window_end_) { + break; + } + + // The window is empty, so we can advance past this chunk + // and keep looking for the next good chunk, + window_end_ += kMinChunkHeaderSize + chunk_len; + window_start_ = window_end_; + } + } + + if (ConsumeWindow(buffer, len)) { + return true; + } + return false; +} + +void PngChunkFilter::BackUp(size_t count) { + if (HadError()) { + return; + } + window_start_ -= count; +} + +bool PngChunkFilter::Rewind() { + if (HadError()) { + return false; + } + window_start_ = 0; + window_end_ = kPngSignatureSize; + return true; +} +} // namespace android diff --git a/libs/androidfw/PngCrunch.cpp b/libs/androidfw/PngCrunch.cpp new file mode 100644 index 000000000000..cf3c0eeff402 --- /dev/null +++ b/libs/androidfw/PngCrunch.cpp @@ -0,0 +1,730 @@ +/* + * Copyright (C) 2016 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. + */ + +#include <png.h> +#include <zlib.h> + +#include <algorithm> +#include <unordered_map> +#include <unordered_set> + +#include "android-base/errors.h" +#include "android-base/logging.h" +#include "android-base/macros.h" +#include "androidfw/Png.h" + +namespace android { + +// Custom deleter that destroys libpng read and info structs. +class PngReadStructDeleter { + public: + PngReadStructDeleter(png_structp read_ptr, png_infop info_ptr) + : read_ptr_(read_ptr), info_ptr_(info_ptr) { + } + + ~PngReadStructDeleter() { + png_destroy_read_struct(&read_ptr_, &info_ptr_, nullptr); + } + + private: + png_structp read_ptr_; + png_infop info_ptr_; + + DISALLOW_COPY_AND_ASSIGN(PngReadStructDeleter); +}; + +// Custom deleter that destroys libpng write and info structs. +class PngWriteStructDeleter { + public: + PngWriteStructDeleter(png_structp write_ptr, png_infop info_ptr) + : write_ptr_(write_ptr), info_ptr_(info_ptr) { + } + + ~PngWriteStructDeleter() { + png_destroy_write_struct(&write_ptr_, &info_ptr_); + } + + private: + png_structp write_ptr_; + png_infop info_ptr_; + + DISALLOW_COPY_AND_ASSIGN(PngWriteStructDeleter); +}; + +// Custom warning logging method that uses IDiagnostics. +static void LogWarning(png_structp png_ptr, png_const_charp warning_msg) { + android::IDiagnostics* diag = (android::IDiagnostics*)png_get_error_ptr(png_ptr); + diag->Warn(android::DiagMessage() << warning_msg); +} + +// Custom error logging method that uses IDiagnostics. +static void LogError(png_structp png_ptr, png_const_charp error_msg) { + android::IDiagnostics* diag = (android::IDiagnostics*)png_get_error_ptr(png_ptr); + diag->Error(android::DiagMessage() << error_msg); + + // Causes libpng to longjmp to the spot where setjmp was set. This is how libpng does + // error handling. If this custom error handler method were to return, libpng would, by + // default, print the error message to stdout and call the same png_longjmp method. + png_longjmp(png_ptr, 1); +} + +static void ReadDataFromStream(png_structp png_ptr, png_bytep buffer, png_size_t len) { + InputStream* in = (InputStream*)png_get_io_ptr(png_ptr); + + const void* in_buffer; + size_t in_len; + if (!in->Next(&in_buffer, &in_len)) { + if (in->HadError()) { + std::stringstream error_msg_builder; + error_msg_builder << "failed reading from input"; + if (!in->GetError().empty()) { + error_msg_builder << ": " << in->GetError(); + } + std::string err = error_msg_builder.str(); + png_error(png_ptr, err.c_str()); + } + return; + } + + const size_t bytes_read = std::min(in_len, len); + memcpy(buffer, in_buffer, bytes_read); + if (bytes_read != in_len) { + in->BackUp(in_len - bytes_read); + } +} + +static void WriteDataToStream(png_structp png_ptr, png_bytep buffer, png_size_t len) { + OutputStream* out = (OutputStream*)png_get_io_ptr(png_ptr); + + void* out_buffer; + size_t out_len; + while (len > 0) { + if (!out->Next(&out_buffer, &out_len)) { + if (out->HadError()) { + std::stringstream err_msg_builder; + err_msg_builder << "failed writing to output"; + if (!out->GetError().empty()) { + err_msg_builder << ": " << out->GetError(); + } + std::string err = out->GetError(); + png_error(png_ptr, err.c_str()); + } + return; + } + + const size_t bytes_written = std::min(out_len, len); + memcpy(out_buffer, buffer, bytes_written); + + // Advance the input buffer. + buffer += bytes_written; + len -= bytes_written; + + // Advance the output buffer. + out_len -= bytes_written; + } + + // If the entire output buffer wasn't used, backup. + if (out_len > 0) { + out->BackUp(out_len); + } +} + +std::unique_ptr<Image> ReadPng(InputStream* in, IDiagnostics* diag) { + // Read the first 8 bytes of the file looking for the PNG signature. + // Bail early if it does not match. + const png_byte* signature; + size_t buffer_size; + if (!in->Next((const void**)&signature, &buffer_size)) { + if (in->HadError()) { + diag->Error(android::DiagMessage() << "failed to read PNG signature: " << in->GetError()); + } else { + diag->Error(android::DiagMessage() << "not enough data for PNG signature"); + } + return {}; + } + + if (buffer_size < kPngSignatureSize || png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { + diag->Error(android::DiagMessage() << "file signature does not match PNG signature"); + return {}; + } + + // Start at the beginning of the first chunk. + in->BackUp(buffer_size - kPngSignatureSize); + + // Create and initialize the png_struct with the default error and warning handlers. + // The header version is also passed in to ensure that this was built against the same + // version of libpng. + png_structp read_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (read_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng read png_struct"); + return {}; + } + + // Create and initialize the memory for image header and data. + png_infop info_ptr = png_create_info_struct(read_ptr); + if (info_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng read png_info"); + png_destroy_read_struct(&read_ptr, nullptr, nullptr); + return {}; + } + + // Automatically release PNG resources at end of scope. + PngReadStructDeleter png_read_deleter(read_ptr, info_ptr); + + // libpng uses longjmp to jump to an error handling routine. + // setjmp will only return true if it was jumped to, aka there was + // an error. + if (setjmp(png_jmpbuf(read_ptr))) { + return {}; + } + + // Handle warnings ourselves via IDiagnostics. + png_set_error_fn(read_ptr, (png_voidp)&diag, LogError, LogWarning); + + // Set up the read functions which read from our custom data sources. + png_set_read_fn(read_ptr, (png_voidp)in, ReadDataFromStream); + + // Skip the signature that we already read. + png_set_sig_bytes(read_ptr, kPngSignatureSize); + + // Read the chunk headers. + png_read_info(read_ptr, info_ptr); + + // Extract image meta-data from the various chunk headers. + uint32_t width, height; + int bit_depth, color_type, interlace_method, compression_method, filter_method; + png_get_IHDR(read_ptr, info_ptr, &width, &height, &bit_depth, &color_type, &interlace_method, + &compression_method, &filter_method); + + // When the image is read, expand it so that it is in RGBA 8888 format + // so that image handling is uniform. + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(read_ptr); + } + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { + png_set_expand_gray_1_2_4_to_8(read_ptr); + } + + if (png_get_valid(read_ptr, info_ptr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(read_ptr); + } + + if (bit_depth == 16) { + png_set_strip_16(read_ptr); + } + + if (!(color_type & PNG_COLOR_MASK_ALPHA)) { + png_set_add_alpha(read_ptr, 0xFF, PNG_FILLER_AFTER); + } + + if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(read_ptr); + } + + if (interlace_method != PNG_INTERLACE_NONE) { + png_set_interlace_handling(read_ptr); + } + + // Once all the options for reading have been set, we need to flush + // them to libpng. + png_read_update_info(read_ptr, info_ptr); + + // 9-patch uses int32_t to index images, so we cap the image dimensions to + // something + // that can always be represented by 9-patch. + if (width > std::numeric_limits<int32_t>::max() || height > std::numeric_limits<int32_t>::max()) { + diag->Error(android::DiagMessage() + << "PNG image dimensions are too large: " << width << "x" << height); + return {}; + } + + std::unique_ptr<Image> output_image = std::make_unique<Image>(); + output_image->width = static_cast<int32_t>(width); + output_image->height = static_cast<int32_t>(height); + + const size_t row_bytes = png_get_rowbytes(read_ptr, info_ptr); + CHECK(row_bytes == 4 * width); // RGBA + + // Allocate one large block to hold the image. + output_image->data = std::unique_ptr<uint8_t[]>(new uint8_t[height * row_bytes]); + + // Create an array of rows that index into the data block. + output_image->rows = std::unique_ptr<uint8_t*[]>(new uint8_t*[height]); + for (uint32_t h = 0; h < height; h++) { + output_image->rows[h] = output_image->data.get() + (h * row_bytes); + } + + // Actually read the image pixels. + png_read_image(read_ptr, output_image->rows.get()); + + // Finish reading. This will read any other chunks after the image data. + png_read_end(read_ptr, info_ptr); + + return output_image; +} + +// Experimentally chosen constant to be added to the overhead of using color type +// PNG_COLOR_TYPE_PALETTE to account for the uncompressability of the palette chunk. +// Without this, many small PNGs encoded with palettes are larger after compression than +// the same PNGs encoded as RGBA. +constexpr static const size_t kPaletteOverheadConstant = 1024u * 10u; + +// Pick a color type by which to encode the image, based on which color type will take +// the least amount of disk space. +// +// 9-patch images traditionally have not been encoded with palettes. +// The original rationale was to avoid dithering until after scaling, +// but I don't think this would be an issue with palettes. Either way, +// our naive size estimation tends to be wrong for small images like 9-patches +// and using palettes balloons the size of the resulting 9-patch. +// In order to not regress in size, restrict 9-patch to not use palettes. + +// The options are: +// +// - RGB +// - RGBA +// - RGB + cheap alpha +// - Color palette +// - Color palette + cheap alpha +// - Color palette + alpha palette +// - Grayscale +// - Grayscale + cheap alpha +// - Grayscale + alpha +// +static int PickColorType(int32_t width, int32_t height, bool grayscale, + bool convertible_to_grayscale, bool has_nine_patch, + size_t color_palette_size, size_t alpha_palette_size) { + const size_t palette_chunk_size = 16 + color_palette_size * 3; + const size_t alpha_chunk_size = 16 + alpha_palette_size; + const size_t color_alpha_data_chunk_size = 16 + 4 * width * height; + const size_t color_data_chunk_size = 16 + 3 * width * height; + const size_t grayscale_alpha_data_chunk_size = 16 + 2 * width * height; + const size_t palette_data_chunk_size = 16 + width * height; + + if (grayscale) { + if (alpha_palette_size == 0) { + // This is the smallest the data can be. + return PNG_COLOR_TYPE_GRAY; + } else if (color_palette_size <= 256 && !has_nine_patch) { + // This grayscale has alpha and can fit within a palette. + // See if it is worth fitting into a palette. + const size_t palette_threshold = palette_chunk_size + alpha_chunk_size + + palette_data_chunk_size + kPaletteOverheadConstant; + if (grayscale_alpha_data_chunk_size > palette_threshold) { + return PNG_COLOR_TYPE_PALETTE; + } + } + return PNG_COLOR_TYPE_GRAY_ALPHA; + } + + if (color_palette_size <= 256 && !has_nine_patch) { + // This image can fit inside a palette. Let's see if it is worth it. + size_t total_size_with_palette = palette_data_chunk_size + palette_chunk_size; + size_t total_size_without_palette = color_data_chunk_size; + if (alpha_palette_size > 0) { + total_size_with_palette += alpha_palette_size; + total_size_without_palette = color_alpha_data_chunk_size; + } + + if (total_size_without_palette > total_size_with_palette + kPaletteOverheadConstant) { + return PNG_COLOR_TYPE_PALETTE; + } + } + + if (convertible_to_grayscale) { + if (alpha_palette_size == 0) { + return PNG_COLOR_TYPE_GRAY; + } else { + return PNG_COLOR_TYPE_GRAY_ALPHA; + } + } + + if (alpha_palette_size == 0) { + return PNG_COLOR_TYPE_RGB; + } + return PNG_COLOR_TYPE_RGBA; +} + +// Assigns indices to the color and alpha palettes, encodes them, and then invokes +// png_set_PLTE/png_set_tRNS. +// This must be done before writing image data. +// Image data must be transformed to use the indices assigned within the palette. +static void WritePalette(png_structp write_ptr, png_infop write_info_ptr, + std::unordered_map<uint32_t, int>* color_palette, + std::unordered_set<uint32_t>* alpha_palette) { + CHECK(color_palette->size() <= 256); + CHECK(alpha_palette->size() <= 256); + + // Populate the PNG palette struct and assign indices to the color palette. + + // Colors in the alpha palette should have smaller indices. + // This will ensure that we can truncate the alpha palette if it is + // smaller than the color palette. + int index = 0; + for (uint32_t color : *alpha_palette) { + (*color_palette)[color] = index++; + } + + // Assign the rest of the entries. + for (auto& entry : *color_palette) { + if (entry.second == -1) { + entry.second = index++; + } + } + + // Create the PNG color palette struct. + auto color_palette_bytes = std::unique_ptr<png_color[]>(new png_color[color_palette->size()]); + + std::unique_ptr<png_byte[]> alpha_palette_bytes; + if (!alpha_palette->empty()) { + alpha_palette_bytes = std::unique_ptr<png_byte[]>(new png_byte[alpha_palette->size()]); + } + + for (const auto& entry : *color_palette) { + const uint32_t color = entry.first; + const int index = entry.second; + CHECK(index >= 0); + CHECK(static_cast<size_t>(index) < color_palette->size()); + + png_colorp slot = color_palette_bytes.get() + index; + slot->red = color >> 24; + slot->green = color >> 16; + slot->blue = color >> 8; + + const png_byte alpha = color & 0x000000ff; + if (alpha != 0xff && alpha_palette_bytes) { + CHECK(static_cast<size_t>(index) < alpha_palette->size()); + alpha_palette_bytes[index] = alpha; + } + } + + // The bytes get copied here, so it is safe to release color_palette_bytes at + // the end of function + // scope. + png_set_PLTE(write_ptr, write_info_ptr, color_palette_bytes.get(), color_palette->size()); + + if (alpha_palette_bytes) { + png_set_tRNS(write_ptr, write_info_ptr, alpha_palette_bytes.get(), alpha_palette->size(), + nullptr); + } +} + +// Write the 9-patch custom PNG chunks to write_info_ptr. This must be done +// before writing image data. +static void WriteNinePatch(png_structp write_ptr, png_infop write_info_ptr, + const NinePatch* nine_patch) { + // The order of the chunks is important. + // 9-patch code in older platforms expects the 9-patch chunk to be last. + + png_unknown_chunk unknown_chunks[3]; + memset(unknown_chunks, 0, sizeof(unknown_chunks)); + + size_t index = 0; + size_t chunk_len = 0; + + std::unique_ptr<uint8_t[]> serialized_outline = + nine_patch->SerializeRoundedRectOutline(&chunk_len); + strcpy((char*)unknown_chunks[index].name, "npOl"); + unknown_chunks[index].size = chunk_len; + unknown_chunks[index].data = (png_bytep)serialized_outline.get(); + unknown_chunks[index].location = PNG_HAVE_PLTE; + index++; + + std::unique_ptr<uint8_t[]> serialized_layout_bounds; + if (nine_patch->layout_bounds.nonZero()) { + serialized_layout_bounds = nine_patch->SerializeLayoutBounds(&chunk_len); + strcpy((char*)unknown_chunks[index].name, "npLb"); + unknown_chunks[index].size = chunk_len; + unknown_chunks[index].data = (png_bytep)serialized_layout_bounds.get(); + unknown_chunks[index].location = PNG_HAVE_PLTE; + index++; + } + + std::unique_ptr<uint8_t[]> serialized_nine_patch = nine_patch->SerializeBase(&chunk_len); + strcpy((char*)unknown_chunks[index].name, "npTc"); + unknown_chunks[index].size = chunk_len; + unknown_chunks[index].data = (png_bytep)serialized_nine_patch.get(); + unknown_chunks[index].location = PNG_HAVE_PLTE; + index++; + + // Handle all unknown chunks. We are manually setting the chunks here, + // so we will only ever handle our custom chunks. + png_set_keep_unknown_chunks(write_ptr, PNG_HANDLE_CHUNK_ALWAYS, nullptr, 0); + + // Set the actual chunks here. The data gets copied, so our buffers can + // safely go out of scope. + png_set_unknown_chunks(write_ptr, write_info_ptr, unknown_chunks, index); +} + +bool WritePng(const Image* image, const NinePatch* nine_patch, OutputStream* out, + const PngOptions& options, IDiagnostics* diag, bool verbose) { + // Create and initialize the write png_struct with the default error and + // warning handlers. + // The header version is also passed in to ensure that this was built against the same + // version of libpng. + png_structp write_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (write_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng write png_struct"); + return false; + } + + // Allocate memory to store image header data. + png_infop write_info_ptr = png_create_info_struct(write_ptr); + if (write_info_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng write png_info"); + png_destroy_write_struct(&write_ptr, nullptr); + return false; + } + + // Automatically release PNG resources at end of scope. + PngWriteStructDeleter png_write_deleter(write_ptr, write_info_ptr); + + // libpng uses longjmp to jump to error handling routines. + // setjmp will return true only if it was jumped to, aka, there was an error. + if (setjmp(png_jmpbuf(write_ptr))) { + return false; + } + + // Handle warnings with our IDiagnostics. + png_set_error_fn(write_ptr, (png_voidp)&diag, LogError, LogWarning); + + // Set up the write functions which write to our custom data sources. + png_set_write_fn(write_ptr, (png_voidp)out, WriteDataToStream, nullptr); + + // We want small files and can take the performance hit to achieve this goal. + png_set_compression_level(write_ptr, Z_BEST_COMPRESSION); + + // Begin analysis of the image data. + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors (palette). + std::unordered_map<uint32_t, int> color_palette; + std::unordered_set<uint32_t> alpha_palette; + bool needs_to_zero_rgb_channels_of_transparent_pixels = false; + bool grayscale = true; + int max_gray_deviation = 0; + + for (int32_t y = 0; y < image->height; y++) { + const uint8_t* row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int red = *row++; + int green = *row++; + int blue = *row++; + int alpha = *row++; + + if (alpha == 0) { + // The color is completely transparent. + // For purposes of palettes and grayscale optimization, + // treat all channels as 0x00. + needs_to_zero_rgb_channels_of_transparent_pixels = + needs_to_zero_rgb_channels_of_transparent_pixels || + (red != 0 || green != 0 || blue != 0); + red = green = blue = 0; + } + + // Insert the color into the color palette. + const uint32_t color = red << 24 | green << 16 | blue << 8 | alpha; + color_palette[color] = -1; + + // If the pixel has non-opaque alpha, insert it into the + // alpha palette. + if (alpha != 0xff) { + alpha_palette.insert(color); + } + + // Check if the image is indeed grayscale. + if (grayscale) { + if (red != green || red != blue) { + grayscale = false; + } + } + + // Calculate the gray scale deviation so that it can be compared + // with the threshold. + max_gray_deviation = std::max(std::abs(red - green), max_gray_deviation); + max_gray_deviation = std::max(std::abs(green - blue), max_gray_deviation); + max_gray_deviation = std::max(std::abs(blue - red), max_gray_deviation); + } + } + + if (verbose) { + android::DiagMessage msg; + msg << " paletteSize=" << color_palette.size() << " alphaPaletteSize=" << alpha_palette.size() + << " maxGrayDeviation=" << max_gray_deviation + << " grayScale=" << (grayscale ? "true" : "false"); + diag->Note(msg); + } + + const bool convertible_to_grayscale = max_gray_deviation <= options.grayscale_tolerance; + + const int new_color_type = + PickColorType(image->width, image->height, grayscale, convertible_to_grayscale, + nine_patch != nullptr, color_palette.size(), alpha_palette.size()); + + if (verbose) { + android::DiagMessage msg; + msg << "encoding PNG "; + if (nine_patch) { + msg << "(with 9-patch) as "; + } + switch (new_color_type) { + case PNG_COLOR_TYPE_GRAY: + msg << "GRAY"; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + msg << "GRAY + ALPHA"; + break; + case PNG_COLOR_TYPE_RGB: + msg << "RGB"; + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + msg << "RGBA"; + break; + case PNG_COLOR_TYPE_PALETTE: + msg << "PALETTE"; + break; + default: + msg << "unknown type " << new_color_type; + break; + } + diag->Note(msg); + } + + png_set_IHDR(write_ptr, write_info_ptr, image->width, image->height, 8, new_color_type, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (new_color_type & PNG_COLOR_MASK_PALETTE) { + // Assigns indices to the palette, and writes the encoded palette to the + // libpng writePtr. + WritePalette(write_ptr, write_info_ptr, &color_palette, &alpha_palette); + png_set_filter(write_ptr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(write_ptr, 0, PNG_ALL_FILTERS); + } + + if (nine_patch) { + WriteNinePatch(write_ptr, write_info_ptr, nine_patch); + } + + // Flush our updates to the header. + png_write_info(write_ptr, write_info_ptr); + + // Write out each row of image data according to its encoding. + if (new_color_type == PNG_COLOR_TYPE_PALETTE) { + // 1 byte/pixel. + auto out_row = std::unique_ptr<png_byte[]>(new png_byte[image->width]); + + for (int32_t y = 0; y < image->height; y++) { + png_const_bytep in_row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int rr = *in_row++; + int gg = *in_row++; + int bb = *in_row++; + int aa = *in_row++; + if (aa == 0) { + // Zero out color channels when transparent. + rr = gg = bb = 0; + } + + const uint32_t color = rr << 24 | gg << 16 | bb << 8 | aa; + const int idx = color_palette[color]; + CHECK(idx != -1); + out_row[x] = static_cast<png_byte>(idx); + } + png_write_row(write_ptr, out_row.get()); + } + } else if (new_color_type == PNG_COLOR_TYPE_GRAY || new_color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + const size_t bpp = new_color_type == PNG_COLOR_TYPE_GRAY ? 1 : 2; + auto out_row = std::unique_ptr<png_byte[]>(new png_byte[image->width * bpp]); + + for (int32_t y = 0; y < image->height; y++) { + png_const_bytep in_row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int rr = in_row[x * 4]; + int gg = in_row[x * 4 + 1]; + int bb = in_row[x * 4 + 2]; + int aa = in_row[x * 4 + 3]; + if (aa == 0) { + // Zero out the gray channel when transparent. + rr = gg = bb = 0; + } + + if (grayscale) { + // The image was already grayscale, red == green == blue. + out_row[x * bpp] = in_row[x * 4]; + } else { + // The image is convertible to grayscale, use linear-luminance of + // sRGB colorspace: + // https://en.wikipedia.org/wiki/Grayscale#Colorimetric_.28luminance-preserving.29_conversion_to_grayscale + out_row[x * bpp] = (png_byte)(rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + + if (bpp == 2) { + // Write out alpha if we have it. + out_row[x * bpp + 1] = aa; + } + } + png_write_row(write_ptr, out_row.get()); + } + } else if (new_color_type == PNG_COLOR_TYPE_RGB || new_color_type == PNG_COLOR_TYPE_RGBA) { + const size_t bpp = new_color_type == PNG_COLOR_TYPE_RGB ? 3 : 4; + if (needs_to_zero_rgb_channels_of_transparent_pixels) { + // The source RGBA data can't be used as-is, because we need to zero out + // the RGB values of transparent pixels. + auto out_row = std::unique_ptr<png_byte[]>(new png_byte[image->width * bpp]); + + for (int32_t y = 0; y < image->height; y++) { + png_const_bytep in_row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int rr = *in_row++; + int gg = *in_row++; + int bb = *in_row++; + int aa = *in_row++; + if (aa == 0) { + // Zero out the RGB channels when transparent. + rr = gg = bb = 0; + } + out_row[x * bpp] = rr; + out_row[x * bpp + 1] = gg; + out_row[x * bpp + 2] = bb; + if (bpp == 4) { + out_row[x * bpp + 3] = aa; + } + } + png_write_row(write_ptr, out_row.get()); + } + } else { + // The source image can be used as-is, just tell libpng whether or not to + // ignore the alpha channel. + if (new_color_type == PNG_COLOR_TYPE_RGB) { + // Delete the extraneous alpha values that we appended to our buffer + // when reading the original values. + png_set_filler(write_ptr, 0, PNG_FILLER_AFTER); + } + png_write_image(write_ptr, image->rows.get()); + } + } else { + LOG(FATAL) << "unreachable"; + } + + png_write_end(write_ptr, write_info_ptr); + return true; +} + +} // namespace android diff --git a/libs/androidfw/include/androidfw/BigBufferStream.h b/libs/androidfw/include/androidfw/BigBufferStream.h new file mode 100644 index 000000000000..c23194bae423 --- /dev/null +++ b/libs/androidfw/include/androidfw/BigBufferStream.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 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. + */ + +#pragma once + +#include "BigBuffer.h" +#include "Streams.h" + +namespace android { + +class BigBufferInputStream : public KnownSizeInputStream { + public: + inline explicit BigBufferInputStream(const BigBuffer* buffer) + : owning_buffer_(0), buffer_(buffer), iter_(buffer->begin()) { + } + + inline explicit BigBufferInputStream(android::BigBuffer&& buffer) + : owning_buffer_(std::move(buffer)), buffer_(&owning_buffer_), iter_(buffer_->begin()) { + } + + virtual ~BigBufferInputStream() = default; + + bool Next(const void** data, size_t* size) override; + + void BackUp(size_t count) override; + + bool CanRewind() const override; + + bool Rewind() override; + + size_t ByteCount() const override; + + bool HadError() const override; + + size_t TotalSize() const override; + + bool ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) override; + + private: + DISALLOW_COPY_AND_ASSIGN(BigBufferInputStream); + + android::BigBuffer owning_buffer_; + const BigBuffer* buffer_; + BigBuffer::const_iterator iter_; + size_t offset_ = 0; + size_t bytes_read_ = 0; +}; + +class BigBufferOutputStream : public OutputStream { + public: + inline explicit BigBufferOutputStream(BigBuffer* buffer) : buffer_(buffer) { + } + virtual ~BigBufferOutputStream() = default; + + bool Next(void** data, size_t* size) override; + + void BackUp(size_t count) override; + + size_t ByteCount() const override; + + bool HadError() const override; + + private: + DISALLOW_COPY_AND_ASSIGN(BigBufferOutputStream); + + BigBuffer* buffer_; +}; + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/FileStream.h b/libs/androidfw/include/androidfw/FileStream.h new file mode 100644 index 000000000000..87c42d12f825 --- /dev/null +++ b/libs/androidfw/include/androidfw/FileStream.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 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. + */ + +#pragma once + +#include <memory> +#include <string> +#include <unistd.h> + +#include "Streams.h" +#include "android-base/macros.h" +#include "android-base/unique_fd.h" + +namespace android { + +constexpr size_t kDefaultBufferCapacity = 4096u; + +class FileInputStream : public InputStream { + public: + explicit FileInputStream(const std::string& path, + size_t buffer_capacity = kDefaultBufferCapacity); + + // Take ownership of `fd`. + explicit FileInputStream(int fd, size_t buffer_capacity = kDefaultBufferCapacity); + + // Take ownership of `fd`. + explicit FileInputStream(android::base::borrowed_fd fd, + size_t buffer_capacity = kDefaultBufferCapacity); + + ~FileInputStream() { + if (should_close_ && (fd_ != -1)) { + close(fd_); + } + } + + bool Next(const void** data, size_t* size) override; + + void BackUp(size_t count) override; + + size_t ByteCount() const override; + + bool HadError() const override; + + std::string GetError() const override; + + bool ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) override; + + private: + DISALLOW_COPY_AND_ASSIGN(FileInputStream); + + int fd_ = -1; + std::string error_; + bool should_close_; + std::unique_ptr<uint8_t[]> buffer_; + size_t buffer_capacity_ = 0u; + size_t buffer_offset_ = 0u; + size_t buffer_size_ = 0u; + size_t total_byte_count_ = 0u; +}; + +class FileOutputStream : public OutputStream { + public: + explicit FileOutputStream(const std::string& path, + size_t buffer_capacity = kDefaultBufferCapacity); + + // Does not take ownership of `fd`. + explicit FileOutputStream(int fd, size_t buffer_capacity = kDefaultBufferCapacity); + + // Takes ownership of `fd`. + explicit FileOutputStream(android::base::unique_fd fd, + size_t buffer_capacity = kDefaultBufferCapacity); + + ~FileOutputStream(); + + bool Next(void** data, size_t* size) override; + + // Immediately flushes out the contents of the buffer to disk. + bool Flush(); + + void BackUp(size_t count) override; + + size_t ByteCount() const override; + + bool HadError() const override; + + std::string GetError() const override; + + private: + DISALLOW_COPY_AND_ASSIGN(FileOutputStream); + + bool FlushImpl(); + + android::base::unique_fd owned_fd_; + int fd_; + std::string error_; + std::unique_ptr<uint8_t[]> buffer_; + size_t buffer_capacity_ = 0u; + size_t buffer_offset_ = 0u; + size_t total_byte_count_ = 0u; +}; + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/IDiagnostics.h b/libs/androidfw/include/androidfw/IDiagnostics.h index 865a298f8389..d1dda818d97c 100644 --- a/libs/androidfw/include/androidfw/IDiagnostics.h +++ b/libs/androidfw/include/androidfw/IDiagnostics.h @@ -17,10 +17,15 @@ #ifndef _ANDROID_DIAGNOSTICS_H #define _ANDROID_DIAGNOSTICS_H +// on some systems ERROR is defined as 0 so android::base::ERROR becomes android::base::0 +// which doesn't compile. We undef it here to avoid that and because we don't ever need that def. +#undef ERROR + #include <sstream> #include <string> #include "Source.h" +#include "android-base/logging.h" #include "android-base/macros.h" #include "androidfw/StringPiece.h" @@ -144,6 +149,36 @@ class NoOpDiagnostics : public IDiagnostics { DISALLOW_COPY_AND_ASSIGN(NoOpDiagnostics); }; +class AndroidLogDiagnostics : public IDiagnostics { + public: + AndroidLogDiagnostics() = default; + + void Log(Level level, DiagMessageActual& actual_msg) override { + android::base::LogSeverity severity; + switch (level) { + case Level::Error: + severity = android::base::ERROR; + break; + + case Level::Warn: + severity = android::base::WARNING; + break; + + case Level::Note: + severity = android::base::INFO; + break; + } + if (!actual_msg.source.path.empty()) { + LOG(severity) << actual_msg.source << ": " + actual_msg.message; + } else { + LOG(severity) << actual_msg.message; + } + } + + DISALLOW_COPY_AND_ASSIGN(AndroidLogDiagnostics); +}; + + } // namespace android #endif /* _ANDROID_DIAGNOSTICS_H */ diff --git a/libs/androidfw/include/androidfw/Image.h b/libs/androidfw/include/androidfw/Image.h new file mode 100644 index 000000000000..c18c34c25bf9 --- /dev/null +++ b/libs/androidfw/include/androidfw/Image.h @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include <cstdint> +#include <memory> +#include <string> +#include <vector> + +#include "android-base/macros.h" + +namespace android { + +/** + * An in-memory image, loaded from disk, with pixels in RGBA_8888 format. + */ +class Image { + public: + explicit Image() = default; + + /** + * A `height` sized array of pointers, where each element points to a + * `width` sized row of RGBA_8888 pixels. + */ + std::unique_ptr<uint8_t*[]> rows; + + /** + * The width of the image in RGBA_8888 pixels. This is int32_t because of + * 9-patch data + * format limitations. + */ + int32_t width = 0; + + /** + * The height of the image in RGBA_8888 pixels. This is int32_t because of + * 9-patch data + * format limitations. + */ + int32_t height = 0; + + /** + * Buffer to the raw image data stored sequentially. + * Use `rows` to access the data on a row-by-row basis. + */ + std::unique_ptr<uint8_t[]> data; + + private: + DISALLOW_COPY_AND_ASSIGN(Image); +}; + +/** + * A range of pixel values, starting at 'start' and ending before 'end' + * exclusive. Or rather [a, b). + */ +struct Range { + int32_t start = 0; + int32_t end = 0; + + explicit Range() = default; + inline explicit Range(int32_t s, int32_t e) : start(s), end(e) { + } +}; + +inline bool operator==(const Range& left, const Range& right) { + return left.start == right.start && left.end == right.end; +} + +/** + * Inset lengths from all edges of a rectangle. `left` and `top` are measured + * from the left and top + * edges, while `right` and `bottom` are measured from the right and bottom + * edges, respectively. + */ +struct Bounds { + int32_t left = 0; + int32_t top = 0; + int32_t right = 0; + int32_t bottom = 0; + + explicit Bounds() = default; + inline explicit Bounds(int32_t l, int32_t t, int32_t r, int32_t b) + : left(l), top(t), right(r), bottom(b) { + } + + bool nonZero() const; +}; + +inline bool Bounds::nonZero() const { + return left != 0 || top != 0 || right != 0 || bottom != 0; +} + +inline bool operator==(const Bounds& left, const Bounds& right) { + return left.left == right.left && left.top == right.top && left.right == right.right && + left.bottom == right.bottom; +} + +/** + * Contains 9-patch data from a source image. All measurements exclude the 1px + * border of the + * source 9-patch image. + */ +class NinePatch { + public: + static std::unique_ptr<NinePatch> Create(uint8_t** rows, const int32_t width, + const int32_t height, std::string* err_out); + + /** + * Packs the RGBA_8888 data pointed to by pixel into a uint32_t + * with format 0xAARRGGBB (the way 9-patch expects it). + */ + static uint32_t PackRGBA(const uint8_t* pixel); + + /** + * 9-patch content padding/insets. All positions are relative to the 9-patch + * NOT including the 1px thick source border. + */ + Bounds padding; + + /** + * Optical layout bounds/insets. This overrides the padding for + * layout purposes. All positions are relative to the 9-patch + * NOT including the 1px thick source border. + * See + * https://developer.android.com/about/versions/android-4.3.html#OpticalBounds + */ + Bounds layout_bounds; + + /** + * Outline of the image, calculated based on opacity. + */ + Bounds outline; + + /** + * The computed radius of the outline. If non-zero, the outline is a + * rounded-rect. + */ + float outline_radius = 0.0f; + + /** + * The largest alpha value within the outline. + */ + uint32_t outline_alpha = 0x000000ffu; + + /** + * Horizontal regions of the image that are stretchable. + * All positions are relative to the 9-patch + * NOT including the 1px thick source border. + */ + std::vector<Range> horizontal_stretch_regions; + + /** + * Vertical regions of the image that are stretchable. + * All positions are relative to the 9-patch + * NOT including the 1px thick source border. + */ + std::vector<Range> vertical_stretch_regions; + + /** + * The colors within each region, fixed or stretchable. + * For w*h regions, the color of region (x,y) is addressable + * via index y*w + x. + */ + std::vector<uint32_t> region_colors; + + /** + * Returns serialized data containing the original basic 9-patch meta data. + * Optical layout bounds and round rect outline data must be serialized + * separately using SerializeOpticalLayoutBounds() and + * SerializeRoundedRectOutline(). + */ + std::unique_ptr<uint8_t[]> SerializeBase(size_t* out_len) const; + + /** + * Serializes the layout bounds. + */ + std::unique_ptr<uint8_t[]> SerializeLayoutBounds(size_t* out_len) const; + + /** + * Serializes the rounded-rect outline. + */ + std::unique_ptr<uint8_t[]> SerializeRoundedRectOutline(size_t* out_len) const; + + private: + explicit NinePatch() = default; + + DISALLOW_COPY_AND_ASSIGN(NinePatch); +}; + +::std::ostream& operator<<(::std::ostream& out, const Range& range); +::std::ostream& operator<<(::std::ostream& out, const Bounds& bounds); +::std::ostream& operator<<(::std::ostream& out, const NinePatch& nine_patch); + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/Png.h b/libs/androidfw/include/androidfw/Png.h new file mode 100644 index 000000000000..2ece43e08110 --- /dev/null +++ b/libs/androidfw/include/androidfw/Png.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 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. + */ + +#pragma once + +#include <string> + +#include "BigBuffer.h" +#include "IDiagnostics.h" +#include "Image.h" +#include "Source.h" +#include "Streams.h" +#include "android-base/macros.h" + +namespace android { +// Size in bytes of the PNG signature. +constexpr size_t kPngSignatureSize = 8u; + +struct PngOptions { + int grayscale_tolerance = 0; +}; + +/** + * Deprecated. Removing once new PNG crunching code is proved to be correct. + */ +class Png { + public: + explicit Png(IDiagnostics* diag) : mDiag(diag) { + } + + bool process(const Source& source, std::istream* input, BigBuffer* outBuffer, + const PngOptions& options); + + private: + DISALLOW_COPY_AND_ASSIGN(Png); + + IDiagnostics* mDiag; +}; + +/** + * An InputStream that filters out unimportant PNG chunks. + */ +class PngChunkFilter : public InputStream { + public: + explicit PngChunkFilter(StringPiece data); + virtual ~PngChunkFilter() = default; + + bool Next(const void** buffer, size_t* len) override; + void BackUp(size_t count) override; + + bool CanRewind() const override { + return true; + } + bool Rewind() override; + size_t ByteCount() const override { + return window_start_; + } + + bool HadError() const override { + return !error_msg_.empty(); + } + std::string GetError() const override { + return error_msg_; + } + + private: + DISALLOW_COPY_AND_ASSIGN(PngChunkFilter); + + bool ConsumeWindow(const void** buffer, size_t* len); + + StringPiece data_; + size_t window_start_ = 0; + size_t window_end_ = 0; + std::string error_msg_; +}; +/** + * Reads a PNG from the InputStream into memory as an RGBA Image. + */ +std::unique_ptr<Image> ReadPng(InputStream* in, IDiagnostics* diag); + +/** + * Writes the RGBA Image, with optional 9-patch meta-data, into the OutputStream + * as a PNG. + */ +bool WritePng(const Image* image, const NinePatch* nine_patch, OutputStream* out, + const PngOptions& options, IDiagnostics* diag, bool verbose); +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index fdb355192676..c0514fdff469 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1875,6 +1875,7 @@ struct FabricatedOverlayEntryParameters { off64_t binary_data_offset; size_t binary_data_size; std::string configuration; + bool nine_patch; }; class AssetManager2; diff --git a/libs/androidfw/include/androidfw/Streams.h b/libs/androidfw/include/androidfw/Streams.h new file mode 100644 index 000000000000..2daf0e2fb06d --- /dev/null +++ b/libs/androidfw/include/androidfw/Streams.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include <string> +#include "android-base/off64_t.h" + +namespace android { + +// InputStream interface that mimics protobuf's ZeroCopyInputStream, +// with added error handling methods to better report issues. +class InputStream { + public: + virtual ~InputStream() = default; + + // Returns a chunk of data for reading. data and size must not be nullptr. + // Returns true so long as there is more data to read, returns false if an error occurred + // or no data remains. If an error occurred, check HadError(). + // The stream owns the buffer returned from this method and the buffer is invalidated + // anytime another mutable method is called. + virtual bool Next(const void** data, size_t* size) = 0; + + // Backup count bytes, where count is smaller or equal to the size of the last buffer returned + // from Next(). + // Useful when the last block returned from Next() wasn't fully read. + virtual void BackUp(size_t count) = 0; + + // Returns true if this InputStream can rewind. If so, Rewind() can be called. + virtual bool CanRewind() const { + return false; + }; + + // Rewinds the stream to the beginning so it can be read again. + // Returns true if the rewind succeeded. + // This does nothing if CanRewind() returns false. + virtual bool Rewind() { + return false; + } + + // Returns the number of bytes that have been read from the stream. + virtual size_t ByteCount() const = 0; + + // Returns an error message if HadError() returned true. + virtual std::string GetError() const { + return {}; + } + + // Returns true if an error occurred. Errors are permanent. + virtual bool HadError() const = 0; + + virtual bool ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) { + (void)data; + (void)byte_count; + (void)offset; + return false; + } +}; + +// A sub-InputStream interface that knows the total size of its stream. +class KnownSizeInputStream : public InputStream { + public: + virtual size_t TotalSize() const = 0; +}; + +// OutputStream interface that mimics protobuf's ZeroCopyOutputStream, +// with added error handling methods to better report issues. +class OutputStream { + public: + virtual ~OutputStream() = default; + + // Returns a buffer to which data can be written to. The data written to this buffer will + // eventually be written to the stream. Call BackUp() if the data written doesn't occupy the + // entire buffer. + // Return false if there was an error. + // The stream owns the buffer returned from this method and the buffer is invalidated + // anytime another mutable method is called. + virtual bool Next(void** data, size_t* size) = 0; + + // Backup count bytes, where count is smaller or equal to the size of the last buffer returned + // from Next(). + // Useful for when the last block returned from Next() wasn't fully written to. + virtual void BackUp(size_t count) = 0; + + // Returns the number of bytes that have been written to the stream. + virtual size_t ByteCount() const = 0; + + // Returns an error message if HadError() returned true. + virtual std::string GetError() const { + return {}; + } + + // Returns true if an error occurred. Errors are permanent. + virtual bool HadError() const = 0; +}; + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/tests/FileStream_test.cpp b/libs/androidfw/tests/FileStream_test.cpp new file mode 100644 index 000000000000..978597507a6d --- /dev/null +++ b/libs/androidfw/tests/FileStream_test.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2017 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. + */ + +#include "androidfw/FileStream.h" +#include "androidfw/StringPiece.h" + +#include "android-base/file.h" +#include "android-base/macros.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::android::StringPiece; +using ::testing::Eq; +using ::testing::NotNull; +using ::testing::StrEq; + +namespace android { + +TEST(FileInputStreamTest, NextAndBackup) { + std::string input = "this is a cool string"; + TemporaryFile file; + ASSERT_THAT(TEMP_FAILURE_RETRY(write(file.fd, input.c_str(), input.size())), Eq(21)); + lseek64(file.fd, 0, SEEK_SET); + + // Use a small buffer size so that we can call Next() a few times. + FileInputStream in(file.release(), 10u); + ASSERT_FALSE(in.HadError()); + EXPECT_THAT(in.ByteCount(), Eq(0u)); + + const void* buffer; + size_t size; + ASSERT_TRUE(in.Next(&buffer, &size)) << in.GetError(); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(in.ByteCount(), Eq(10u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("this is a ")); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(in.ByteCount(), Eq(20u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("cool strin")); + + in.BackUp(5u); + EXPECT_THAT(in.ByteCount(), Eq(15u)); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(5u)); + ASSERT_THAT(buffer, NotNull()); + ASSERT_THAT(in.ByteCount(), Eq(20u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("strin")); + + // Backup 1 more than possible. Should clamp. + in.BackUp(11u); + EXPECT_THAT(in.ByteCount(), Eq(10u)); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + ASSERT_THAT(in.ByteCount(), Eq(20u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("cool strin")); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(1u)); + ASSERT_THAT(buffer, NotNull()); + ASSERT_THAT(in.ByteCount(), Eq(21u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("g")); + + EXPECT_FALSE(in.Next(&buffer, &size)); + EXPECT_FALSE(in.HadError()); +} + +TEST(FileOutputStreamTest, NextAndBackup) { + const std::string input = "this is a cool string"; + + TemporaryFile file; + + FileOutputStream out(file.fd, 10u); + ASSERT_FALSE(out.HadError()); + EXPECT_THAT(out.ByteCount(), Eq(0u)); + + void* buffer; + size_t size; + ASSERT_TRUE(out.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(out.ByteCount(), Eq(10u)); + memcpy(reinterpret_cast<char*>(buffer), input.c_str(), size); + + ASSERT_TRUE(out.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(out.ByteCount(), Eq(20u)); + memcpy(reinterpret_cast<char*>(buffer), input.c_str() + 10u, size); + + ASSERT_TRUE(out.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(out.ByteCount(), Eq(30u)); + reinterpret_cast<char*>(buffer)[0] = input[20u]; + out.BackUp(size - 1); + EXPECT_THAT(out.ByteCount(), Eq(21u)); + + ASSERT_TRUE(out.Flush()); + + lseek64(file.fd, 0, SEEK_SET); + + std::string actual; + ASSERT_TRUE(android::base::ReadFdToString(file.fd, &actual)); + EXPECT_THAT(actual, StrEq(input)); +} + +} // namespace android diff --git a/libs/androidfw/tests/NinePatch_test.cpp b/libs/androidfw/tests/NinePatch_test.cpp new file mode 100644 index 000000000000..7ee8e9ebd624 --- /dev/null +++ b/libs/androidfw/tests/NinePatch_test.cpp @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2016 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. + */ + +#include "androidfw/Image.h" +#include "androidfw/ResourceTypes.h" +#include "gtest/gtest.h" + +namespace android { + +// Pixels are in RGBA_8888 packing. + +#define RED "\xff\x00\x00\xff" +#define BLUE "\x00\x00\xff\xff" +#define GREEN "\xff\x00\x00\xff" +#define GR_70 "\xff\x00\x00\xb3" +#define GR_50 "\xff\x00\x00\x80" +#define GR_20 "\xff\x00\x00\x33" +#define BLACK "\x00\x00\x00\xff" +#define WHITE "\xff\xff\xff\xff" +#define TRANS "\x00\x00\x00\x00" + +static uint8_t* k2x2[] = { + (uint8_t*)WHITE WHITE, + (uint8_t*)WHITE WHITE, +}; + +static uint8_t* kMixedNeutralColor3x3[] = { + (uint8_t*)WHITE BLACK TRANS, + (uint8_t*)TRANS RED TRANS, + (uint8_t*)WHITE WHITE WHITE, +}; + +static uint8_t* kTransparentNeutralColor3x3[] = { + (uint8_t*)TRANS BLACK TRANS, + (uint8_t*)BLACK RED BLACK, + (uint8_t*)TRANS BLACK TRANS, +}; + +static uint8_t* kSingleStretch7x6[] = { + (uint8_t*)WHITE WHITE BLACK BLACK BLACK WHITE WHITE, + (uint8_t*)WHITE RED RED RED RED RED WHITE, + (uint8_t*)BLACK RED RED RED RED RED WHITE, + (uint8_t*)BLACK RED RED RED RED RED WHITE, + (uint8_t*)WHITE RED RED RED RED RED WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kMultipleStretch10x7[] = { + (uint8_t*)WHITE WHITE BLACK WHITE BLACK BLACK WHITE BLACK WHITE WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)WHITE RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kPadding6x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE BLACK, (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE BLACK BLACK WHITE WHITE, +}; + +static uint8_t* kLayoutBoundsWrongEdge3x3[] = { + (uint8_t*)WHITE RED WHITE, + (uint8_t*)RED WHITE WHITE, + (uint8_t*)WHITE WHITE WHITE, +}; + +static uint8_t* kLayoutBoundsNotEdgeAligned5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE RED, (uint8_t*)WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE RED WHITE WHITE, +}; + +static uint8_t* kLayoutBounds5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE RED WHITE RED WHITE, +}; + +static uint8_t* kAsymmetricLayoutBounds5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE RED WHITE WHITE WHITE, +}; + +static uint8_t* kPaddingAndLayoutBounds5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE WHITE WHITE WHITE BLACK, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE RED BLACK RED WHITE, +}; + +static uint8_t* kColorfulImage5x5[] = { + (uint8_t*)WHITE BLACK WHITE BLACK WHITE, (uint8_t*)BLACK RED BLUE GREEN WHITE, + (uint8_t*)BLACK RED GREEN GREEN WHITE, (uint8_t*)WHITE TRANS BLUE GREEN WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineOpaque10x10[] = { + (uint8_t*)WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineTranslucent10x10[] = { + (uint8_t*)WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineOffsetTranslucent12x10[] = { + (uint8_t*)WHITE WHITE WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineRadius5x5[] = { + (uint8_t*)WHITE BLACK BLACK BLACK WHITE, (uint8_t*)BLACK TRANS GREEN TRANS WHITE, + (uint8_t*)BLACK GREEN GREEN GREEN WHITE, (uint8_t*)BLACK TRANS GREEN TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kStretchAndPadding5x5[] = { + (uint8_t*)WHITE WHITE BLACK WHITE WHITE, (uint8_t*)WHITE RED RED RED WHITE, + (uint8_t*)BLACK RED RED RED BLACK, (uint8_t*)WHITE RED RED RED WHITE, + (uint8_t*)WHITE WHITE BLACK WHITE WHITE, +}; + +TEST(NinePatchTest, Minimum3x3) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(k2x2, 2, 2, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, MixedNeutralColors) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(kMixedNeutralColor3x3, 3, 3, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, TransparentNeutralColor) { + std::string err; + EXPECT_NE(nullptr, NinePatch::Create(kTransparentNeutralColor3x3, 3, 3, &err)); +} + +TEST(NinePatchTest, SingleStretchRegion) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kSingleStretch7x6, 7, 6, &err); + ASSERT_NE(nullptr, nine_patch); + + ASSERT_EQ(1u, nine_patch->horizontal_stretch_regions.size()); + ASSERT_EQ(1u, nine_patch->vertical_stretch_regions.size()); + + EXPECT_EQ(Range(1, 4), nine_patch->horizontal_stretch_regions.front()); + EXPECT_EQ(Range(1, 3), nine_patch->vertical_stretch_regions.front()); +} + +TEST(NinePatchTest, MultipleStretchRegions) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kMultipleStretch10x7, 10, 7, &err); + ASSERT_NE(nullptr, nine_patch); + + ASSERT_EQ(3u, nine_patch->horizontal_stretch_regions.size()); + ASSERT_EQ(2u, nine_patch->vertical_stretch_regions.size()); + + EXPECT_EQ(Range(1, 2), nine_patch->horizontal_stretch_regions[0]); + EXPECT_EQ(Range(3, 5), nine_patch->horizontal_stretch_regions[1]); + EXPECT_EQ(Range(6, 7), nine_patch->horizontal_stretch_regions[2]); + + EXPECT_EQ(Range(0, 2), nine_patch->vertical_stretch_regions[0]); + EXPECT_EQ(Range(3, 5), nine_patch->vertical_stretch_regions[1]); +} + +TEST(NinePatchTest, InferPaddingFromStretchRegions) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kMultipleStretch10x7, 10, 7, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 0, 1, 0), nine_patch->padding); +} + +TEST(NinePatchTest, Padding) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kPadding6x5, 6, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->padding); +} + +TEST(NinePatchTest, LayoutBoundsAreOnWrongEdge) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(kLayoutBoundsWrongEdge3x3, 3, 3, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, LayoutBoundsMustTouchEdges) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(kLayoutBoundsNotEdgeAligned5x5, 5, 5, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, LayoutBounds) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kLayoutBounds5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->layout_bounds); + + nine_patch = NinePatch::Create(kAsymmetricLayoutBounds5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 0, 0), nine_patch->layout_bounds); +} + +TEST(NinePatchTest, PaddingAndLayoutBounds) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kPaddingAndLayoutBounds5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->padding); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->layout_bounds); +} + +TEST(NinePatchTest, RegionColorsAreCorrect) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kColorfulImage5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + + std::vector<uint32_t> expected_colors = { + NinePatch::PackRGBA((uint8_t*)RED), (uint32_t)android::Res_png_9patch::NO_COLOR, + NinePatch::PackRGBA((uint8_t*)GREEN), (uint32_t)android::Res_png_9patch::TRANSPARENT_COLOR, + NinePatch::PackRGBA((uint8_t*)BLUE), NinePatch::PackRGBA((uint8_t*)GREEN), + }; + EXPECT_EQ(expected_colors, nine_patch->region_colors); +} + +TEST(NinePatchTest, OutlineFromOpaqueImage) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kOutlineOpaque10x10, 10, 10, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(2, 2, 2, 2), nine_patch->outline); + EXPECT_EQ(0x000000ffu, nine_patch->outline_alpha); + EXPECT_EQ(0.0f, nine_patch->outline_radius); +} + +TEST(NinePatchTest, OutlineFromTranslucentImage) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kOutlineTranslucent10x10, 10, 10, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(3, 3, 3, 3), nine_patch->outline); + EXPECT_EQ(0x000000b3u, nine_patch->outline_alpha); + EXPECT_EQ(0.0f, nine_patch->outline_radius); +} + +TEST(NinePatchTest, OutlineFromOffCenterImage) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = + NinePatch::Create(kOutlineOffsetTranslucent12x10, 12, 10, &err); + ASSERT_NE(nullptr, nine_patch); + + // TODO(adamlesinski): The old AAPT algorithm searches from the outside to the + // middle for each inset. If the outline is shifted, the search may not find a + // closer bounds. + // This check should be: + // EXPECT_EQ(Bounds(5, 3, 3, 3), ninePatch->outline); + // but until I know what behavior I'm breaking, I will leave it at the + // incorrect: + EXPECT_EQ(Bounds(4, 3, 3, 3), nine_patch->outline); + + EXPECT_EQ(0x000000b3u, nine_patch->outline_alpha); + EXPECT_EQ(0.0f, nine_patch->outline_radius); +} + +TEST(NinePatchTest, OutlineRadius) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kOutlineRadius5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(0, 0, 0, 0), nine_patch->outline); + EXPECT_EQ(3.4142f, nine_patch->outline_radius); +} + +::testing::AssertionResult BigEndianOne(uint8_t* cursor) { + if (cursor[0] == 0 && cursor[1] == 0 && cursor[2] == 0 && cursor[3] == 1) { + return ::testing::AssertionSuccess(); + } + return ::testing::AssertionFailure() << "Not BigEndian 1"; +} + +TEST(NinePatchTest, SerializePngEndianness) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kStretchAndPadding5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + + size_t len; + std::unique_ptr<uint8_t[]> data = nine_patch->SerializeBase(&len); + ASSERT_NE(nullptr, data); + ASSERT_NE(0u, len); + + // Skip past wasDeserialized + numXDivs + numYDivs + numColors + xDivsOffset + + // yDivsOffset + // (12 bytes) + uint8_t* cursor = data.get() + 12; + + // Check that padding is big-endian. Expecting value 1. + EXPECT_TRUE(BigEndianOne(cursor)); + EXPECT_TRUE(BigEndianOne(cursor + 4)); + EXPECT_TRUE(BigEndianOne(cursor + 8)); + EXPECT_TRUE(BigEndianOne(cursor + 12)); +} + +} // namespace android diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index da728f90e8e0..79a735786c38 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -553,6 +553,7 @@ cc_defaults { "utils/Blur.cpp", "utils/Color.cpp", "utils/LinearAllocator.cpp", + "utils/TypefaceUtils.cpp", "utils/VectorDrawableUtils.cpp", "AnimationContext.cpp", "Animator.cpp", diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp index cd4fae86aa52..b667daf9c850 100644 --- a/libs/hwui/CanvasTransform.cpp +++ b/libs/hwui/CanvasTransform.cpp @@ -80,6 +80,19 @@ SkColor transformColorInverse(ColorTransform transform, SkColor color) { static void applyColorTransform(ColorTransform transform, SkPaint& paint) { if (transform == ColorTransform::None) return; + if (transform == ColorTransform::Invert) { + auto filter = SkHighContrastFilter::Make( + {/* grayscale= */ false, SkHighContrastConfig::InvertStyle::kInvertLightness, + /* contrast= */ 0.0f}); + + if (paint.getColorFilter()) { + paint.setColorFilter(SkColorFilters::Compose(filter, paint.refColorFilter())); + } else { + paint.setColorFilter(filter); + } + return; + } + SkColor newColor = transformColor(transform, paint.getColor()); paint.setColor(newColor); diff --git a/libs/hwui/CanvasTransform.h b/libs/hwui/CanvasTransform.h index 291f4cf7193b..288dca4de5c1 100644 --- a/libs/hwui/CanvasTransform.h +++ b/libs/hwui/CanvasTransform.h @@ -29,12 +29,15 @@ enum class UsageHint { Unknown = 0, Background = 1, Foreground = 2, + // Contains foreground (usually text), like a button or chip + Container = 3 }; enum class ColorTransform { None, Light, Dark, + Invert }; // True if the paint was modified, false otherwise diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h index 8c180da9c84f..b1c5bf49ede5 100644 --- a/libs/hwui/DisplayList.h +++ b/libs/hwui/DisplayList.h @@ -145,6 +145,8 @@ public: return mImpl && mImpl->hasText(); } + [[nodiscard]] bool hasFill() const { return mImpl && mImpl->hasFill(); } + void applyColorTransform(ColorTransform transform) { if (mImpl) { mImpl->applyColorTransform(transform); diff --git a/libs/hwui/Mesh.cpp b/libs/hwui/Mesh.cpp index e59bc9565a59..37a7d74330e9 100644 --- a/libs/hwui/Mesh.cpp +++ b/libs/hwui/Mesh.cpp @@ -90,8 +90,8 @@ std::tuple<bool, SkString> Mesh::validate() { FAIL_MESH_VALIDATE("%s mode requires at least %zu vertices but vertex count is %zu.", modeToStr(meshMode), min_vcount_for_mode(meshMode), mVertexCount); } - SkASSERT(!fICount); - SkASSERT(!fIOffset); + LOG_ALWAYS_FATAL_IF(mIndexCount != 0); + LOG_ALWAYS_FATAL_IF(mIndexOffset != 0); } if (!sm.ok()) { diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index ff0d8d74831c..3b694c5d399b 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -718,6 +718,27 @@ static constexpr inline bool is_power_of_two(int value) { return (value & (value - 1)) == 0; } +template <typename T> +constexpr bool doesPaintHaveFill(T& paint) { + using T1 = std::remove_cv_t<T>; + if constexpr (std::is_same_v<T1, SkPaint>) { + return paint.getStyle() != SkPaint::Style::kStroke_Style; + } else if constexpr (std::is_same_v<T1, SkPaint&>) { + return paint.getStyle() != SkPaint::Style::kStroke_Style; + } else if constexpr (std::is_same_v<T1, SkPaint*>) { + return paint && paint->getStyle() != SkPaint::Style::kStroke_Style; + } else if constexpr (std::is_same_v<T1, const SkPaint*>) { + return paint && paint->getStyle() != SkPaint::Style::kStroke_Style; + } + + return false; +} + +template <typename... Args> +constexpr bool hasPaintWithFill(Args&&... args) { + return (... || doesPaintHaveFill(args)); +} + template <typename T, typename... Args> void* DisplayListData::push(size_t pod, Args&&... args) { size_t skip = SkAlignPtr(sizeof(T) + pod); @@ -736,6 +757,14 @@ void* DisplayListData::push(size_t pod, Args&&... args) { new (op) T{std::forward<Args>(args)...}; op->type = (uint32_t)T::kType; op->skip = skip; + + // check if this is a fill op or not, in case we need to avoid messing with it with force invert + if constexpr (!std::is_same_v<T, DrawTextBlob>) { + if (hasPaintWithFill(args...)) { + mHasFill = true; + } + } + return op + 1; } diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 4f54ee286a56..afadbfda7471 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -110,7 +110,7 @@ class RecordingCanvas; class DisplayListData final { public: - DisplayListData() : mHasText(false) {} + DisplayListData() : mHasText(false), mHasFill(false) {} ~DisplayListData(); void draw(SkCanvas* canvas) const; @@ -121,6 +121,7 @@ public: void applyColorTransform(ColorTransform transform); bool hasText() const { return mHasText; } + bool hasFill() const { return mHasFill; } size_t usedSize() const { return fUsed; } size_t allocatedSize() const { return fReserved; } @@ -192,6 +193,7 @@ private: size_t fReserved = 0; bool mHasText : 1; + bool mHasFill : 1; }; class RecordingCanvas final : public SkCanvasVirtualEnforcer<SkNoDrawCanvas> { diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 3e131bc44d39..0b42c88aa448 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -427,7 +427,13 @@ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { children.push_back(node); }); if (mDisplayList.hasText()) { - usage = UsageHint::Foreground; + if (mDisplayList.hasFill()) { + // Handle a special case for custom views that draw both text and background in the + // same RenderNode, which would otherwise be altered to white-on-white text. + usage = UsageHint::Container; + } else { + usage = UsageHint::Foreground; + } } if (usage == UsageHint::Unknown) { if (children.size() > 1) { @@ -453,8 +459,13 @@ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { drawn.join(bounds); } } - mDisplayList.applyColorTransform( - usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light); + + if (usage == UsageHint::Container) { + mDisplayList.applyColorTransform(ColorTransform::Invert); + } else { + mDisplayList.applyColorTransform(usage == UsageHint::Background ? ColorTransform::Dark + : ColorTransform::Light); + } } void RenderNode::pushStagingDisplayListChanges(TreeObserver& observer, TreeInfo& info) { diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 31fc929dfcdf..008ea3aaa2e7 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -589,10 +589,40 @@ void SkiaCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Pain // Canvas draw operations: Bitmaps // ---------------------------------------------------------------------------- +bool SkiaCanvas::useGainmapShader(Bitmap& bitmap) { + // If the bitmap doesn't have a gainmap, don't use the gainmap shader + if (!bitmap.hasGainmap()) return false; + + // If we don't have an owned canvas, then we're either hardware accelerated or drawing + // to a picture - use the gainmap shader out of caution. Ideally a picture canvas would + // use a drawable here instead to defer making that decision until the last possible + // moment + if (!mCanvasOwned) return true; + + auto info = mCanvasOwned->imageInfo(); + + // If it's an unknown colortype then it's not a bitmap-backed canvas + if (info.colorType() == SkColorType::kUnknown_SkColorType) return true; + + skcms_TransferFunction tfn; + info.colorSpace()->transferFn(&tfn); + + auto transferType = skcms_TransferFunction_getType(&tfn); + switch (transferType) { + case skcms_TFType_HLGish: + case skcms_TFType_HLGinvish: + case skcms_TFType_PQish: + return true; + case skcms_TFType_Invalid: + case skcms_TFType_sRGBish: + return false; + } +} + void SkiaCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) { auto image = bitmap.makeImage(); - if (bitmap.hasGainmap()) { + if (useGainmapShader(bitmap)) { Paint gainmapPaint = paint ? *paint : Paint(); sk_sp<SkShader> gainmapShader = uirenderer::MakeGainmapShader( image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info, @@ -619,7 +649,7 @@ void SkiaCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float s SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - if (bitmap.hasGainmap()) { + if (useGainmapShader(bitmap)) { Paint gainmapPaint = paint ? *paint : Paint(); sk_sp<SkShader> gainmapShader = uirenderer::MakeGainmapShader( image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info, diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index ced02241ffe2..4bf1790e2415 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -223,6 +223,8 @@ private: void drawPoints(const float* points, int count, const Paint& paint, SkCanvas::PointMode mode); + bool useGainmapShader(Bitmap& bitmap); + class Clip; std::unique_ptr<SkCanvas> mCanvasOwned; // Might own a canvas we allocated. diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 78a64795967a..ca119757e816 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -1,6 +1,13 @@ package: "com.android.graphics.hwui.flags" flag { + name: "matrix_44" + namespace: "core_graphics" + description: "API for 4x4 matrix and related canvas functions" + bug: "280116960" +} + +flag { name: "limited_hdr" namespace: "core_graphics" description: "API to enable apps to restrict the amount of HDR headroom that is used" @@ -41,3 +48,10 @@ flag { description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK" bug: "292545615" } + +flag { + name: "animate_hdr_transitions" + namespace: "core_graphics" + description: "Automatically animate all changes in HDR headroom" + bug: "314810174" +} diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index 34cb4aef70d9..f4ee36ec66d1 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -30,6 +30,7 @@ #include <minikin/MinikinExtent.h> #include <minikin/MinikinPaint.h> #include <minikin/MinikinRect.h> +#include <utils/TypefaceUtils.h> namespace android { @@ -142,7 +143,7 @@ std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( skVariation[i].value = SkFloatToScalar(variations[i].value); } args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); - sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); + sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), args)); return std::make_shared<MinikinFontSkia>(std::move(face), mSourceId, mFontData, mFontSize, diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h index caffdfc907f7..ef4dce57bf46 100644 --- a/libs/hwui/hwui/Paint.h +++ b/libs/hwui/hwui/Paint.h @@ -17,19 +17,19 @@ #ifndef ANDROID_GRAPHICS_PAINT_H_ #define ANDROID_GRAPHICS_PAINT_H_ -#include "Typeface.h" - -#include <cutils/compiler.h> - #include <SkFont.h> #include <SkPaint.h> #include <SkSamplingOptions.h> -#include <string> - -#include <minikin/FontFamily.h> +#include <cutils/compiler.h> #include <minikin/FamilyVariant.h> +#include <minikin/FontFamily.h> +#include <minikin/FontFeature.h> #include <minikin/Hyphenator.h> +#include <string> + +#include "Typeface.h" + namespace android { class BlurDrawLooper; @@ -82,11 +82,15 @@ public: float getWordSpacing() const { return mWordSpacing; } - void setFontFeatureSettings(const std::string& fontFeatureSettings) { - mFontFeatureSettings = fontFeatureSettings; + void setFontFeatureSettings(std::string_view fontFeatures) { + mFontFeatureSettings = minikin::FontFeature::parse(fontFeatures); } - std::string getFontFeatureSettings() const { return mFontFeatureSettings; } + void resetFontFeatures() { mFontFeatureSettings.clear(); } + + const std::vector<minikin::FontFeature>& getFontFeatureSettings() const { + return mFontFeatureSettings; + } void setMinikinLocaleListId(uint32_t minikinLocaleListId) { mMinikinLocaleListId = minikinLocaleListId; @@ -170,7 +174,7 @@ private: float mLetterSpacing = 0; float mWordSpacing = 0; - std::string mFontFeatureSettings; + std::vector<minikin::FontFeature> mFontFeatureSettings; uint32_t mMinikinLocaleListId; std::optional<minikin::FamilyVariant> mFamilyVariant; uint32_t mHyphenEdit = 0; diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index b63ee1bd3d98..a9d1a2aed8cc 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -25,8 +25,9 @@ #include "MinikinSkia.h" #include "SkPaint.h" -#include "SkStream.h" // Fot tests. +#include "SkStream.h" // For tests. #include "SkTypeface.h" +#include "utils/TypefaceUtils.h" #include <minikin/FontCollection.h> #include <minikin/FontFamily.h> @@ -186,7 +187,9 @@ void Typeface::setRobotoTypefaceForTest() { LOG_ALWAYS_FATAL_IF(fstat(fd, &st) == -1, "Failed to stat file %s", kRobotoFont); void* data = mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0); std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(data, st.st_size)); - sk_sp<SkTypeface> typeface = SkTypeface::MakeFromStream(std::move(fontData)); + sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); + LOG_ALWAYS_FATAL_IF(fm == nullptr, "Could not load FreeType SkFontMgr"); + sk_sp<SkTypeface> typeface = fm->makeFromStream(std::move(fontData)); LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", kRobotoFont); std::shared_ptr<minikin::MinikinFont> font = diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index 0c3af61fc089..e6d790f56d0f 100644 --- a/libs/hwui/jni/FontFamily.cpp +++ b/libs/hwui/jni/FontFamily.cpp @@ -31,6 +31,7 @@ #include <minikin/FontFamily.h> #include <minikin/LocaleList.h> #include <ui/FatVector.h> +#include <utils/TypefaceUtils.h> #include <memory> @@ -125,7 +126,7 @@ static bool addSkTypeface(NativeFamilyBuilder* builder, sk_sp<SkData>&& data, in args.setCollectionIndex(ttcIndex); args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); - sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); + sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args)); if (face == NULL) { ALOGE("addFont failed to create font, invalid request"); diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index 8c71d6fc7860..d84b73d1a1ca 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -33,6 +33,7 @@ #include <cassert> #include <cstring> #include <memory> +#include <string_view> #include <vector> #include "ColorFilter.h" @@ -690,10 +691,11 @@ namespace PaintGlue { jstring settings) { Paint* paint = reinterpret_cast<Paint*>(paintHandle); if (!settings) { - paint->setFontFeatureSettings(std::string()); + paint->resetFontFeatures(); } else { ScopedUtfChars settingsChars(env, settings); - paint->setFontFeatureSettings(std::string(settingsChars.c_str(), settingsChars.size())); + paint->setFontFeatureSettings( + std::string_view(settingsChars.c_str(), settingsChars.size())); } } diff --git a/libs/hwui/jni/YuvToJpegEncoder.cpp b/libs/hwui/jni/YuvToJpegEncoder.cpp index 4dbfa88d6301..353300186555 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.cpp +++ b/libs/hwui/jni/YuvToJpegEncoder.cpp @@ -2,6 +2,7 @@ #include "SkStream.h" #include "YuvToJpegEncoder.h" #include <ui/PixelFormat.h> +#include <utils/Errors.h> #include <hardware/hardware.h> #include "graphics_jni_helpers.h" @@ -295,7 +296,7 @@ void Yuv422IToJpegEncoder::configSamplingFactors(jpeg_compress_struct* cinfo) { } /////////////////////////////////////////////////////////////////////////////// -using namespace android::ultrahdr; +using namespace ultrahdr; ultrahdr_color_gamut P010Yuv420ToJpegREncoder::findColorGamut(JNIEnv* env, int aDataSpace) { switch (aDataSpace & ADataSpace::STANDARD_MASK) { @@ -332,7 +333,8 @@ ultrahdr_transfer_function P010Yuv420ToJpegREncoder::findHdrTransferFunction(JNI bool P010Yuv420ToJpegREncoder::encode(JNIEnv* env, SkWStream* stream, void* hdr, int hdrColorSpace, void* sdr, int sdrColorSpace, - int width, int height, int jpegQuality) { + int width, int height, int jpegQuality, ScopedByteArrayRO* jExif, + ScopedIntArrayRO* jHdrStrides, ScopedIntArrayRO* jSdrStrides) { // Check SDR color space. Now we only support SRGB transfer function if ((sdrColorSpace & ADataSpace::TRANSFER_MASK) != ADataSpace::TRANSFER_SRGB) { jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException"); @@ -340,6 +342,19 @@ bool P010Yuv420ToJpegREncoder::encode(JNIEnv* env, "The requested SDR color space is not supported. Transfer function must be SRGB"); return false; } + // Check HDR and SDR strides length. + // HDR is YCBCR_P010 color format, and its strides length must be 2 (Y, chroma (Cb, Cr)). + // SDR is YUV_420_888 color format, and its strides length must be 3 (Y, Cb, Cr). + if (jHdrStrides->size() != 2) { + jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException"); + env->ThrowNew(IllegalArgumentException, "HDR stride length must be 2."); + return false; + } + if (jSdrStrides->size() != 3) { + jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException"); + env->ThrowNew(IllegalArgumentException, "SDR stride length must be 3."); + return false; + } ultrahdr_color_gamut hdrColorGamut = findColorGamut(env, hdrColorSpace); ultrahdr_color_gamut sdrColorGamut = findColorGamut(env, sdrColorSpace); @@ -351,20 +366,32 @@ bool P010Yuv420ToJpegREncoder::encode(JNIEnv* env, return false; } + const int* hdrStrides = reinterpret_cast<const int*>(jHdrStrides->get()); + const int* sdrStrides = reinterpret_cast<const int*>(jSdrStrides->get()); + JpegR jpegREncoder; jpegr_uncompressed_struct p010; p010.data = hdr; p010.width = width; p010.height = height; + // Divided by 2 because unit in libultrader is pixel and in YuvImage it is byte. + p010.luma_stride = (hdrStrides[0] + 1) / 2; + p010.chroma_stride = (hdrStrides[1] + 1) / 2; p010.colorGamut = hdrColorGamut; jpegr_uncompressed_struct yuv420; yuv420.data = sdr; yuv420.width = width; yuv420.height = height; + yuv420.luma_stride = sdrStrides[0]; + yuv420.chroma_stride = sdrStrides[1]; yuv420.colorGamut = sdrColorGamut; + jpegr_exif_struct exif; + exif.data = const_cast<void*>(reinterpret_cast<const void*>(jExif->get())); + exif.length = jExif->size(); + jpegr_compressed_struct jpegR; jpegR.maxLength = width * height * sizeof(uint8_t); @@ -373,7 +400,8 @@ bool P010Yuv420ToJpegREncoder::encode(JNIEnv* env, if (int success = jpegREncoder.encodeJPEGR(&p010, &yuv420, hdrTransferFunction, - &jpegR, jpegQuality, nullptr); success != android::OK) { + &jpegR, jpegQuality, + exif.length > 0 ? &exif : NULL); success != JPEGR_NO_ERROR) { ALOGW("Encode JPEG/R failed, error code: %d.", success); return false; } @@ -415,20 +443,27 @@ static jboolean YuvImage_compressToJpeg(JNIEnv* env, jobject, jbyteArray inYuv, static jboolean YuvImage_compressToJpegR(JNIEnv* env, jobject, jbyteArray inHdr, jint hdrColorSpace, jbyteArray inSdr, jint sdrColorSpace, jint width, jint height, jint quality, jobject jstream, - jbyteArray jstorage) { + jbyteArray jstorage, jbyteArray jExif, + jintArray jHdrStrides, jintArray jSdrStrides) { jbyte* hdr = env->GetByteArrayElements(inHdr, NULL); jbyte* sdr = env->GetByteArrayElements(inSdr, NULL); + ScopedByteArrayRO exif(env, jExif); + ScopedIntArrayRO hdrStrides(env, jHdrStrides); + ScopedIntArrayRO sdrStrides(env, jSdrStrides); + SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage); P010Yuv420ToJpegREncoder encoder; jboolean result = JNI_FALSE; if (encoder.encode(env, strm, hdr, hdrColorSpace, sdr, sdrColorSpace, - width, height, quality)) { + width, height, quality, &exif, + &hdrStrides, &sdrStrides)) { result = JNI_TRUE; } env->ReleaseByteArrayElements(inHdr, hdr, 0); env->ReleaseByteArrayElements(inSdr, sdr, 0); + delete strm; return result; } @@ -437,7 +472,7 @@ static jboolean YuvImage_compressToJpegR(JNIEnv* env, jobject, jbyteArray inHdr, static const JNINativeMethod gYuvImageMethods[] = { { "nativeCompressToJpeg", "([BIII[I[IILjava/io/OutputStream;[B)Z", (void*)YuvImage_compressToJpeg }, - { "nativeCompressToJpegR", "([BI[BIIIILjava/io/OutputStream;[B)Z", + { "nativeCompressToJpegR", "([BI[BIIIILjava/io/OutputStream;[B[B[I[I)Z", (void*)YuvImage_compressToJpegR } }; diff --git a/libs/hwui/jni/YuvToJpegEncoder.h b/libs/hwui/jni/YuvToJpegEncoder.h index 8ef780547184..629f1e64726b 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.h +++ b/libs/hwui/jni/YuvToJpegEncoder.h @@ -2,6 +2,7 @@ #define _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ #include <android/data_space.h> +#include <nativehelper/ScopedPrimitiveArray.h> #include <ultrahdr/jpegr.h> extern "C" { @@ -90,11 +91,15 @@ public: * @param width Width of the Yuv data in terms of pixels. * @param height Height of the Yuv data in terms of pixels. * @param jpegQuality Picture quality in [0, 100]. + * @param exif Buffer holds EXIF package. + * @param hdrStrides The number of row bytes in each image plane of the HDR input. + * @param sdrStrides The number of row bytes in each image plane of the SDR input. * @return true if successfully compressed the stream. */ bool encode(JNIEnv* env, SkWStream* stream, void* hdr, int hdrColorSpace, void* sdr, int sdrColorSpace, - int width, int height, int jpegQuality); + int width, int height, int jpegQuality, ScopedByteArrayRO* exif, + ScopedIntArrayRO* hdrStrides, ScopedIntArrayRO* sdrStrides); /** Map data space (defined in DataSpace.java and data_space.h) to the color gamut * used in JPEG/R @@ -103,7 +108,7 @@ public: * @param aDataSpace data space defined in data_space.h. * @return color gamut for JPEG/R. */ - static android::ultrahdr::ultrahdr_color_gamut findColorGamut(JNIEnv* env, int aDataSpace); + static ultrahdr::ultrahdr_color_gamut findColorGamut(JNIEnv* env, int aDataSpace); /** Map data space (defined in DataSpace.java and data_space.h) to the transfer function * used in JPEG/R @@ -112,8 +117,8 @@ public: * @param aDataSpace data space defined in data_space.h. * @return color gamut for JPEG/R. */ - static android::ultrahdr::ultrahdr_transfer_function findHdrTransferFunction( - JNIEnv* env, int aDataSpace); + static ultrahdr::ultrahdr_transfer_function findHdrTransferFunction(JNIEnv* env, + int aDataSpace); }; #endif // _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index 2ec94c954fe9..f405abaaf5b4 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -37,6 +37,7 @@ #include <minikin/LocaleList.h> #include <minikin/SystemFonts.h> #include <ui/FatVector.h> +#include <utils/TypefaceUtils.h> #include <memory> @@ -459,7 +460,7 @@ std::shared_ptr<minikin::MinikinFont> createMinikinFontSkia( args.setCollectionIndex(ttcIndex); args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); - sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); + sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args)); if (face == nullptr) { return nullptr; diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 2b2e3995d17e..ffa915ad968c 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -56,6 +56,7 @@ void RenderNodeDrawable::drawBackwardsProjectedNodes(SkCanvas* canvas, int nestLevel) const { LOG_ALWAYS_FATAL_IF(0 == nestLevel && !displayList.mProjectionReceiver); for (auto& child : displayList.mChildNodes) { + if (!child.getRenderNode()->isRenderable()) continue; const RenderProperties& childProperties = child.getNodeProperties(); // immediate children cannot be projected on their parent diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index e5bd5c9b2a3b..b9dc1c49f09e 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -96,6 +96,8 @@ public: bool hasText() const { return mDisplayList.hasText(); } + bool hasFill() const { return mDisplayList.hasFill(); } + /** * Attempts to reset and reuse this DisplayList. * diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 12cb69da772b..d74748936d15 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -105,7 +105,7 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( ProfileType::None != Properties::getProfileType())) { SkCanvas* profileCanvas = backBuffer->getCanvas(); SkAutoCanvasRestore saver(profileCanvas, true); - profileCanvas->concat(mVkSurface->getCurrentPreTransform()); + profileCanvas->concat(preTransform); SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height()); profiler->draw(profileRenderer); } diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp index 6df34bee2224..64d38b9ef466 100644 --- a/libs/hwui/renderthread/ReliableSurface.cpp +++ b/libs/hwui/renderthread/ReliableSurface.cpp @@ -150,11 +150,11 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) { } AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{ - .usage = mUsage, - .format = mFormat, .width = 1, .height = 1, .layers = 1, + .format = mFormat, + .usage = mUsage, .rfu0 = 0, .rfu1 = 0, }; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index c3c136feef69..eab36050896f 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -123,7 +123,7 @@ void RenderProxy::setSurfaceControl(ASurfaceControl* surfaceControl) { } void RenderProxy::allocateBuffers() { - mRenderThread.queue().post([=]() { mContext->allocateBuffers(); }); + mRenderThread.queue().post([this]() { mContext->allocateBuffers(); }); } bool RenderProxy::pause() { @@ -136,15 +136,16 @@ void RenderProxy::setStopped(bool stopped) { void RenderProxy::setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha) { mRenderThread.queue().post( - [=]() { mContext->setLightAlpha(ambientShadowAlpha, spotShadowAlpha); }); + [=, this]() { mContext->setLightAlpha(ambientShadowAlpha, spotShadowAlpha); }); } void RenderProxy::setLightGeometry(const Vector3& lightCenter, float lightRadius) { - mRenderThread.queue().post([=]() { mContext->setLightGeometry(lightCenter, lightRadius); }); + mRenderThread.queue().post( + [=, this]() { mContext->setLightGeometry(lightCenter, lightRadius); }); } void RenderProxy::setOpaque(bool opaque) { - mRenderThread.queue().post([=]() { mContext->setOpaque(opaque); }); + mRenderThread.queue().post([=, this]() { mContext->setOpaque(opaque); }); } float RenderProxy::setColorMode(ColorMode mode) { @@ -152,9 +153,9 @@ float RenderProxy::setColorMode(ColorMode mode) { // an async call since we already know the return value if (mode == ColorMode::Hdr || mode == ColorMode::Hdr10) { return mRenderThread.queue().runSync( - [=]() -> float { return mContext->setColorMode(mode); }); + [=, this]() -> float { return mContext->setColorMode(mode); }); } else { - mRenderThread.queue().post([=]() { mContext->setColorMode(mode); }); + mRenderThread.queue().post([=, this]() { mContext->setColorMode(mode); }); return 1.f; } } @@ -179,7 +180,7 @@ void RenderProxy::destroy() { // destroyCanvasAndSurface() needs a fence as when it returns the // underlying BufferQueue is going to be released from under // the render thread. - mRenderThread.queue().runSync([=]() { mContext->destroy(); }); + mRenderThread.queue().runSync([this]() { mContext->destroy(); }); } void RenderProxy::destroyFunctor(int functor) { @@ -300,7 +301,7 @@ void RenderProxy::dumpProfileInfo(int fd, int dumpFlags) { } void RenderProxy::resetProfileInfo() { - mRenderThread.queue().runSync([=]() { + mRenderThread.queue().runSync([this]() { std::lock_guard lock(mRenderThread.getJankDataMutex()); mContext->resetFrameStats(); }); @@ -355,15 +356,15 @@ int RenderProxy::getRenderThreadTid() { } void RenderProxy::addRenderNode(RenderNode* node, bool placeFront) { - mRenderThread.queue().post([=]() { mContext->addRenderNode(node, placeFront); }); + mRenderThread.queue().post([=, this]() { mContext->addRenderNode(node, placeFront); }); } void RenderProxy::removeRenderNode(RenderNode* node) { - mRenderThread.queue().post([=]() { mContext->removeRenderNode(node); }); + mRenderThread.queue().post([=, this]() { mContext->removeRenderNode(node); }); } void RenderProxy::drawRenderNode(RenderNode* node) { - mRenderThread.queue().runSync([=]() { mContext->prepareAndDraw(node); }); + mRenderThread.queue().runSync([=, this]() { mContext->prepareAndDraw(node); }); } void RenderProxy::setContentDrawBounds(int left, int top, int right, int bottom) { diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index f76ea063842f..623ee4e6c27e 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -150,7 +150,7 @@ void RenderThread::frameCallback(int64_t vsyncId, int64_t frameDeadline, int64_t ATRACE_FORMAT("queue mFrameCallbackTask to run after %.2fms", toFloatMillis(runAt - SteadyClock::now()).count()); queue().postAt(toNsecs_t(runAt.time_since_epoch()).count(), - [=]() { dispatchFrameCallbacks(); }); + [this]() { dispatchFrameCallbacks(); }); } } diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp index 1f6edf36af25..18c50472a7df 100644 --- a/libs/hwui/tests/unit/CanvasOpTests.cpp +++ b/libs/hwui/tests/unit/CanvasOpTests.cpp @@ -225,9 +225,9 @@ TEST(CanvasOp, simpleDrawLines) { TEST(CanvasOp, simpleDrawRect) { CanvasOpBuffer buffer; EXPECT_EQ(buffer.size(), 0); - buffer.push<Op::DrawRect> ({ - .paint = SkPaint{}, - .rect = SkRect::MakeEmpty() + buffer.push<Op::DrawRect>({ + .rect = SkRect::MakeEmpty(), + .paint = SkPaint{}, }); CallCountingCanvas canvas; @@ -242,9 +242,9 @@ TEST(CanvasOp, simpleDrawRegionRect) { EXPECT_EQ(buffer.size(), 0); SkRegion region; region.setRect(SkIRect::MakeWH(12, 50)); - buffer.push<Op::DrawRegion> ({ - .paint = SkPaint{}, - .region = region + buffer.push<Op::DrawRegion>({ + .region = region, + .paint = SkPaint{}, }); CallCountingCanvas canvas; @@ -264,9 +264,9 @@ TEST(CanvasOp, simpleDrawRegionPath) { clip.setRect(SkIRect::MakeWH(100, 100)); SkRegion region; region.setPath(path, clip); - buffer.push<Op::DrawRegion> ({ - .paint = SkPaint{}, - .region = region + buffer.push<Op::DrawRegion>({ + .region = region, + .paint = SkPaint{}, }); CallCountingCanvas canvas; @@ -279,11 +279,11 @@ TEST(CanvasOp, simpleDrawRegionPath) { TEST(CanvasOp, simpleDrawRoundRect) { CanvasOpBuffer buffer; EXPECT_EQ(buffer.size(), 0); - buffer.push<Op::DrawRoundRect> ({ - .paint = SkPaint{}, - .rect = SkRect::MakeEmpty(), - .rx = 10, - .ry = 10 + buffer.push<Op::DrawRoundRect>({ + .rect = SkRect::MakeEmpty(), + .rx = 10, + .ry = 10, + .paint = SkPaint{}, }); CallCountingCanvas canvas; @@ -611,9 +611,9 @@ TEST(CanvasOp, immediateRendering) { EXPECT_EQ(0, canvas->sumTotalDrawCalls()); ImmediateModeRasterizer rasterizer{canvas}; - auto op = CanvasOp<Op::DrawRect> { - .paint = SkPaint{}, - .rect = SkRect::MakeEmpty() + auto op = CanvasOp<Op::DrawRect>{ + .rect = SkRect::MakeEmpty(), + .paint = SkPaint{}, }; EXPECT_TRUE(CanvasOpTraits::can_draw<decltype(op)>); rasterizer.draw(op); diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp index 8273524167f9..e727ea899098 100644 --- a/libs/hwui/tests/unit/RenderNodeTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -331,3 +331,31 @@ RENDERTHREAD_TEST(DISABLED_RenderNode, prepareTree_HwLayer_AVD_enqueueDamage) { EXPECT_EQ(uirenderer::Rect(0, 0, 200, 400), info.layerUpdateQueue->entries().at(0).damage); canvasContext->destroy(); } + +TEST(RenderNode, hasNoFill) { + auto rootNode = + TestUtils::createNode(0, 0, 200, 400, [](RenderProperties& props, Canvas& canvas) { + Paint paint; + paint.setStyle(SkPaint::Style::kStroke_Style); + canvas.drawRect(10, 10, 100, 100, paint); + }); + + TestUtils::syncHierarchyPropertiesAndDisplayList(rootNode); + + EXPECT_FALSE(rootNode.get()->getDisplayList().hasFill()); + EXPECT_FALSE(rootNode.get()->getDisplayList().hasText()); +} + +TEST(RenderNode, hasFill) { + auto rootNode = + TestUtils::createNode(0, 0, 200, 400, [](RenderProperties& props, Canvas& canvas) { + Paint paint; + paint.setStyle(SkPaint::kStrokeAndFill_Style); + canvas.drawRect(10, 10, 100, 100, paint); + }); + + TestUtils::syncHierarchyPropertiesAndDisplayList(rootNode); + + EXPECT_TRUE(rootNode.get()->getDisplayList().hasFill()); + EXPECT_FALSE(rootNode.get()->getDisplayList().hasText()); +} diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp index 499afa039d1f..c71c4d243a8b 100644 --- a/libs/hwui/tests/unit/TypefaceTests.cpp +++ b/libs/hwui/tests/unit/TypefaceTests.cpp @@ -29,6 +29,7 @@ #include "hwui/MinikinSkia.h" #include "hwui/Typeface.h" +#include "utils/TypefaceUtils.h" using namespace android; @@ -56,7 +57,7 @@ std::shared_ptr<minikin::FontFamily> buildFamily(const char* fileName) { sk_sp<SkData> skData = SkData::MakeWithProc(data, st.st_size, unmap, reinterpret_cast<void*>(st.st_size)); std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(skData)); - sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); + sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); sk_sp<SkTypeface> typeface(fm->makeFromStream(std::move(fontData))); LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", fileName); std::shared_ptr<minikin::MinikinFont> font = diff --git a/libs/hwui/tests/unit/UnderlineTest.cpp b/libs/hwui/tests/unit/UnderlineTest.cpp index db2be20936fb..c70a30477ecf 100644 --- a/libs/hwui/tests/unit/UnderlineTest.cpp +++ b/libs/hwui/tests/unit/UnderlineTest.cpp @@ -36,6 +36,7 @@ #include "hwui/MinikinUtils.h" #include "hwui/Paint.h" #include "hwui/Typeface.h" +#include "utils/TypefaceUtils.h" using namespace android; @@ -66,7 +67,7 @@ std::shared_ptr<minikin::FontFamily> buildFamily(const char* fileName) { sk_sp<SkData> skData = SkData::MakeWithProc(data, st.st_size, unmap, reinterpret_cast<void*>(st.st_size)); std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(skData)); - sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault()); + sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); sk_sp<SkTypeface> typeface(fm->makeFromStream(std::move(fontData))); LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", fileName); std::shared_ptr<minikin::MinikinFont> font = diff --git a/libs/hwui/utils/TypefaceUtils.cpp b/libs/hwui/utils/TypefaceUtils.cpp new file mode 100644 index 000000000000..a30b9257cd28 --- /dev/null +++ b/libs/hwui/utils/TypefaceUtils.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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. + */ + +#include <utils/TypefaceUtils.h> + +#include "include/ports/SkFontMgr_empty.h" + +namespace android { + +sk_sp<SkFontMgr> FreeTypeFontMgr() { + static sk_sp<SkFontMgr> mgr = SkFontMgr_New_Custom_Empty(); + return mgr; +} + +} // namespace android diff --git a/libs/hwui/utils/TypefaceUtils.h b/libs/hwui/utils/TypefaceUtils.h new file mode 100644 index 000000000000..c0adeaea3c6c --- /dev/null +++ b/libs/hwui/utils/TypefaceUtils.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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. + */ + +#pragma once + +#include "SkFontMgr.h" +#include "SkRefCnt.h" + +namespace android { + +// Return an SkFontMgr which is capable of turning bytes into a SkTypeface using Freetype. +// There are no other fonts inside this SkFontMgr (e.g. no system fonts). +sk_sp<SkFontMgr> FreeTypeFontMgr(); + +} // namespace android
\ No newline at end of file diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index fa07c3989720..a8b963367f4c 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -65,9 +65,9 @@ public: void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, BitSet32 spotIdBits, int32_t displayId) override; void clearSpots() override; + void updatePointerIcon(PointerIconStyle iconId) override; + void setCustomPointerIcon(const SpriteIcon& icon) override; - void updatePointerIcon(PointerIconStyle iconId); - void setCustomPointerIcon(const SpriteIcon& icon); virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout); void doInactivityTimeout(); void reloadPointerResources(); @@ -192,10 +192,10 @@ public: void setPresentation(Presentation) override { LOG_ALWAYS_FATAL("Should not be called"); } - void updatePointerIcon(PointerIconStyle) { + void updatePointerIcon(PointerIconStyle) override { LOG_ALWAYS_FATAL("Should not be called"); } - void setCustomPointerIcon(const SpriteIcon&) { + void setCustomPointerIcon(const SpriteIcon&) override { LOG_ALWAYS_FATAL("Should not be called"); } // fade() should not be called by inactivity timeout. Do nothing. |