summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/Android.bp18
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java54
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java75
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java9
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java2
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java68
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java80
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationRequestCallback.java115
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java62
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java487
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java80
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java79
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java386
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java196
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java22
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java5
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java180
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java28
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java21
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java21
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java96
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java1
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java2
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java177
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java313
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java22
-rw-r--r--libs/WindowManager/Jetpack/window-extensions-core-release.aarbin3714 -> 0 bytes
-rw-r--r--libs/WindowManager/Jetpack/window-extensions-release.aarbin41055 -> 0 bytes
-rw-r--r--libs/WindowManager/Shell/Android.bp2
-rw-r--r--libs/WindowManager/Shell/AndroidManifest.xml1
-rw-r--r--libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto58
-rw-r--r--libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml (renamed from libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml)14
-rw-r--r--libs/WindowManager/Shell/res/color/taskbar_background_dark.xml (renamed from libs/WindowManager/Shell/res/color-night/taskbar_background.xml)2
-rw-r--r--libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml (renamed from libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml)2
-rw-r--r--libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml (renamed from libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml)6
-rw-r--r--libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml (renamed from libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml)8
-rw-r--r--libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml (renamed from libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml)4
-rw-r--r--libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_left_hand.xml27
-rw-r--r--libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_right_hand.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml3
-rw-r--r--libs/WindowManager/Shell/res/drawable/bubble_manage_menu_bg.xml2
-rw-r--r--libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml31
-rw-r--r--libs/WindowManager/Shell/res/drawable/caption_floating_button.xml31
-rw-r--r--libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml31
-rw-r--r--libs/WindowManager/Shell/res/drawable/caption_more_button.xml31
-rw-r--r--libs/WindowManager/Shell/res/drawable/caption_select_button.xml30
-rw-r--r--libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml28
-rw-r--r--libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml11
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml3
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_close.xml (renamed from libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml)17
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml26
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_floating.xml (renamed from libs/WindowManager/Shell/res/drawable/caption_collapse_menu_button.xml)20
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml (renamed from libs/WindowManager/Shell/res/drawable/caption_close_button.xml)20
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml34
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml (renamed from libs/WindowManager/Shell/res/drawable/caption_screenshot_button.xml)20
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml (renamed from libs/WindowManager/Shell/res/color/taskbar_background.xml)10
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml22
-rw-r--r--libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_24.xml21
-rw-r--r--libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml3
-rw-r--r--libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml3
-rw-r--r--libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml3
-rw-r--r--libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml5
-rw-r--r--libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml5
-rw-r--r--libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml21
-rw-r--r--libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml2
-rw-r--r--libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_focus.xml (renamed from libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml)19
-rw-r--r--libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_swap.xml (renamed from libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml)16
-rw-r--r--libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml21
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml24
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_flyout.xml5
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_manage_button.xml3
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml4
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml92
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml136
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml (renamed from libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml)26
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml28
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_app_info_pill.xml57
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_more_actions_pill.xml47
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_windowing_pill.xml62
-rw-r--r--libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml3
-rw-r--r--libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml5
-rw-r--r--libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml4
-rw-r--r--libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml19
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_menu.xml141
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml40
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml3
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_split_menu_view.xml125
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml41
-rw-r--r--libs/WindowManager/Shell/res/values-af/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-af/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-am/strings.xml23
-rw-r--r--libs/WindowManager/Shell/res/values-am/strings_tv.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ar/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ar/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-as/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-as/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-az/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-az/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-be/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-be/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-bg/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-bg/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-bn/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-bn/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-bs/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-bs/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ca/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ca/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-cs/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-cs/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-da/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-da/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-de/strings.xml16
-rw-r--r--libs/WindowManager/Shell/res/values-de/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-el/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-el/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rAU/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rCA/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rGB/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rIN/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rXC/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-es-rUS/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-es/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-es/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-et/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-et/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-eu/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-eu/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fa/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-fa/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fi/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-fi/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fr-rCA/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings.xml15
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-gl/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-gl/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-gu/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-gu/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-hi/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-hi/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-hr/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-hr/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-hu/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-hu/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-hy/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-hy/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-in/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-in/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-is/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-is/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-it/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-it/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-iw/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-iw/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ja/strings.xml15
-rw-r--r--libs/WindowManager/Shell/res/values-ja/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ka/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ka/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-kk/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-kk/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-km/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-km/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-kn/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-kn/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ko/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ko/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ky/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-ky/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-lo/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-lo/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-lt/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-lt/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-lv/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-lv/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-mk/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-mk/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ml/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ml/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-mn/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-mn/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-mr/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-mr/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ms/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ms/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-my/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-my/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-nb/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-nb/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ne/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ne/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-night/colors.xml1
-rw-r--r--libs/WindowManager/Shell/res/values-night/styles.xml33
-rw-r--r--libs/WindowManager/Shell/res/values-nl/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-nl/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-or/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-or/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pa/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-pa/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pl/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-pl/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rBR/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rPT/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pt/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-pt/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ro/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-ro/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ru/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ru/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-si/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-si/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sk/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-sk/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sl/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-sl/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sq/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-sq/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sr/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-sr/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sv/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-sv/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sw/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-sw/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ta/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-ta/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-te/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-te/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-television/dimen.xml3
-rw-r--r--libs/WindowManager/Shell/res/values-th/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-th/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-tl/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-tl/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-tr/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-tr/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-tvdpi/dimen.xml32
-rw-r--r--libs/WindowManager/Shell/res/values-uk/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-uk/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ur/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-ur/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-uz/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-uz/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-vi/strings.xml13
-rw-r--r--libs/WindowManager/Shell/res/values-vi/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rCN/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rHK/strings.xml17
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rTW/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zu/strings.xml11
-rw-r--r--libs/WindowManager/Shell/res/values-zu/strings_tv.xml2
-rw-r--r--libs/WindowManager/Shell/res/values/colors.xml20
-rw-r--r--libs/WindowManager/Shell/res/values/colors_tv.xml21
-rw-r--r--libs/WindowManager/Shell/res/values/config.xml3
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml35
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml19
-rw-r--r--libs/WindowManager/Shell/res/values/strings_tv.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java257
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java66
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java127
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java54
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java120
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt)13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java795
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java106
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java411
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java364
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java419
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/StatusBarCustomizer.java30
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java119
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java119
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java120
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java565
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java97
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java79
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java84
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt103
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java86
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java185
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java282
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java100
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl (renamed from libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java)22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java233
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java267
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java212
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java42
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java129
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java)91
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java137
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java173
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java70
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java30
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java83
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java52
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java43
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java70
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java128
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt233
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java330
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt426
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java248
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java187
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java140
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/IDragAndDrop.aidl (renamed from libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java)23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java288
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java424
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java74
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java91
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java328
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java482
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java43
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java127
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java71
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java93
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java246
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java106
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java250
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java56
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java98
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java473
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java98
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java647
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java303
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java845
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java294
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java89
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java61
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java1046
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java234
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java399
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java1015
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java218
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java94
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java70
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java223
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java513
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java817
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java475
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java165
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java151
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java257
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java64
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java)2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java)9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java)312
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java416
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java425
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java307
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java162
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java63
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java342
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java104
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java869
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt74
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java364
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java325
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java411
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java187
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java152
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java143
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java404
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java203
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java197
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java180
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java192
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt84
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt44
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt28
-rw-r--r--libs/WindowManager/Shell/tests/flicker/Android.bp4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/AndroidTest.xml22
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt36
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt457
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt18
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt136
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt61
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt24
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt1
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt150
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt93
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt91
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt89
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt)60
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt)25
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt)34
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt104
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt159
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt)32
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt)19
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt)27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt60
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt74
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt90
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt52
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt212
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt211
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt200
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt83
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt106
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt88
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt)57
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt48
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt155
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt30
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt195
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt221
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt49
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt221
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt140
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt57
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt92
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt68
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt48
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt67
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt96
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt100
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt124
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt106
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt116
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt48
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt69
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt69
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt102
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt88
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTestCfArm.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt)38
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt117
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt72
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt110
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt93
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt113
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt122
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt71
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt187
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt37
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt174
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt158
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt165
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt166
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt44
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt79
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt)37
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt30
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt190
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt102
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt10
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt50
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt143
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt153
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt161
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt134
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt151
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt169
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt115
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt159
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt113
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt55
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt376
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt130
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt159
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt159
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt159
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt230
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt111
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt79
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt83
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt70
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt76
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt94
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt91
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt94
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt93
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt79
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt155
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt81
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt79
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt79
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt77
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt67
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp35
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml147
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.pngbin1966 -> 0 bytes
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml31
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml48
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml48
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml32
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml30
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml141
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml23
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml32
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml32
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml34
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java77
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java173
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java108
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java35
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java66
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java82
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java308
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java61
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java33
-rw-r--r--libs/WindowManager/Shell/tests/unittest/Android.bp4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java87
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java22
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java115
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java439
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java110
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java232
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java137
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt181
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java57
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java27
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt52
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java21
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java67
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java97
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java120
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt238
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt204
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java159
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java151
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java33
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java163
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java14
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java14
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java209
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java57
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java382
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt34
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java348
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt81
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java336
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java21
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java170
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java111
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java294
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java)201
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java333
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java644
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/StubTransaction.java313
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java21
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt232
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt)126
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt355
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java161
-rw-r--r--libs/androidfw/Android.bp11
-rw-r--r--[-rwxr-xr-x]libs/androidfw/ApkAssets.cpp13
-rw-r--r--libs/androidfw/ApkParsing.cpp110
-rw-r--r--libs/androidfw/AssetManager2.cpp231
-rw-r--r--libs/androidfw/AssetsProvider.cpp103
-rw-r--r--libs/androidfw/BigBuffer.cpp87
-rw-r--r--libs/androidfw/ConfigDescription.cpp47
-rw-r--r--libs/androidfw/CursorWindow.cpp10
-rw-r--r--libs/androidfw/Idmap.cpp76
-rw-r--r--libs/androidfw/LoadedArsc.cpp137
-rw-r--r--libs/androidfw/Locale.cpp6
-rw-r--r--libs/androidfw/PosixUtils.cpp54
-rw-r--r--libs/androidfw/ResourceTimer.cpp271
-rw-r--r--libs/androidfw/ResourceTypes.cpp144
-rw-r--r--libs/androidfw/ResourceUtils.cpp10
-rw-r--r--libs/androidfw/StringPool.cpp507
-rw-r--r--libs/androidfw/TypeWrappers.cpp13
-rw-r--r--libs/androidfw/Util.cpp145
-rw-r--r--libs/androidfw/include/androidfw/ApkParsing.h31
-rw-r--r--libs/androidfw/include/androidfw/AssetManager2.h20
-rw-r--r--libs/androidfw/include/androidfw/AssetsProvider.h31
-rw-r--r--libs/androidfw/include/androidfw/BigBuffer.h192
-rw-r--r--libs/androidfw/include/androidfw/ByteBucketArray.h58
-rw-r--r--libs/androidfw/include/androidfw/ConfigDescription.h8
-rw-r--r--libs/androidfw/include/androidfw/CursorWindow.h3
-rw-r--r--libs/androidfw/include/androidfw/IDiagnostics.h130
-rw-r--r--libs/androidfw/include/androidfw/Idmap.h29
-rw-r--r--libs/androidfw/include/androidfw/LoadedArsc.h21
-rw-r--r--libs/androidfw/include/androidfw/Locale.h6
-rw-r--r--libs/androidfw/include/androidfw/PosixUtils.h14
-rw-r--r--libs/androidfw/include/androidfw/ResourceTimer.h221
-rw-r--r--libs/androidfw/include/androidfw/ResourceTypes.h184
-rw-r--r--libs/androidfw/include/androidfw/ResourceUtils.h4
-rw-r--r--libs/androidfw/include/androidfw/Source.h90
-rw-r--r--libs/androidfw/include/androidfw/StringPiece.h292
-rw-r--r--libs/androidfw/include/androidfw/StringPool.h228
-rw-r--r--libs/androidfw/include/androidfw/Util.h135
-rw-r--r--libs/androidfw/include/androidfw/misc.h4
-rw-r--r--libs/androidfw/misc.cpp44
-rw-r--r--libs/androidfw/tests/ApkParsing_test.cpp77
-rw-r--r--libs/androidfw/tests/AttributeResolution_bench.cpp4
-rw-r--r--libs/androidfw/tests/BigBuffer_test.cpp101
-rw-r--r--libs/androidfw/tests/ByteBucketArray_test.cpp53
-rw-r--r--libs/androidfw/tests/ConfigDescription_test.cpp24
-rw-r--r--libs/androidfw/tests/Config_test.cpp14
-rw-r--r--libs/androidfw/tests/CursorWindow_test.cpp31
-rw-r--r--libs/androidfw/tests/LoadedArsc_test.cpp136
-rw-r--r--libs/androidfw/tests/PosixUtils_test.cpp18
-rw-r--r--libs/androidfw/tests/ResTable_test.cpp68
-rw-r--r--libs/androidfw/tests/ResourceTimer_test.cpp245
-rw-r--r--libs/androidfw/tests/SparseEntry_bench.cpp67
-rw-r--r--libs/androidfw/tests/StringPiece_test.cpp32
-rw-r--r--libs/androidfw/tests/StringPool_test.cpp388
-rw-r--r--libs/androidfw/tests/TypeWrappers_test.cpp170
-rw-r--r--libs/androidfw/tests/data/overlay/overlay.idmapbin636 -> 732 bytes
-rw-r--r--libs/androidfw/tests/data/sparse/Android.bp23
-rw-r--r--libs/androidfw/tests/data/sparse/AndroidManifest.xml1
-rw-r--r--libs/androidfw/tests/data/sparse/R.h2
-rwxr-xr-xlibs/androidfw/tests/data/sparse/gen_strings.sh10
-rw-r--r--libs/androidfw/tests/data/sparse/not_sparse.apkbin62155 -> 62219 bytes
-rw-r--r--libs/androidfw/tests/data/sparse/res/values-land/strings.xml (renamed from libs/androidfw/tests/data/sparse/res/values-v26/strings.xml)2
-rw-r--r--libs/androidfw/tests/data/sparse/res/values-land/values.xml (renamed from libs/androidfw/tests/data/sparse/res/values-v26/values.xml)0
-rw-r--r--libs/androidfw/tests/data/sparse/sparse.apkbin59459 -> 59523 bytes
-rw-r--r--libs/dream/lowlight/Android.bp6
-rw-r--r--libs/dream/lowlight/res/values/config.xml3
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java117
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt138
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java111
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt124
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java61
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt82
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt9
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt8
-rw-r--r--libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt)22
-rw-r--r--libs/dream/lowlight/tests/Android.bp2
-rw-r--r--libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java94
-rw-r--r--libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt169
-rw-r--r--libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java113
-rw-r--r--libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt184
-rw-r--r--libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt125
-rw-r--r--libs/hwui/Android.bp29
-rw-r--r--libs/hwui/CanvasTransform.cpp34
-rw-r--r--libs/hwui/CanvasTransform.h2
-rw-r--r--libs/hwui/ColorMode.h5
-rw-r--r--libs/hwui/CopyRequest.h42
-rw-r--r--libs/hwui/DamageAccumulator.cpp43
-rw-r--r--libs/hwui/DamageAccumulator.h4
-rw-r--r--libs/hwui/DeferredLayerUpdater.h1
-rw-r--r--libs/hwui/DeviceInfo.cpp14
-rw-r--r--libs/hwui/DeviceInfo.h10
-rw-r--r--libs/hwui/DisplayListOps.in2
-rw-r--r--libs/hwui/FrameInfo.h1
-rw-r--r--libs/hwui/Gainmap.cpp (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt)31
-rw-r--r--libs/hwui/Gainmap.h (renamed from libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java)27
-rw-r--r--libs/hwui/HardwareBitmapUploader.cpp9
-rw-r--r--libs/hwui/HardwareBitmapUploader.h3
-rw-r--r--libs/hwui/Layer.cpp2
-rw-r--r--libs/hwui/MemoryPolicy.cpp69
-rw-r--r--libs/hwui/MemoryPolicy.h69
-rw-r--r--libs/hwui/Mesh.cpp102
-rw-r--r--libs/hwui/Mesh.h199
-rw-r--r--libs/hwui/Properties.cpp33
-rw-r--r--libs/hwui/Properties.h46
-rw-r--r--libs/hwui/Readback.cpp127
-rw-r--r--libs/hwui/Readback.h20
-rw-r--r--libs/hwui/RecordingCanvas.cpp258
-rw-r--r--libs/hwui/RecordingCanvas.h103
-rw-r--r--libs/hwui/Rect.h9
-rw-r--r--libs/hwui/RenderNode.cpp3
-rw-r--r--libs/hwui/RenderNode.h5
-rw-r--r--libs/hwui/SafeMath.h107
-rw-r--r--libs/hwui/SkiaCanvas.cpp111
-rw-r--r--libs/hwui/SkiaCanvas.h36
-rw-r--r--libs/hwui/SkiaInterpolator.cpp27
-rw-r--r--libs/hwui/SkiaInterpolator.h12
-rw-r--r--libs/hwui/Tonemapper.cpp117
-rw-r--r--libs/hwui/Tonemapper.h28
-rw-r--r--libs/hwui/TreeInfo.h4
-rw-r--r--libs/hwui/VectorDrawable.cpp5
-rw-r--r--libs/hwui/VectorDrawable.h1
-rw-r--r--libs/hwui/apex/LayoutlibLoader.cpp5
-rw-r--r--libs/hwui/apex/android_bitmap.cpp5
-rw-r--r--libs/hwui/apex/android_canvas.cpp2
-rw-r--r--libs/hwui/apex/android_paint.cpp1
-rw-r--r--libs/hwui/apex/jni_runtime.cpp119
-rw-r--r--libs/hwui/canvas/CanvasOps.h12
-rw-r--r--libs/hwui/effects/GainmapRenderer.cpp304
-rw-r--r--libs/hwui/effects/GainmapRenderer.h40
-rw-r--r--libs/hwui/hwui/AnimatedImageDrawable.cpp33
-rw-r--r--libs/hwui/hwui/AnimatedImageDrawable.h17
-rw-r--r--libs/hwui/hwui/Bitmap.cpp75
-rw-r--r--libs/hwui/hwui/Bitmap.h15
-rw-r--r--libs/hwui/hwui/BlurDrawLooper.cpp2
-rw-r--r--libs/hwui/hwui/Canvas.cpp1
-rw-r--r--libs/hwui/hwui/Canvas.h17
-rw-r--r--libs/hwui/hwui/ImageDecoder.cpp114
-rw-r--r--libs/hwui/hwui/ImageDecoder.h9
-rw-r--r--libs/hwui/hwui/MinikinSkia.cpp5
-rw-r--r--libs/hwui/hwui/Typeface.cpp13
-rw-r--r--libs/hwui/hwui/Typeface.h3
-rw-r--r--libs/hwui/jni/AnimatedImageDrawable.cpp9
-rw-r--r--[-rwxr-xr-x]libs/hwui/jni/Bitmap.cpp320
-rw-r--r--libs/hwui/jni/Bitmap.h1
-rw-r--r--libs/hwui/jni/BitmapFactory.cpp196
-rw-r--r--libs/hwui/jni/BitmapFactory.h6
-rw-r--r--libs/hwui/jni/BitmapRegionDecoder.cpp201
-rw-r--r--libs/hwui/jni/BufferUtils.cpp130
-rw-r--r--libs/hwui/jni/BufferUtils.h32
-rw-r--r--libs/hwui/jni/ByteBufferStreamAdaptor.cpp1
-rw-r--r--libs/hwui/jni/ColorFilter.cpp1
-rw-r--r--libs/hwui/jni/FontFamily.cpp5
-rw-r--r--libs/hwui/jni/GIFMovie.cpp2
-rw-r--r--libs/hwui/jni/Gainmap.cpp278
-rw-r--r--libs/hwui/jni/Graphics.cpp70
-rw-r--r--libs/hwui/jni/GraphicsJNI.h59
-rw-r--r--libs/hwui/jni/HardwareBufferHelpers.cpp68
-rw-r--r--libs/hwui/jni/HardwareBufferHelpers.h38
-rw-r--r--libs/hwui/jni/ImageDecoder.cpp71
-rw-r--r--libs/hwui/jni/Interpolator.cpp4
-rw-r--r--libs/hwui/jni/JvmErrorReporter.h47
-rw-r--r--libs/hwui/jni/MaskFilter.cpp1
-rw-r--r--libs/hwui/jni/MeshSpecification.cpp164
-rw-r--r--libs/hwui/jni/Movie.cpp2
-rw-r--r--libs/hwui/jni/Movie.h1
-rw-r--r--libs/hwui/jni/MovieImpl.cpp9
-rw-r--r--libs/hwui/jni/NinePatch.cpp2
-rw-r--r--libs/hwui/jni/NinePatchPeeker.cpp2
-rw-r--r--libs/hwui/jni/Paint.cpp256
-rw-r--r--libs/hwui/jni/Path.cpp123
-rw-r--r--libs/hwui/jni/PathEffect.cpp6
-rw-r--r--libs/hwui/jni/PathIterator.cpp81
-rw-r--r--libs/hwui/jni/RenderEffect.cpp1
-rw-r--r--libs/hwui/jni/ScopedParcel.cpp85
-rw-r--r--libs/hwui/jni/ScopedParcel.h63
-rw-r--r--libs/hwui/jni/Shader.cpp87
-rw-r--r--libs/hwui/jni/Typeface.cpp118
-rw-r--r--libs/hwui/jni/Utils.cpp3
-rw-r--r--libs/hwui/jni/Utils.h3
-rw-r--r--libs/hwui/jni/YuvToJpegEncoder.cpp191
-rw-r--r--libs/hwui/jni/YuvToJpegEncoder.h51
-rw-r--r--libs/hwui/jni/android_graphics_Canvas.cpp93
-rw-r--r--libs/hwui/jni/android_graphics_ColorSpace.cpp50
-rw-r--r--libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp176
-rw-r--r--libs/hwui/jni/android_graphics_HardwareRenderer.cpp192
-rw-r--r--libs/hwui/jni/android_graphics_Matrix.cpp2
-rw-r--r--libs/hwui/jni/android_graphics_Mesh.cpp205
-rw-r--r--libs/hwui/jni/android_graphics_RenderNode.cpp132
-rw-r--r--libs/hwui/jni/fonts/Font.cpp11
-rw-r--r--libs/hwui/jni/fonts/FontFamily.cpp15
-rw-r--r--libs/hwui/jni/text/GraphemeBreak.cpp67
-rw-r--r--libs/hwui/pipeline/skia/DumpOpsCanvas.h2
-rw-r--r--libs/hwui/pipeline/skia/GLFunctorDrawable.cpp8
-rw-r--r--libs/hwui/pipeline/skia/HolePunch.h1
-rw-r--r--libs/hwui/pipeline/skia/LayerDrawable.cpp61
-rw-r--r--libs/hwui/pipeline/skia/RenderNodeDrawable.cpp9
-rw-r--r--libs/hwui/pipeline/skia/RenderNodeDrawable.h1
-rw-r--r--libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp7
-rw-r--r--libs/hwui/pipeline/skia/ShaderCache.cpp4
-rw-r--r--libs/hwui/pipeline/skia/ShaderCache.h5
-rw-r--r--libs/hwui/pipeline/skia/SkiaDisplayList.cpp8
-rw-r--r--libs/hwui/pipeline/skia/SkiaDisplayList.h2
-rw-r--r--libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp60
-rw-r--r--libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h16
-rw-r--r--libs/hwui/pipeline/skia/SkiaPipeline.cpp61
-rw-r--r--libs/hwui/pipeline/skia/SkiaPipeline.h18
-rw-r--r--libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp8
-rw-r--r--libs/hwui/pipeline/skia/SkiaProfileRenderer.h9
-rw-r--r--libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp102
-rw-r--r--libs/hwui/pipeline/skia/SkiaRecordingCanvas.h10
-rw-r--r--libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp48
-rw-r--r--libs/hwui/pipeline/skia/SkiaVulkanPipeline.h24
-rw-r--r--libs/hwui/pipeline/skia/StretchMask.cpp8
-rw-r--r--libs/hwui/pipeline/skia/TransformCanvas.cpp10
-rw-r--r--libs/hwui/pipeline/skia/TransformCanvas.h7
-rw-r--r--libs/hwui/pipeline/skia/VkFunctorDrawable.cpp8
-rw-r--r--libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp4
-rw-r--r--libs/hwui/private/hwui/DrawGlInfo.h12
-rw-r--r--libs/hwui/private/hwui/DrawVkInfo.h8
-rw-r--r--libs/hwui/renderthread/CacheManager.cpp215
-rw-r--r--libs/hwui/renderthread/CacheManager.h47
-rw-r--r--libs/hwui/renderthread/CanvasContext.cpp184
-rw-r--r--libs/hwui/renderthread/CanvasContext.h75
-rw-r--r--libs/hwui/renderthread/DrawFrameTask.cpp149
-rw-r--r--libs/hwui/renderthread/DrawFrameTask.h38
-rw-r--r--libs/hwui/renderthread/EglManager.cpp26
-rw-r--r--libs/hwui/renderthread/EglManager.h1
-rw-r--r--libs/hwui/renderthread/HardwareBufferRenderParams.h70
-rw-r--r--libs/hwui/renderthread/HintSessionWrapper.cpp185
-rw-r--r--libs/hwui/renderthread/HintSessionWrapper.h61
-rw-r--r--libs/hwui/renderthread/IRenderPipeline.h18
-rw-r--r--libs/hwui/renderthread/RenderProxy.cpp80
-rw-r--r--libs/hwui/renderthread/RenderProxy.h22
-rw-r--r--libs/hwui/renderthread/RenderThread.cpp36
-rw-r--r--libs/hwui/renderthread/RenderThread.h7
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp119
-rw-r--r--libs/hwui/renderthread/VulkanManager.h29
-rw-r--r--libs/hwui/renderthread/VulkanSurface.cpp56
-rw-r--r--libs/hwui/renderthread/VulkanSurface.h4
-rw-r--r--libs/hwui/tests/common/BitmapAllocationTestUtils.h5
-rw-r--r--libs/hwui/tests/common/CallCountingCanvas.h2
-rw-r--r--libs/hwui/tests/common/TestContext.cpp12
-rw-r--r--libs/hwui/tests/common/TestListViewSceneBase.cpp2
-rw-r--r--libs/hwui/tests/common/TestUtils.cpp6
-rw-r--r--libs/hwui/tests/common/TestUtils.h56
-rw-r--r--libs/hwui/tests/common/scenes/BitmapFillrate.cpp1
-rw-r--r--libs/hwui/tests/common/scenes/BitmapShaders.cpp12
-rw-r--r--libs/hwui/tests/common/scenes/ClippingAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/HwBitmap565.cpp6
-rw-r--r--libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/HwLayerAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/JankyScene.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp1
-rw-r--r--libs/hwui/tests/common/scenes/ListViewAnimation.cpp11
-rw-r--r--libs/hwui/tests/common/scenes/MagnifierAnimation.cpp55
-rw-r--r--libs/hwui/tests/common/scenes/OvalAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/PathClippingAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp6
-rw-r--r--libs/hwui/tests/common/scenes/RecentsAnimation.cpp5
-rw-r--r--libs/hwui/tests/common/scenes/RectGridAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp1
-rw-r--r--libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/ShapeAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp4
-rw-r--r--libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp1
-rw-r--r--libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp13
-rw-r--r--libs/hwui/tests/common/scenes/TextAnimation.cpp2
-rw-r--r--libs/hwui/tests/common/scenes/TvApp.cpp5
-rw-r--r--libs/hwui/tests/microbench/DisplayListCanvasBench.cpp2
-rw-r--r--libs/hwui/tests/microbench/RenderNodeBench.cpp2
-rw-r--r--libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp2
-rw-r--r--libs/hwui/tests/unit/CacheManagerTests.cpp10
-rw-r--r--libs/hwui/tests/unit/CanvasContextTests.cpp4
-rw-r--r--libs/hwui/tests/unit/CanvasOpTests.cpp11
-rw-r--r--libs/hwui/tests/unit/EglManagerTests.cpp2
-rw-r--r--libs/hwui/tests/unit/FatalTestCanvas.h2
-rw-r--r--libs/hwui/tests/unit/RenderNodeDrawableTests.cpp11
-rw-r--r--libs/hwui/tests/unit/RenderNodeTests.cpp4
-rw-r--r--libs/hwui/tests/unit/SkiaBehaviorTests.cpp7
-rw-r--r--libs/hwui/tests/unit/SkiaCanvasTests.cpp10
-rw-r--r--libs/hwui/tests/unit/SkiaDisplayListTests.cpp4
-rw-r--r--libs/hwui/tests/unit/SkiaPipelineTests.cpp1
-rw-r--r--libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp1
-rw-r--r--libs/hwui/tests/unit/TypefaceTests.cpp72
-rw-r--r--libs/hwui/tests/unit/VectorDrawableTests.cpp6
-rw-r--r--libs/hwui/utils/AutoMalloc.h94
-rw-r--r--libs/hwui/utils/Color.cpp68
-rw-r--r--libs/hwui/utils/Color.h2
-rw-r--r--libs/hwui/utils/PaintUtils.h1
-rw-r--r--libs/input/MouseCursorController.cpp145
-rw-r--r--libs/input/MouseCursorController.h21
-rw-r--r--libs/input/PointerController.cpp108
-rw-r--r--libs/input/PointerController.h39
-rw-r--r--libs/input/PointerControllerContext.h12
-rw-r--r--libs/input/SpriteController.cpp37
-rw-r--r--libs/input/SpriteController.h2
-rw-r--r--libs/input/SpriteIcon.h10
-rw-r--r--libs/input/TouchSpotController.cpp35
-rw-r--r--libs/input/TouchSpotController.h3
-rw-r--r--libs/input/tests/PointerController_test.cpp109
-rw-r--r--libs/securebox/Android.bp8
-rw-r--r--libs/securebox/OWNERS1
-rw-r--r--libs/securebox/src/com/android/security/SecureBox.java461
-rw-r--r--libs/securebox/tests/Android.bp46
-rw-r--r--libs/securebox/tests/AndroidManifest.xml33
-rw-r--r--libs/securebox/tests/AndroidTest.xml31
-rw-r--r--libs/securebox/tests/src/com/android/security/SecureBoxTest.java371
-rw-r--r--libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java4
1006 files changed, 51568 insertions, 19204 deletions
diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp
index a5b192cd7ceb..abe8f859f2fe 100644
--- a/libs/WindowManager/Jetpack/Android.bp
+++ b/libs/WindowManager/Jetpack/Android.bp
@@ -55,20 +55,6 @@ prebuilt_etc {
}
// Extensions
-// NOTE: This module is still under active development and must not
-// be used in production. Use 'androidx.window.sidecar' instead.
-android_library_import {
- name: "window-extensions",
- aars: ["window-extensions-release.aar"],
- sdk_version: "current",
-}
-
-android_library_import {
- name: "window-extensions-core",
- aars: ["window-extensions-core-release.aar"],
- sdk_version: "current",
-}
-
java_library {
name: "androidx.window.extensions",
srcs: [
@@ -77,8 +63,8 @@ java_library {
"src/androidx/window/common/**/*.java",
],
static_libs: [
- "window-extensions",
- "window-extensions-core",
+ "androidx.window.extensions_extensions-nodeps",
+ "androidx.window.extensions.core_core-nodeps",
],
installable: true,
sdk_version: "core_platform",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
index 68ff806c6765..65955b1d9bcc 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
@@ -21,6 +21,7 @@ import static androidx.window.util.ExtensionHelper.isZero;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.graphics.Rect;
+import android.hardware.devicestate.DeviceStateManager;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -33,7 +34,8 @@ import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-/** A representation of a folding feature for both Extension and Sidecar.
+/**
+ * A representation of a folding feature for both Extension and Sidecar.
* For Sidecar this is the same as combining {@link androidx.window.sidecar.SidecarDeviceState} and
* {@link androidx.window.sidecar.SidecarDisplayFeature}. For Extensions this is the mirror of
* {@link androidx.window.extensions.layout.FoldingFeature}.
@@ -67,10 +69,11 @@ public final class CommonFoldingFeature {
public static final int COMMON_STATE_UNKNOWN = -1;
/**
- * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar
- * and Extensions do not match exactly.
+ * A common state that contains no folding features. For example, an in-folding device in the
+ * "closed" device state.
*/
- public static final int COMMON_STATE_FLAT = 3;
+ public static final int COMMON_STATE_NO_FOLDING_FEATURES = 1;
+
/**
* A common state to represent a HALF_OPENED hinge. This is needed because the definitions in
* Sidecar and Extensions do not match exactly.
@@ -78,9 +81,27 @@ public final class CommonFoldingFeature {
public static final int COMMON_STATE_HALF_OPENED = 2;
/**
- * The possible states for a folding hinge.
+ * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar
+ * and Extensions do not match exactly.
+ */
+ public static final int COMMON_STATE_FLAT = 3;
+
+ /**
+ * A common state where the hinge state should be derived using the base state from
+ * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)} instead of the
+ * emulated state. This is an internal state and must not be passed to clients.
*/
- @IntDef({COMMON_STATE_UNKNOWN, COMMON_STATE_FLAT, COMMON_STATE_HALF_OPENED})
+ public static final int COMMON_STATE_USE_BASE_STATE = 1000;
+
+ /**
+ * The possible states for a folding hinge. Common in this context means normalized between
+ * extensions and sidecar.
+ */
+ @IntDef({COMMON_STATE_UNKNOWN,
+ COMMON_STATE_NO_FOLDING_FEATURES,
+ COMMON_STATE_HALF_OPENED,
+ COMMON_STATE_FLAT,
+ COMMON_STATE_USE_BASE_STATE})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
}
@@ -167,7 +188,7 @@ public final class CommonFoldingFeature {
}
String stateString = featureMatcher.group(6);
stateString = stateString == null ? "" : stateString;
- final int state;
+ @State final int state;
switch (stateString) {
case PATTERN_STATE_FLAT:
state = COMMON_STATE_FLAT;
@@ -191,8 +212,8 @@ public final class CommonFoldingFeature {
@NonNull
private final Rect mRect;
- CommonFoldingFeature(int type, int state, @NonNull Rect rect) {
- assertValidState(state);
+ CommonFoldingFeature(int type, @State int state, @NonNull Rect rect) {
+ assertReportableState(state);
this.mType = type;
this.mState = state;
if (rect.width() == 0 && rect.height() == 0) {
@@ -231,13 +252,22 @@ public final class CommonFoldingFeature {
}
@Override
+ public String toString() {
+ return "CommonFoldingFeature=[Type: " + mType + ", state: " + mState + "]";
+ }
+
+ @Override
public int hashCode() {
return Objects.hash(mType, mState, mRect);
}
- private static void assertValidState(@Nullable Integer state) {
- if (state != null && state != COMMON_STATE_FLAT
- && state != COMMON_STATE_HALF_OPENED && state != COMMON_STATE_UNKNOWN) {
+ /**
+ * Checks if the provided folding feature state should be reported to clients. See
+ * {@link androidx.window.extensions.layout.FoldingFeature}
+ */
+ private static void assertReportableState(@State int state) {
+ if (state != COMMON_STATE_FLAT && state != COMMON_STATE_HALF_OPENED
+ && state != COMMON_STATE_UNKNOWN) {
throw new IllegalArgumentException("Invalid state: " + state
+ "must be either COMMON_STATE_FLAT or COMMON_STATE_HALF_OPENED");
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 0bdf98c8680f..a184dff5005b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -19,6 +19,7 @@ package androidx.window.common;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
+import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE;
import static androidx.window.common.CommonFoldingFeature.parseListFromString;
import android.annotation.NonNull;
@@ -38,7 +39,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.function.Consumer;
/**
@@ -52,13 +52,54 @@ public final class DeviceStateManagerFoldingFeatureProducer
DeviceStateManagerFoldingFeatureProducer.class.getSimpleName();
private static final boolean DEBUG = false;
+ /**
+ * Emulated device state {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)} to
+ * {@link CommonFoldingFeature.State} map.
+ */
private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+ /**
+ * Emulated device state received via
+ * {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)}.
+ * "Emulated" states differ from "base" state in the sense that they may not correspond 1:1 with
+ * physical device states. They represent the state of the device when various software
+ * features and APIs are applied. The emulated states generally consist of all "base" states,
+ * but may have additional states such as "concurrent" or "rear display". Concurrent mode for
+ * example is activated via public API and can be active in both the "open" and "half folded"
+ * device states.
+ */
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
+ /**
+ * Base device state received via
+ * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)}.
+ * "Base" in this context means the "physical" state of the device.
+ */
+ private int mCurrentBaseDeviceState = INVALID_DEVICE_STATE;
+
@NonNull
private final BaseDataProducer<String> mRawFoldSupplier;
+ private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+ @Override
+ public void onStateChanged(int state) {
+ mCurrentDeviceState = state;
+ mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
+ .this::notifyFoldingFeatureChange);
+ }
+
+ @Override
+ public void onBaseStateChanged(int state) {
+ mCurrentBaseDeviceState = state;
+
+ if (mDeviceStateToPostureMap.get(mCurrentDeviceState)
+ == COMMON_STATE_USE_BASE_STATE) {
+ mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
+ .this::notifyFoldingFeatureChange);
+ }
+ }
+ };
+
public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context,
@NonNull BaseDataProducer<String> rawFoldSupplier) {
mRawFoldSupplier = rawFoldSupplier;
@@ -92,12 +133,8 @@ public final class DeviceStateManagerFoldingFeatureProducer
}
if (mDeviceStateToPostureMap.size() > 0) {
- DeviceStateCallback deviceStateCallback = (state) -> {
- mCurrentDeviceState = state;
- mRawFoldSupplier.getData(this::notifyFoldingFeatureChange);
- };
Objects.requireNonNull(context.getSystemService(DeviceStateManager.class))
- .registerCallback(context.getMainExecutor(), deviceStateCallback);
+ .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
}
}
@@ -129,14 +166,13 @@ public final class DeviceStateManagerFoldingFeatureProducer
}
@Override
- protected void onListenersChanged(
- @NonNull Set<Consumer<List<CommonFoldingFeature>>> callbacks) {
- super.onListenersChanged(callbacks);
- if (callbacks.isEmpty()) {
+ protected void onListenersChanged() {
+ super.onListenersChanged();
+ if (hasListeners()) {
+ mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
+ } else {
mCurrentDeviceState = INVALID_DEVICE_STATE;
mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
- } else {
- mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
}
}
@@ -178,11 +214,18 @@ public final class DeviceStateManagerFoldingFeatureProducer
}
private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
- final int globalHingeState = globalHingeState();
- return parseListFromString(displayFeaturesString, globalHingeState);
+ return parseListFromString(displayFeaturesString, currentHingeState());
}
- private int globalHingeState() {
- return mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN);
+ @CommonFoldingFeature.State
+ private int currentHingeState() {
+ @CommonFoldingFeature.State
+ int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN);
+
+ if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
+ posture = mDeviceStateToPostureMap.get(mCurrentBaseDeviceState, COMMON_STATE_UNKNOWN);
+ }
+
+ return posture;
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 7906342d445d..8906e6d3d02e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -31,7 +31,6 @@ import androidx.window.util.BaseDataProducer;
import com.android.internal.R;
import java.util.Optional;
-import java.util.Set;
import java.util.function.Consumer;
/**
@@ -86,11 +85,11 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
}
@Override
- protected void onListenersChanged(Set<Consumer<String>> callbacks) {
- if (callbacks.isEmpty()) {
- unregisterObserversIfNeeded();
- } else {
+ protected void onListenersChanged() {
+ if (hasListeners()) {
registerObserversIfNeeded();
+ } else {
+ unregisterObserversIfNeeded();
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 666b472c3716..76e0e1eb7a95 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -48,7 +48,7 @@ public class WindowExtensionsImpl implements WindowExtensions {
// TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return 2;
+ return 3;
}
@NonNull
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java
new file mode 100644
index 000000000000..849b500f36e4
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java
@@ -0,0 +1,68 @@
+/*
+ * 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 androidx.window.extensions.area;
+
+import android.app.Presentation;
+import android.content.Context;
+import android.view.Display;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
+
+/**
+ * {@link Presentation} object that is used to present extra content
+ * on the rear facing display when in a rear display presentation feature.
+ */
+class RearDisplayPresentation extends Presentation implements ExtensionWindowAreaPresentation {
+
+ @NonNull
+ private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer;
+
+ RearDisplayPresentation(@NonNull Context outerContext, @NonNull Display display,
+ @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) {
+ super(outerContext, display);
+ mStateConsumer = stateConsumer;
+ }
+
+ /**
+ * {@code mStateConsumer} is notified that their content is now visible when the
+ * {@link Presentation} object is started. There is no comparable callback for
+ * {@link WindowAreaComponent#SESSION_STATE_CONTENT_INVISIBLE} in {@link #onStop()} due to the
+ * timing of when a {@link android.hardware.devicestate.DeviceStateRequest} is cancelled
+ * ending rear display presentation mode happening before the {@link Presentation} is stopped.
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mStateConsumer.accept(WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE);
+ }
+
+ @NonNull
+ @Override
+ public Context getPresentationContext() {
+ return getContext();
+ }
+
+ @Override
+ public void setPresentationView(View view) {
+ setContentView(view);
+ if (!isShowing()) {
+ show();
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java
new file mode 100644
index 000000000000..0e9119a4332b
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java
@@ -0,0 +1,80 @@
+/*
+ * 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 androidx.window.extensions.area;
+
+import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE;
+import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE;
+
+import android.content.Context;
+import android.view.Display;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Consumer;
+
+import java.util.Objects;
+
+/**
+ * Controller class that manages the creation of the {@link android.app.Presentation} object used
+ * to show content on the rear facing display. This controller notifies the session callback with
+ * {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus} values when the
+ * feature is active, or when the feature has been ended.
+ */
+class RearDisplayPresentationController {
+
+ // Original context that requested to enable rear display presentation mode
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer;
+ @Nullable
+ private ExtensionWindowAreaPresentation mExtensionWindowAreaPresentation;
+
+ /**
+ * Creates the RearDisplayPresentationController
+ * @param context Originating {@link android.content.Context} that is initiating the rear
+ * display presentation session.
+ * @param stateConsumer {@link Consumer} that will be notified that the session is active when
+ * the device state request is active and the session has been created. If the device
+ * state request is cancelled, the callback will be notified that the session has been
+ * ended. This could occur through a call to cancel the feature or if the device is
+ * manipulated in a way that cancels any device state override.
+ */
+ RearDisplayPresentationController(@NonNull Context context,
+ @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(stateConsumer);
+
+ mContext = context;
+ mStateConsumer = stateConsumer;
+ }
+
+ public void startSession(@NonNull Display rearDisplay) {
+ mExtensionWindowAreaPresentation =
+ new RearDisplayPresentation(mContext, rearDisplay, mStateConsumer);
+ mStateConsumer.accept(SESSION_STATE_ACTIVE);
+ }
+
+ public void endSession() {
+ mStateConsumer.accept(SESSION_STATE_INACTIVE);
+ }
+
+ @Nullable
+ public ExtensionWindowAreaPresentation getWindowAreaPresentation() {
+ return mExtensionWindowAreaPresentation;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationRequestCallback.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationRequestCallback.java
new file mode 100644
index 000000000000..ea33426d0bdf
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationRequestCallback.java
@@ -0,0 +1,115 @@
+/*
+ * 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 androidx.window.extensions.area;
+
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Callback class to be notified of updates to a {@link DeviceStateRequest} for the rear display
+ * presentation state. This class notifies the {@link RearDisplayPresentationController} when the
+ * device is ready to enable the rear display presentation feature.
+ */
+public class RearDisplayPresentationRequestCallback implements DeviceStateRequest.Callback {
+
+ private static final String TAG = RearDisplayPresentationRequestCallback.class.getSimpleName();
+
+ @NonNull
+ private final DisplayManager mDisplayManager;
+ @NonNull
+ private final DisplayManager.DisplayListener mRearDisplayListener = new RearDisplayListener();
+ @NonNull
+ private final RearDisplayPresentationController mRearDisplayPresentationController;
+ private boolean mWaitingForRearDisplay = false;
+
+ public RearDisplayPresentationRequestCallback(@NonNull Context context,
+ @NonNull RearDisplayPresentationController rearDisplayPresentationController) {
+ mDisplayManager = context.getSystemService(DisplayManager.class);
+ mDisplayManager.registerDisplayListener(mRearDisplayListener,
+ context.getMainThreadHandler(), DisplayManager.EVENT_FLAG_DISPLAY_ADDED
+ | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED);
+
+ mRearDisplayPresentationController = rearDisplayPresentationController;
+ }
+
+ @Override
+ public void onRequestActivated(@NonNull DeviceStateRequest request) {
+ Display[] rearDisplays = mDisplayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_REAR);
+ if (rearDisplays.length == 0) {
+ // No rear facing display found, marking waiting for display flag as true.
+ mWaitingForRearDisplay = true;
+ return;
+ }
+ mDisplayManager.unregisterDisplayListener(mRearDisplayListener);
+ mRearDisplayPresentationController.startSession(rearDisplays[0]);
+ }
+
+ @Override
+ public void onRequestCanceled(@NonNull DeviceStateRequest request) {
+ mDisplayManager.unregisterDisplayListener(mRearDisplayListener);
+ mRearDisplayPresentationController.endSession();
+ }
+
+ /**
+ * {@link DisplayManager.DisplayListener} to be used if a rear facing {@link Display} isn't
+ * available synchronously when the device is entered into the rear display presentation state.
+ * A rear facing {@link Display} is a {@link Display} that is categorized as
+ * {@link DisplayManager#DISPLAY_CATEGORY_REAR}. This can occur if {@link DisplayManager} is
+ * still in the process of configuring itself for this state when
+ * {@link DeviceStateRequest.Callback#onRequestActivated} is called.
+ *
+ * The {@link DisplayManager.DisplayListener} removes itself when a rear facing display is
+ * found.
+ */
+ private class RearDisplayListener implements DisplayManager.DisplayListener {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ Display display = mDisplayManager.getDisplay(displayId);
+ if (mWaitingForRearDisplay && (display.getFlags() & Display.FLAG_REAR) != 0) {
+ startRearDisplayPresentation(display);
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ Display display = mDisplayManager.getDisplay(displayId);
+ if (mWaitingForRearDisplay && (display.getFlags() & Display.FLAG_REAR) != 0) {
+ startRearDisplayPresentation(display);
+ }
+ }
+
+ /**
+ * Starts a new {@link RearDisplayPresentation} with the updated {@link Display} with a
+ * category of {@link DisplayManager#DISPLAY_CATEGORY_REAR}.
+ */
+ private void startRearDisplayPresentation(Display rearDisplay) {
+ // We have been notified of a change to a rear display, we can unregister the
+ // callback and stop waiting for a display
+ mDisplayManager.unregisterDisplayListener(this);
+ mWaitingForRearDisplay = false;
+
+ mRearDisplayPresentationController.startSession(rearDisplay);
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java
new file mode 100644
index 000000000000..0b1423ae48c0
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java
@@ -0,0 +1,62 @@
+/*
+ * 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 androidx.window.extensions.area;
+
+import android.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Class that provides information around the current status of a window area feature. Contains
+ * the current {@link WindowAreaComponent.WindowAreaStatus} value corresponding to the
+ * rear display presentation feature, as well as the {@link DisplayMetrics} for the rear facing
+ * display.
+ */
+class RearDisplayPresentationStatus implements ExtensionWindowAreaStatus {
+
+ @WindowAreaComponent.WindowAreaStatus
+ private final int mWindowAreaStatus;
+
+ @NonNull
+ private final DisplayMetrics mDisplayMetrics;
+
+ RearDisplayPresentationStatus(@WindowAreaComponent.WindowAreaStatus int status,
+ @NonNull DisplayMetrics displayMetrics) {
+ mWindowAreaStatus = status;
+ mDisplayMetrics = displayMetrics;
+ }
+
+ /**
+ * Returns the {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus}
+ * value that relates to the current status of a feature.
+ */
+ @Override
+ @WindowAreaComponent.WindowAreaStatus
+ public int getWindowAreaStatus() {
+ return mWindowAreaStatus;
+ }
+
+ /**
+ * Returns the {@link DisplayMetrics} that corresponds to the window area that a feature
+ * interacts with. This is converted to size class information provided to developers.
+ */
+ @Override
+ @NonNull
+ public DisplayMetrics getWindowAreaDisplayMetrics() {
+ return mDisplayMetrics;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index 9118ee2bf125..1e6e50359cf3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -22,14 +22,25 @@ import android.app.Activity;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateRequest;
+import android.hardware.display.DisplayManager;
import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.Pair;
+import android.view.Display;
+import android.view.DisplayAddress;
+import android.view.Surface;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.WindowExtensions;
import androidx.window.extensions.core.util.function.Consumer;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -44,63 +55,98 @@ import java.util.concurrent.Executor;
public class WindowAreaComponentImpl implements WindowAreaComponent,
DeviceStateManager.DeviceStateCallback {
+ private static final int INVALID_DISPLAY_ADDRESS = -1;
private final Object mLock = new Object();
+ @NonNull
private final DeviceStateManager mDeviceStateManager;
+ @NonNull
+ private final DisplayManager mDisplayManager;
+ @NonNull
private final Executor mExecutor;
@GuardedBy("mLock")
private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>();
+ @GuardedBy("mLock")
+ private final ArraySet<Consumer<ExtensionWindowAreaStatus>>
+ mRearDisplayPresentationStatusListeners = new ArraySet<>();
private final int mRearDisplayState;
+ private final int mConcurrentDisplayState;
+ @NonNull
+ private final int[] mFoldedDeviceStates;
+ private long mRearDisplayAddress = INVALID_DISPLAY_ADDRESS;
@WindowAreaSessionState
private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
@GuardedBy("mLock")
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
@GuardedBy("mLock")
- private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE;
+ private int[] mCurrentSupportedDeviceStates;
+
+ @GuardedBy("mLock")
+ private DeviceStateRequest mRearDisplayStateRequest;
@GuardedBy("mLock")
- private DeviceStateRequest mDeviceStateRequest;
+ private RearDisplayPresentationController mRearDisplayPresentationController;
+
+ @Nullable
+ @GuardedBy("mLock")
+ private DisplayMetrics mRearDisplayMetrics;
+
+ @WindowAreaSessionState
+ @GuardedBy("mLock")
+ private int mLastReportedRearDisplayPresentationStatus;
public WindowAreaComponentImpl(@NonNull Context context) {
mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+ mDisplayManager = context.getSystemService(DisplayManager.class);
mExecutor = context.getMainExecutor();
+ mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedStates();
+ mFoldedDeviceStates = context.getResources().getIntArray(
+ R.array.config_foldedDeviceStates);
+
// TODO(b/236022708) Move rear display state to device state config file
mRearDisplayState = context.getResources().getInteger(
R.integer.config_deviceStateRearDisplay);
+ mConcurrentDisplayState = context.getResources().getInteger(
+ R.integer.config_deviceStateConcurrentRearDisplay);
+
mDeviceStateManager.registerCallback(mExecutor, this);
+ mRearDisplayAddress = getRearDisplayAddress(context);
}
/**
* Adds a listener interested in receiving updates on the RearDisplayStatus
* of the device. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
+ * extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
- * Depending on the initial state of the device, we will return either
+ * Rear display mode moves the calling application to the display on the device that is
+ * facing the same direction as the rear cameras. This would be the cover display on a fold-in
+ * style device when the device is opened.
+ *
+ * Depending on the initial state of the device, the {@link Consumer} will receive either
* {@link WindowAreaComponent#STATUS_AVAILABLE} or
* {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that
- * state respectively. When the rear display feature is triggered, we update the status to be
- * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_
- *
- * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently
- * enabled.
+ * state respectively. When the rear display feature is triggered, the status is updated to be
+ * {@link WindowAreaComponent#STATUS_ACTIVE}.
+ * TODO(b/240727590): Prefix with AREA_
*
* @param consumer {@link Consumer} interested in receiving updates to the status of
* rear display mode.
*/
+ @Override
public void addRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
mRearDisplayStatusListeners.add(consumer);
- // If current device state is still invalid, we haven't gotten our initial value yet
+ // If current device state is still invalid, the initial value has not been provided.
if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
return;
}
- consumer.accept(getCurrentStatus());
+ consumer.accept(getCurrentRearDisplayModeStatus());
}
}
@@ -108,6 +154,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
* Removes a listener no longer interested in receiving updates.
* @param consumer no longer interested in receiving updates to RearDisplayStatus
*/
+ @Override
public void removeRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
@@ -118,13 +165,17 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
/**
* Creates and starts a rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
+ * extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
- * When we enable rear display mode, we submit a request to {@link DeviceStateManager}
+ * Rear display mode moves the calling application to the display on the device that is
+ * facing the same direction as the rear cameras. This would be the cover display on a fold-in
+ * style device when the device is opened.
+ *
+ * When rear display mode is enabled, a request is made to {@link DeviceStateManager}
* to override the device state to the state that corresponds to RearDisplay
- * mode. When the {@link DeviceStateRequest} is activated, we let the
- * consumer know that the session is active by sending
+ * mode. When the {@link DeviceStateRequest} is activated, the provided {@link Consumer} is
+ * notified that the session is active by receiving
* {@link WindowAreaComponent#SESSION_STATE_ACTIVE}.
*
* @param activity to provide updates to the client on
@@ -132,19 +183,20 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
* @param rearDisplaySessionCallback to provide updates to the client on
* the status of the Session
*/
+ @Override
public void startRearDisplaySession(@NonNull Activity activity,
@NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) {
synchronized (mLock) {
- if (mDeviceStateRequest != null) {
+ if (mRearDisplayStateRequest != null) {
// Rear display session is already active
throw new IllegalStateException(
"Unable to start new rear display session as one is already active");
}
- mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
+ mRearDisplayStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
mDeviceStateManager.requestState(
- mDeviceStateRequest,
+ mRearDisplayStateRequest,
mExecutor,
- new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback)
+ new RearDisplayStateRequestCallbackAdapter(rearDisplaySessionCallback)
);
}
}
@@ -152,13 +204,14 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
/**
* Ends the current rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
- * provided by the developer.
+ * extensions, the result of the listener will be posted on the executor
+ * provided by the developer at the initial call site.
*/
+ @Override
public void endRearDisplaySession() {
synchronized (mLock) {
- if (mDeviceStateRequest != null || isRearDisplayActive()) {
- mDeviceStateRequest = null;
+ if (mRearDisplayStateRequest != null || isRearDisplayActive()) {
+ mRearDisplayStateRequest = null;
mDeviceStateManager.cancelStateRequest();
} else {
throw new IllegalStateException(
@@ -167,13 +220,245 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
}
}
+ /**
+ * Returns the{@link DisplayMetrics} associated with the rear facing display. If the rear facing
+ * display was not found in the display list, but we have already computed the
+ * {@link DisplayMetrics} for that display, we return the cached value. If no display has been
+ * found, then we return an empty {@link DisplayMetrics} value.
+ *
+ * TODO(b/267563768): Update with guidance from Display team for missing displays.
+ *
+ * @since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
@Override
- public void onBaseStateChanged(int state) {
+ @NonNull
+ public DisplayMetrics getRearDisplayMetrics() {
+ DisplayMetrics rearDisplayMetrics = null;
+
+ // DISPLAY_CATEGORY_REAR displays are only available when you are in the concurrent
+ // display state, so we have to look through all displays to match the address
+ final Display[] displays = mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
+
+
+ for (int i = 0; i < displays.length; i++) {
+ DisplayAddress.Physical address =
+ (DisplayAddress.Physical) displays[i].getAddress();
+ if (mRearDisplayAddress == address.getPhysicalDisplayId()) {
+ rearDisplayMetrics = new DisplayMetrics();
+ final Display rearDisplay = displays[i];
+
+ // We must always retrieve the metrics for the rear display regardless of if it is
+ // the default display or not.
+ rearDisplay.getRealMetrics(rearDisplayMetrics);
+
+ // TODO(b/287170025): This should be something like if (!rearDisplay.isEnabled)
+ // instead. Currently when the rear display is disabled, its state is STATE_OFF.
+ if (rearDisplay.getDisplayId() != Display.DEFAULT_DISPLAY) {
+ final Display defaultDisplay = mDisplayManager
+ .getDisplay(Display.DEFAULT_DISPLAY);
+ rotateRearDisplayMetricsIfNeeded(defaultDisplay.getRotation(),
+ rearDisplay.getRotation(), rearDisplayMetrics);
+ }
+ break;
+ }
+ }
+
synchronized (mLock) {
- mCurrentDeviceBaseState = state;
- if (state == mCurrentDeviceState) {
- updateStatusConsumers(getCurrentStatus());
+ // Update the rear display metrics with our latest value if one was received
+ if (rearDisplayMetrics != null) {
+ mRearDisplayMetrics = rearDisplayMetrics;
}
+
+ return Objects.requireNonNullElseGet(mRearDisplayMetrics, DisplayMetrics::new);
+ }
+ }
+
+ /**
+ * Adds a listener interested in receiving updates on the RearDisplayPresentationStatus
+ * of the device. Because this is being called from the OEM provided
+ * extensions, the result of the listener will be posted on the executor
+ * provided by the developer at the initial call site.
+ *
+ * Rear display presentation mode is a feature where an {@link Activity} can present
+ * additional content on a device with a second display that is facing the same direction
+ * as the rear camera (i.e. the cover display on a fold-in style device). The calling
+ * {@link Activity} does not move, whereas in rear display mode it does.
+ *
+ * This listener receives a {@link Pair} with the first item being the
+ * {@link WindowAreaComponent.WindowAreaStatus} that corresponds to the current status of the
+ * feature, and the second being the {@link DisplayMetrics} of the display that would be
+ * presented to when the feature is active.
+ *
+ * Depending on the initial state of the device, the {@link Consumer} will receive either
+ * {@link WindowAreaComponent#STATUS_AVAILABLE} or
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE} for the status value of the {@link Pair} if
+ * the feature is supported or not in that state respectively. Rear display presentation mode is
+ * currently not supported when the device is folded. When the rear display presentation feature
+ * is triggered, the status is updated to be {@link WindowAreaComponent#STATUS_UNAVAILABLE}.
+ * TODO(b/240727590): Prefix with AREA_
+ *
+ * TODO(b/239833099): Add a STATUS_ACTIVE option to let apps know if a feature is currently
+ * enabled.
+ *
+ * @param consumer {@link Consumer} interested in receiving updates to the status of
+ * rear display presentation mode.
+ */
+ @Override
+ public void addRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+ synchronized (mLock) {
+ mRearDisplayPresentationStatusListeners.add(consumer);
+
+ // If current device state is still invalid, the initial value has not been provided
+ if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+ return;
+ }
+ @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus();
+ DisplayMetrics metrics = currentStatus == STATUS_UNSUPPORTED ? new DisplayMetrics()
+ : getRearDisplayMetrics();
+ consumer.accept(
+ new RearDisplayPresentationStatus(currentStatus, metrics));
+ }
+ }
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ * @param consumer no longer interested in receiving updates to RearDisplayPresentationStatus
+ */
+ @Override
+ public void removeRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+ synchronized (mLock) {
+ mRearDisplayPresentationStatusListeners.remove(consumer);
+ }
+ }
+
+ /**
+ * Creates and starts a rear display presentation session and sends state updates to the
+ * consumer provided. This consumer will receive a constant represented by
+ * {@link WindowAreaSessionState} to represent the state of the current rear display
+ * session. It will be translated to a more friendly interface in the library.
+ *
+ * Because this is being called from the OEM provided extensions, the library
+ * will post the result of the listener on the executor provided by the developer.
+ *
+ * Rear display presentation mode refers to a feature where an {@link Activity} can present
+ * additional content on a device with a second display that is facing the same direction
+ * as the rear camera (i.e. the cover display on a fold-in style device). The calling
+ * {@link Activity} stays on the user-facing display.
+ *
+ * @param activity that the OEM implementation will use as a base
+ * context and to identify the source display area of the request.
+ * The reference to the activity instance must not be stored in the OEM
+ * implementation to prevent memory leaks.
+ * @param consumer to provide updates to the client on the status of the session
+ * @throws UnsupportedOperationException if this method is called when rear display presentation
+ * mode is not available. This could be to an incompatible device state or when
+ * another process is currently in this mode.
+ */
+ @Override
+ public void startRearDisplayPresentationSession(@NonNull Activity activity,
+ @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {
+ synchronized (mLock) {
+ if (mRearDisplayPresentationController != null) {
+ // Rear display presentation session is already active
+ throw new IllegalStateException(
+ "Unable to start new rear display presentation session as one is already "
+ + "active");
+ }
+ if (getCurrentRearDisplayPresentationModeStatus()
+ != WindowAreaComponent.STATUS_AVAILABLE) {
+ throw new IllegalStateException(
+ "Unable to start new rear display presentation session as the feature is "
+ + "is not currently available");
+ }
+
+ mRearDisplayPresentationController = new RearDisplayPresentationController(activity,
+ stateStatus -> {
+ synchronized (mLock) {
+ if (stateStatus == SESSION_STATE_INACTIVE) {
+ // If the last reported session status was VISIBLE
+ // then the ACTIVE state should be dispatched before INACTIVE
+ // due to not having a good mechanism to know when
+ // the content is no longer visible before it's fully removed
+ if (getLastReportedRearDisplayPresentationStatus()
+ == SESSION_STATE_CONTENT_VISIBLE) {
+ consumer.accept(SESSION_STATE_ACTIVE);
+ }
+ mRearDisplayPresentationController = null;
+ }
+ mLastReportedRearDisplayPresentationStatus = stateStatus;
+ consumer.accept(stateStatus);
+ }
+ });
+ RearDisplayPresentationRequestCallback deviceStateCallback =
+ new RearDisplayPresentationRequestCallback(activity,
+ mRearDisplayPresentationController);
+ DeviceStateRequest concurrentDisplayStateRequest = DeviceStateRequest.newBuilder(
+ mConcurrentDisplayState).build();
+
+ try {
+ mDeviceStateManager.requestState(
+ concurrentDisplayStateRequest,
+ mExecutor,
+ deviceStateCallback
+ );
+ } catch (SecurityException e) {
+ // If a SecurityException occurs when invoking DeviceStateManager#requestState
+ // (e.g. if the caller is not in the foreground, or if it does not have the required
+ // permissions), we should first clean up our local state before re-throwing the
+ // SecurityException to the caller. Otherwise, subsequent attempts to
+ // startRearDisplayPresentationSession will always fail.
+ mRearDisplayPresentationController = null;
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Ends the current rear display presentation session and provides updates to the
+ * callback provided. When this is ended, the presented content from the calling
+ * {@link Activity} will also be removed from the rear facing display.
+ * Because this is being called from the OEM provided extensions, the result of the listener
+ * will be posted on the executor provided by the developer at the initial call site.
+ *
+ * Cancelling the {@link DeviceStateRequest} and exiting the rear display presentation state,
+ * will remove the presentation window from the cover display as the cover display is no longer
+ * enabled.
+ */
+ @Override
+ public void endRearDisplayPresentationSession() {
+ synchronized (mLock) {
+ if (mRearDisplayPresentationController != null) {
+ mDeviceStateManager.cancelStateRequest();
+ } else {
+ throw new IllegalStateException(
+ "Unable to cancel a rear display presentation session as there is no "
+ + "active session");
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
+ synchronized (mLock) {
+ ExtensionWindowAreaPresentation presentation = null;
+ if (mRearDisplayPresentationController != null) {
+ presentation = mRearDisplayPresentationController.getWindowAreaPresentation();
+ }
+ return presentation;
+ }
+ }
+
+ @Override
+ public void onSupportedStatesChanged(int[] supportedStates) {
+ synchronized (mLock) {
+ mCurrentSupportedDeviceStates = supportedStates;
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
+ updateRearDisplayPresentationStatusListeners(
+ getCurrentRearDisplayPresentationModeStatus());
}
}
@@ -181,38 +466,45 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
public void onStateChanged(int state) {
synchronized (mLock) {
mCurrentDeviceState = state;
- updateStatusConsumers(getCurrentStatus());
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
+ updateRearDisplayPresentationStatusListeners(
+ getCurrentRearDisplayPresentationModeStatus());
}
}
@GuardedBy("mLock")
- private int getCurrentStatus() {
+ private int getCurrentRearDisplayModeStatus() {
if (mRearDisplayState == INVALID_DEVICE_STATE) {
return WindowAreaComponent.STATUS_UNSUPPORTED;
}
- if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
- || isRearDisplayActive()) {
+ if (!ArrayUtils.contains(mCurrentSupportedDeviceStates, mRearDisplayState)) {
return WindowAreaComponent.STATUS_UNAVAILABLE;
}
+
+ if (isRearDisplayActive()) {
+ return WindowAreaComponent.STATUS_ACTIVE;
+ }
+
return WindowAreaComponent.STATUS_AVAILABLE;
}
/**
* Helper method to determine if a rear display session is currently active by checking
- * if the current device configuration matches that of rear display. This would be true
- * if there is a device override currently active (base state != current state) and the current
- * state is that which corresponds to {@code mRearDisplayState}
- * @return {@code true} if the device is in rear display mode and {@code false} if not
+ * if the current device state is that which corresponds to {@code mRearDisplayState}.
+ *
+ * @return {@code true} if the device is in rear display state {@code false} if not
*/
@GuardedBy("mLock")
private boolean isRearDisplayActive() {
- return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState
- == mRearDisplayState);
+ return mCurrentDeviceState == mRearDisplayState;
}
@GuardedBy("mLock")
- private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) {
+ private void updateRearDisplayStatusListeners(@WindowAreaStatus int windowAreaStatus) {
+ if (mRearDisplayState == INVALID_DEVICE_STATE) {
+ return;
+ }
synchronized (mLock) {
for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) {
mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus);
@@ -220,26 +512,99 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
}
}
+ @GuardedBy("mLock")
+ private int getCurrentRearDisplayPresentationModeStatus() {
+ if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
+ return WindowAreaComponent.STATUS_UNSUPPORTED;
+ }
+
+ if (mCurrentDeviceState == mConcurrentDisplayState
+ || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mConcurrentDisplayState)
+ || isDeviceFolded()) {
+ return WindowAreaComponent.STATUS_UNAVAILABLE;
+ }
+ return WindowAreaComponent.STATUS_AVAILABLE;
+ }
+
+ @GuardedBy("mLock")
+ private boolean isDeviceFolded() {
+ return ArrayUtils.contains(mFoldedDeviceStates, mCurrentDeviceState);
+ }
+
+ @GuardedBy("mLock")
+ private void updateRearDisplayPresentationStatusListeners(
+ @WindowAreaStatus int windowAreaStatus) {
+ if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
+ return;
+ }
+ RearDisplayPresentationStatus consumerValue = new RearDisplayPresentationStatus(
+ windowAreaStatus, getRearDisplayMetrics());
+ synchronized (mLock) {
+ for (int i = 0; i < mRearDisplayPresentationStatusListeners.size(); i++) {
+ mRearDisplayPresentationStatusListeners.valueAt(i).accept(consumerValue);
+ }
+ }
+ }
+
+ private long getRearDisplayAddress(Context context) {
+ String address = context.getResources().getString(
+ R.string.config_rearDisplayPhysicalAddress);
+ return address.isEmpty() ? INVALID_DISPLAY_ADDRESS : Long.parseLong(address);
+ }
+
+ @GuardedBy("mLock")
+ @WindowAreaSessionState
+ private int getLastReportedRearDisplayPresentationStatus() {
+ return mLastReportedRearDisplayPresentationStatus;
+ }
+
+ @VisibleForTesting
+ static void rotateRearDisplayMetricsIfNeeded(
+ @Surface.Rotation int defaultDisplayRotation,
+ @Surface.Rotation int rearDisplayRotation,
+ @NonNull DisplayMetrics inOutMetrics) {
+ // If the rear display has a non-zero rotation, it means the backing DisplayContent /
+ // DisplayRotation is fresh.
+ if (rearDisplayRotation != Surface.ROTATION_0) {
+ return;
+ }
+
+ // If the default display is 0 or 180, the rear display must also be 0 or 180.
+ if (defaultDisplayRotation == Surface.ROTATION_0
+ || defaultDisplayRotation == Surface.ROTATION_180) {
+ return;
+ }
+
+ final int heightPixels = inOutMetrics.heightPixels;
+ final int widthPixels = inOutMetrics.widthPixels;
+ inOutMetrics.widthPixels = heightPixels;
+ inOutMetrics.heightPixels = widthPixels;
+
+ final int noncompatHeightPixels = inOutMetrics.noncompatHeightPixels;
+ final int noncompatWidthPixels = inOutMetrics.noncompatWidthPixels;
+ inOutMetrics.noncompatWidthPixels = noncompatHeightPixels;
+ inOutMetrics.noncompatHeightPixels = noncompatWidthPixels;
+ }
+
/**
* Callback for the {@link DeviceStateRequest} to be notified of when the request has been
* activated or cancelled. This callback provides information to the client library
* on the status of the RearDisplay session through {@code mRearDisplaySessionCallback}
*/
- private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
+ private class RearDisplayStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
private final Consumer<Integer> mRearDisplaySessionCallback;
- DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
+ RearDisplayStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
mRearDisplaySessionCallback = callback;
}
@Override
public void onRequestActivated(@NonNull DeviceStateRequest request) {
synchronized (mLock) {
- if (request.equals(mDeviceStateRequest)) {
+ if (request.equals(mRearDisplayStateRequest)) {
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
- updateStatusConsumers(getCurrentStatus());
}
}
}
@@ -247,46 +612,12 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
@Override
public void onRequestCanceled(DeviceStateRequest request) {
synchronized (mLock) {
- if (request.equals(mDeviceStateRequest)) {
- mDeviceStateRequest = null;
+ if (request.equals(mRearDisplayStateRequest)) {
+ mRearDisplayStateRequest = null;
}
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
- updateStatusConsumers(getCurrentStatus());
}
}
}
-
- @Override
- public void addRearDisplayPresentationStatusListener(
- @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
- throw new UnsupportedOperationException(
- "addRearDisplayPresentationStatusListener is not supported in API_VERSION=2");
- }
-
- @Override
- public void removeRearDisplayPresentationStatusListener(
- @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
- throw new UnsupportedOperationException(
- "removeRearDisplayPresentationStatusListener is not supported in API_VERSION=2");
- }
-
- @Override
- public void startRearDisplayPresentationSession(@NonNull Activity activity,
- @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {
- throw new UnsupportedOperationException(
- "startRearDisplayPresentationSession is not supported in API_VERSION=2");
- }
-
- @Override
- public void endRearDisplayPresentationSession() {
- throw new UnsupportedOperationException(
- "endRearDisplayPresentationSession is not supported in API_VERSION=2");
- }
-
- @Override
- public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
- throw new UnsupportedOperationException(
- "getRearDisplayPresentation is not supported in API_VERSION=2");
- }
}
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 00e13c94ea90..d94e8e426c4b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -109,38 +109,38 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* be resized based on {@param launchingFragmentBounds}.
* Otherwise, we will create a new TaskFragment with the given
* token for the {@param launchingActivity}.
- * @param launchingFragmentBounds the initial bounds for the launching TaskFragment.
+ * @param launchingRelBounds the initial relative bounds for the launching TaskFragment.
* @param launchingActivity the Activity to put on the left hand side of the split as the
* primary.
* @param secondaryFragmentToken token to create the secondary TaskFragment with.
- * @param secondaryFragmentBounds the initial bounds for the secondary TaskFragment
+ * @param secondaryRelBounds the initial relative bounds for the secondary TaskFragment
* @param activityIntent Intent to start the secondary Activity with.
* @param activityOptions ActivityOptions to start the secondary Activity with.
* @param windowingMode the windowing mode to set for the TaskFragments.
* @param splitAttributes the {@link SplitAttributes} to represent the split.
*/
void startActivityToSide(@NonNull WindowContainerTransaction wct,
- @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds,
+ @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingRelBounds,
@NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken,
- @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent,
+ @NonNull Rect secondaryRelBounds, @NonNull Intent activityIntent,
@Nullable Bundle activityOptions, @NonNull SplitRule rule,
@WindowingMode int windowingMode, @NonNull SplitAttributes splitAttributes) {
final IBinder ownerToken = launchingActivity.getActivityToken();
// Create or resize the launching TaskFragment.
if (mFragmentInfos.containsKey(launchingFragmentToken)) {
- resizeTaskFragment(wct, launchingFragmentToken, launchingFragmentBounds);
+ resizeTaskFragment(wct, launchingFragmentToken, launchingRelBounds);
updateWindowingMode(wct, launchingFragmentToken, windowingMode);
} else {
createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken,
- launchingFragmentBounds, windowingMode, launchingActivity);
+ launchingRelBounds, windowingMode, launchingActivity);
}
updateAnimationParams(wct, launchingFragmentToken, splitAttributes);
// Create a TaskFragment for the secondary activity.
final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
getOrganizerToken(), secondaryFragmentToken, ownerToken)
- .setInitialBounds(secondaryFragmentBounds)
+ .setInitialRelativeBounds(secondaryRelBounds)
.setWindowingMode(windowingMode)
// Make sure to set the paired fragment token so that the new TaskFragment will be
// positioned right above the paired TaskFragment.
@@ -154,7 +154,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
activityOptions);
// Set adjacent to each other so that the containers below will be invisible.
- setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule);
+ setAdjacentTaskFragmentsWithRule(wct, launchingFragmentToken, secondaryFragmentToken, rule);
setCompanionTaskFragment(wct, launchingFragmentToken, secondaryFragmentToken, rule,
false /* isStacked */);
}
@@ -167,7 +167,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
void expandTaskFragment(@NonNull WindowContainerTransaction wct,
@NonNull IBinder fragmentToken) {
resizeTaskFragment(wct, fragmentToken, new Rect());
- setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
+ clearAdjacentTaskFragments(wct, fragmentToken);
updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
}
@@ -190,8 +190,9 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* have to be a child of this task fragment, but must belong to the same task.
*/
void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
- @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
- createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
+ @NonNull IBinder ownerToken, @NonNull Rect relBounds,
+ @WindowingMode int windowingMode) {
+ createTaskFragment(wct, fragmentToken, ownerToken, relBounds, windowingMode,
null /* pairedActivityToken */);
}
@@ -203,11 +204,11 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* positioned right above it.
*/
void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
- @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode,
+ @NonNull IBinder ownerToken, @NonNull Rect relBounds, @WindowingMode int windowingMode,
@Nullable IBinder pairedActivityToken) {
final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
getOrganizerToken(), fragmentToken, ownerToken)
- .setInitialBounds(bounds)
+ .setInitialRelativeBounds(relBounds)
.setWindowingMode(windowingMode)
.setPairedActivityToken(pairedActivityToken)
.build();
@@ -229,29 +230,45 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* have to be a child of this task fragment, but must belong to the same task.
*/
private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
- @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
+ @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relBounds,
@WindowingMode int windowingMode, @NonNull Activity activity) {
final IBinder reparentActivityToken = activity.getActivityToken();
- createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
+ createTaskFragment(wct, fragmentToken, ownerToken, relBounds, windowingMode,
reparentActivityToken);
wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken);
}
- void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
- @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) {
+ /**
+ * Sets the two given TaskFragments as adjacent to each other with respecting the given
+ * {@link SplitRule} for {@link WindowContainerTransaction.TaskFragmentAdjacentParams}.
+ */
+ void setAdjacentTaskFragmentsWithRule(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule) {
WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null;
final boolean finishSecondaryWithPrimary =
- splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
+ SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
final boolean finishPrimaryWithSecondary =
- splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
+ SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) {
adjacentParams = new WindowContainerTransaction.TaskFragmentAdjacentParams();
adjacentParams.setShouldDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary);
adjacentParams.setShouldDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary);
}
+ setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
+ }
+
+ void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder primary, @NonNull IBinder secondary,
+ @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
}
+ void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
+ // Clear primary will also clear secondary.
+ wct.clearAdjacentTaskFragments(fragmentToken);
+ }
+
void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct,
@NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule,
boolean isStacked) {
@@ -262,7 +279,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
} else {
finishPrimaryWithSecondary = shouldFinishPrimaryWithSecondary(splitRule);
}
- wct.setCompanionTaskFragment(primary, finishPrimaryWithSecondary ? secondary : null);
+ setCompanionTaskFragment(wct, primary, finishPrimaryWithSecondary ? secondary : null);
final boolean finishSecondaryWithPrimary;
if (isStacked) {
@@ -271,19 +288,24 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
} else {
finishSecondaryWithPrimary = shouldFinishSecondaryWithPrimary(splitRule);
}
- wct.setCompanionTaskFragment(secondary, finishSecondaryWithPrimary ? primary : null);
+ setCompanionTaskFragment(wct, secondary, finishSecondaryWithPrimary ? primary : null);
+ }
+
+ void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
+ @Nullable IBinder secondary) {
+ wct.setCompanionTaskFragment(primary, secondary);
}
void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
- @Nullable Rect bounds) {
+ @Nullable Rect relBounds) {
if (!mFragmentInfos.containsKey(fragmentToken)) {
throw new IllegalArgumentException(
"Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
}
- if (bounds == null) {
- bounds = new Rect();
+ if (relBounds == null) {
+ relBounds = new Rect();
}
- wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds);
+ wct.setRelativeBounds(mFragmentInfos.get(fragmentToken).getToken(), relBounds);
}
void updateWindowingMode(@NonNull WindowContainerTransaction wct,
@@ -310,16 +332,12 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
OP_TYPE_SET_ANIMATION_PARAMS)
.setAnimationParams(animationParams)
.build();
- wct.setTaskFragmentOperation(fragmentToken, operation);
+ wct.addTaskFragmentOperation(fragmentToken, operation);
}
void deleteTaskFragment(@NonNull WindowContainerTransaction wct,
@NonNull IBinder fragmentToken) {
- if (!mFragmentInfos.containsKey(fragmentToken)) {
- throw new IllegalArgumentException(
- "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
- }
- wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken());
+ wct.deleteTaskFragment(fragmentToken);
}
void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 77284c4166bd..18497ad249ee 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -17,12 +17,15 @@
package androidx.window.extensions.embedding;
import android.app.Activity;
-import android.content.res.Configuration;
+import android.os.Binder;
+import android.os.IBinder;
import android.util.Pair;
import android.util.Size;
+import android.window.TaskFragmentParentInfo;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Function;
/**
* Client-side descriptor of a split that holds two containers.
@@ -34,8 +37,14 @@ class SplitContainer {
private final TaskFragmentContainer mSecondaryContainer;
@NonNull
private final SplitRule mSplitRule;
+ /** @see SplitContainer#getCurrentSplitAttributes() */
@NonNull
- private SplitAttributes mSplitAttributes;
+ private SplitAttributes mCurrentSplitAttributes;
+ /** @see SplitContainer#getDefaultSplitAttributes() */
+ @NonNull
+ private SplitAttributes mDefaultSplitAttributes;
+ @NonNull
+ private final IBinder mToken;
SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
@NonNull Activity primaryActivity,
@@ -45,7 +54,9 @@ class SplitContainer {
mPrimaryContainer = primaryContainer;
mSecondaryContainer = secondaryContainer;
mSplitRule = splitRule;
- mSplitAttributes = splitAttributes;
+ mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes();
+ mCurrentSplitAttributes = splitAttributes;
+ mToken = new Binder("SplitContainer");
if (shouldFinishPrimaryWithSecondary(splitRule)) {
if (mPrimaryContainer.getRunningActivityCount() == 1
@@ -78,19 +89,60 @@ class SplitContainer {
return mSplitRule;
}
+ /**
+ * Returns the current {@link SplitAttributes} this {@code SplitContainer} is showing.
+ * <p>
+ * If the {@code SplitAttributes} calculator function is not set by
+ * {@link SplitController#setSplitAttributesCalculator(Function)}, the current
+ * {@code SplitAttributes} is either to expand the containers if the size constraints of
+ * {@link #getSplitRule()} are not satisfied,
+ * or the {@link #getDefaultSplitAttributes()}, otherwise.
+ * </p><p>
+ * If the {@code SplitAttributes} calculator function is set, the current
+ * {@code SplitAttributes} will be customized by the function, which can be any
+ * {@code SplitAttributes}.
+ * </p>
+ *
+ * @see SplitAttributes.SplitType.ExpandContainersSplitType
+ */
+ @NonNull
+ SplitAttributes getCurrentSplitAttributes() {
+ return mCurrentSplitAttributes;
+ }
+
+ /**
+ * Returns the default {@link SplitAttributes} when the parent task container bounds satisfy
+ * {@link #getSplitRule()} constraints.
+ * <p>
+ * The value is usually from {@link SplitRule#getDefaultSplitAttributes} unless it is overridden
+ * by {@link SplitController#updateSplitAttributes(IBinder, SplitAttributes)}.
+ */
+ @NonNull
+ SplitAttributes getDefaultSplitAttributes() {
+ return mDefaultSplitAttributes;
+ }
+
@NonNull
- SplitAttributes getSplitAttributes() {
- return mSplitAttributes;
+ IBinder getToken() {
+ return mToken;
}
/**
* Updates the {@link SplitAttributes} to this container.
* It is usually used when there's a folding state change or
- * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, int,
- * Configuration)}.
+ * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction,
+ * int, TaskFragmentParentInfo)}.
+ */
+ void updateCurrentSplitAttributes(@NonNull SplitAttributes splitAttributes) {
+ mCurrentSplitAttributes = splitAttributes;
+ }
+
+ /**
+ * Overrides the default {@link SplitAttributes} to this container, which may be different
+ * from {@link SplitRule#getDefaultSplitAttributes}.
*/
- void setSplitAttributes(@NonNull SplitAttributes splitAttributes) {
- mSplitAttributes = splitAttributes;
+ void updateDefaultSplitAttributes(@NonNull SplitAttributes splitAttributes) {
+ mDefaultSplitAttributes = splitAttributes;
}
@NonNull
@@ -112,7 +164,7 @@ class SplitContainer {
@NonNull
SplitInfo toSplitInfo() {
return new SplitInfo(mPrimaryContainer.toActivityStack(),
- mSecondaryContainer.toActivityStack(), mSplitAttributes);
+ mSecondaryContainer.toActivityStack(), mCurrentSplitAttributes, mToken);
}
static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
@@ -171,9 +223,10 @@ class SplitContainer {
public String toString() {
return "SplitContainer{"
+ " primaryContainer=" + mPrimaryContainer
- + " secondaryContainer=" + mSecondaryContainer
- + " splitRule=" + mSplitRule
- + " splitAttributes" + mSplitAttributes
+ + ", secondaryContainer=" + mSecondaryContainer
+ + ", splitRule=" + mSplitRule
+ + ", currentSplitAttributes" + mCurrentSplitAttributes
+ + ", defaultSplitAttributes" + mDefaultSplitAttributes
+ "}";
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 569eb801989b..89f4890c254e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -20,19 +20,19 @@ import static android.app.ActivityManager.START_SUCCESS;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
+import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
-import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
-import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
@@ -40,7 +40,9 @@ import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceh
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
+import static androidx.window.extensions.embedding.SplitPresenter.getActivitiesMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
+import static androidx.window.extensions.embedding.SplitPresenter.getTaskWindowMetrics;
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;
import android.app.Activity;
@@ -67,6 +69,7 @@ import android.util.SparseArray;
import android.view.WindowMetrics;
import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOperation;
import android.window.TaskFragmentParentInfo;
import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
@@ -86,6 +89,7 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -98,7 +102,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
ActivityEmbeddingComponent {
static final String TAG = "SplitController";
static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
@VisibleForTesting
@GuardedBy("mLock")
@@ -229,6 +233,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return mSplitAttributesCalculator;
}
+ @Override
+ @NonNull
+ public ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options,
+ @NonNull IBinder token) {
+ options.setLaunchTaskFragmentToken(token);
+ return options;
+ }
+
@NonNull
@GuardedBy("mLock")
@VisibleForTesting
@@ -270,6 +282,102 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
+ @Override
+ public void finishActivityStacks(@NonNull Set<IBinder> activityStackTokens) {
+ if (activityStackTokens.isEmpty()) {
+ return;
+ }
+ synchronized (mLock) {
+ // Translate ActivityStack to TaskFragmentContainer.
+ final List<TaskFragmentContainer> pendingFinishingContainers =
+ activityStackTokens.stream()
+ .map(token -> {
+ synchronized (mLock) {
+ return getContainer(token);
+ }
+ }).filter(Objects::nonNull)
+ .toList();
+
+ if (pendingFinishingContainers.isEmpty()) {
+ return;
+ }
+ // Start transaction with close transit type.
+ final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+
+ forAllTaskContainers(taskContainer -> {
+ synchronized (mLock) {
+ final List<TaskFragmentContainer> containers = taskContainer.mContainers;
+ // Clean up the TaskFragmentContainers by the z-order from the lowest.
+ for (int i = 0; i < containers.size(); i++) {
+ final TaskFragmentContainer container = containers.get(i);
+ if (pendingFinishingContainers.contains(container)) {
+ // Don't update records here to prevent double invocation.
+ container.finish(false /* shouldFinishDependant */, mPresenter,
+ wct, this, false /* shouldRemoveRecord */);
+ }
+ }
+ // Remove container records.
+ removeContainers(taskContainer, pendingFinishingContainers);
+ // Update the change to the server side.
+ updateContainersInTaskIfVisible(wct, taskContainer.getTaskId());
+ }
+ });
+
+ // Apply the transaction.
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ }
+ }
+
+ @Override
+ public void invalidateTopVisibleSplitAttributes() {
+ synchronized (mLock) {
+ WindowContainerTransaction wct = mTransactionManager.startNewTransaction()
+ .getTransaction();
+ forAllTaskContainers(taskContainer -> {
+ synchronized (mLock) {
+ updateContainersInTaskIfVisible(wct, taskContainer.getTaskId());
+ }
+ });
+ mTransactionManager.getCurrentTransactionRecord()
+ .apply(false /* shouldApplyIndependently */);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void forAllTaskContainers(@NonNull Consumer<TaskContainer> callback) {
+ for (int i = mTaskContainers.size() - 1; i >= 0; --i) {
+ callback.accept(mTaskContainers.valueAt(i));
+ }
+ }
+
+ @Override
+ public void updateSplitAttributes(@NonNull IBinder splitInfoToken,
+ @NonNull SplitAttributes splitAttributes) {
+ Objects.requireNonNull(splitInfoToken);
+ Objects.requireNonNull(splitAttributes);
+ synchronized (mLock) {
+ final SplitContainer splitContainer = getSplitContainer(splitInfoToken);
+ if (splitContainer == null) {
+ Log.w(TAG, "Cannot find SplitContainer for token:" + splitInfoToken);
+ return;
+ }
+ // Override the default split Attributes so that it will be applied
+ // if the SplitContainer is not visible currently.
+ splitContainer.updateDefaultSplitAttributes(splitAttributes);
+
+ final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+ if (updateSplitContainerIfNeeded(splitContainer, wct, splitAttributes)) {
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ } else {
+ // Abort if the SplitContainer wasn't updated.
+ transactionRecord.abort();
+ }
+ }
+ }
+
/**
* Called when the transaction is ready so that the organizer can update the TaskFragments based
* on the changes in transaction.
@@ -355,7 +463,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
container.setInfo(wct, taskFragmentInfo);
if (container.isFinished()) {
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else {
// Update with the latest Task configuration.
@@ -391,22 +500,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Do not finish the dependents if the last activity is reparented to PiP.
// Instead, the original split should be cleanup, and the dependent may be
// expanded to fullscreen.
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
cleanupForEnterPip(wct, container);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (taskFragmentInfo.isTaskClearedForReuse()) {
// Do not finish the dependents if this TaskFragment was cleared due to
// launching activity in the Task.
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (taskFragmentInfo.isClearedForReorderActivityToFront()) {
// Do not finish the dependents if this TaskFragment was cleared to reorder
// the launching Activity to front of the Task.
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (!container.isWaitingActivityAppear()) {
// Do not finish the container before the expected activity appear until
// timeout.
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
}
} else if (wasInPip && isInPip) {
@@ -417,6 +531,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// All overrides will be cleanup.
container.setLastRequestedBounds(null /* bounds */);
container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED);
+ container.clearLastAdjacentTaskFragment();
+ container.setLastCompanionTaskFragment(null /* fragmentToken */);
+ container.setLastRequestAnimationParams(TaskFragmentAnimationParams.DEFAULT);
cleanupForEnterPip(wct, container);
} else if (wasInPip) {
// Exit PIP.
@@ -586,11 +703,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
@Nullable IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo,
- int opType, @NonNull Throwable exception) {
+ @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) {
Log.e(TAG, "onTaskFragmentError=" + exception.getMessage());
switch (opType) {
- case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
- case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: {
+ case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
+ case OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: {
final TaskFragmentContainer container;
if (taskFragmentInfo != null) {
container = getContainer(taskFragmentInfo.getFragmentToken());
@@ -605,7 +722,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
container.setInfo(wct, taskFragmentInfo);
container.clearPendingAppearedActivities();
if (container.isEmpty()) {
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
}
break;
@@ -632,41 +750,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
- /** Returns whether the given {@link TaskContainer} may show in split. */
- // Suppress GuardedBy warning because lint asks to mark this method as
- // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
- @SuppressWarnings("GuardedBy")
- @GuardedBy("mLock")
- private boolean mayShowSplit(@NonNull TaskContainer taskContainer) {
- // No split inside PIP.
- if (taskContainer.isInPictureInPicture()) {
- return false;
- }
- // Always assume the TaskContainer if SplitAttributesCalculator is set
- if (mSplitAttributesCalculator != null) {
- return true;
- }
- // Check if the parent container bounds can support any split rule.
- for (EmbeddingRule rule : mSplitRules) {
- if (!(rule instanceof SplitRule)) {
- continue;
- }
- final SplitRule splitRule = (SplitRule) rule;
- final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
- taskContainer.getTaskProperties(), splitRule, null /* minDimensionsPair */);
- if (shouldShowSplit(splitAttributes)) {
- return true;
- }
- }
- return false;
- }
-
@VisibleForTesting
@GuardedBy("mLock")
void onActivityCreated(@NonNull WindowContainerTransaction wct,
@NonNull Activity launchedActivity) {
- // TODO(b/229680885): we don't support launching into primary yet because we want to always
- // launch the new activity on top.
resolveActivityToContainer(wct, launchedActivity, false /* isOnReparent */);
updateCallbackIfNecessary();
}
@@ -702,6 +789,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return true;
}
+ final TaskFragmentContainer container = getContainerWithActivity(activity);
+ if (!isOnReparent && container != null
+ && container.getTaskContainer().getTopTaskFragmentContainer() != container) {
+ // Do not resolve if the launched activity is not the top-most container in the Task.
+ return true;
+ }
+
/*
* We will check the following to see if there is any embedding rule matched:
* 1. Whether the new launched activity should always expand.
@@ -724,6 +818,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return true;
}
+ // Skip resolving the following split-rules if the launched activity has been requested
+ // to be launched into its current container.
+ if (container != null && container.isActivityInRequestedTaskFragment(
+ activity.getActivityToken())) {
+ return true;
+ }
+
// 3. Whether the new launched activity has already been in a split with a rule matched.
if (isNewActivityInSplitWithRuleMatched(activity)) {
return true;
@@ -901,7 +1002,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
Activity findActivityBelow(@NonNull Activity activity) {
Activity activityBelow = null;
final TaskFragmentContainer container = getContainerWithActivity(activity);
- if (container != null) {
+ // Looking for the activity below from the information we already have if the container
+ // only embeds activities of the same process because activities of other processes are not
+ // available in this embedding host process for security concern.
+ if (container != null && !container.hasCrossProcessActivities()) {
final List<Activity> containerActivities = container.collectNonFinishingActivities();
final int index = containerActivities.indexOf(activity);
if (index > 0) {
@@ -935,9 +1039,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final TaskFragmentContainer primaryContainer = getContainerWithActivity(
primaryActivity);
final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer);
- final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity);
+ final TaskContainer.TaskProperties taskProperties = mPresenter
+ .getTaskProperties(primaryActivity);
+ final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes(
+ taskProperties, splitRule, splitRule.getDefaultSplitAttributes(),
+ getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity));
if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
- && canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)) {
+ && canReuseContainer(splitRule, splitContainer.getSplitRule(),
+ getTaskWindowMetrics(taskProperties.getConfiguration()),
+ calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes())) {
// Can launch in the existing secondary container if the rules share the same
// presentation.
final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
@@ -956,7 +1066,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
// Create new split pair.
- mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule);
+ mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule,
+ calculatedSplitAttributes);
return true;
}
@@ -1022,7 +1133,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
}
@@ -1180,9 +1292,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity);
final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer);
- final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity);
+ final TaskContainer.TaskProperties taskProperties = mPresenter
+ .getTaskProperties(primaryActivity);
+ final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(
+ taskProperties.getConfiguration());
+ final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes(
+ taskProperties, splitRule, splitRule.getDefaultSplitAttributes(),
+ getActivityIntentMinDimensionsPair(primaryActivity, intent));
if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer()
- && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)
+ && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics,
+ calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes())
// TODO(b/231845476) we should always respect clearTop.
|| !respectClearTop)
&& mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
@@ -1193,7 +1312,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
// Create a new TaskFragment to split with the primary activity for the new activity.
return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent,
- splitRule);
+ splitRule, calculatedSplitAttributes);
}
/**
@@ -1343,20 +1462,33 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Removes the container from bookkeeping records.
*/
void removeContainer(@NonNull TaskFragmentContainer container) {
+ removeContainers(container.getTaskContainer(), Collections.singletonList(container));
+ }
+
+ /**
+ * Removes containers from bookkeeping records.
+ */
+ void removeContainers(@NonNull TaskContainer taskContainer,
+ @NonNull List<TaskFragmentContainer> containers) {
// Remove all split containers that included this one
- final TaskContainer taskContainer = container.getTaskContainer();
- taskContainer.mContainers.remove(container);
+ taskContainer.mContainers.removeAll(containers);
// Marked as a pending removal which will be removed after it is actually removed on the
// server side (#onTaskFragmentVanished).
// In this way, we can keep track of the Task bounds until we no longer have any
// TaskFragment there.
- taskContainer.mFinishedContainer.add(container.getTaskFragmentToken());
+ taskContainer.mFinishedContainer.addAll(containers.stream().map(
+ TaskFragmentContainer::getTaskFragmentToken).toList());
// Cleanup any split references.
final List<SplitContainer> containersToRemove = new ArrayList<>();
for (SplitContainer splitContainer : taskContainer.mSplitContainers) {
- if (container.equals(splitContainer.getSecondaryContainer())
- || container.equals(splitContainer.getPrimaryContainer())) {
+ if (containersToRemove.contains(splitContainer)) {
+ // Don't need to check because it has been in the remove list.
+ continue;
+ }
+ if (containers.stream().anyMatch(container ->
+ splitContainer.getPrimaryContainer().equals(container)
+ || splitContainer.getSecondaryContainer().equals(container))) {
containersToRemove.add(splitContainer);
}
}
@@ -1364,7 +1496,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Cleanup any dependent references.
for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) {
- containerToUpdate.removeContainerToFinishOnExit(container);
+ containerToUpdate.removeContainersToFinishOnExit(containers);
}
}
@@ -1444,26 +1576,54 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (splitContainer == null) {
return;
}
+
+ updateSplitContainerIfNeeded(splitContainer, wct, null /* splitAttributes */);
+ }
+
+ /**
+ * Updates {@link SplitContainer} with the given {@link SplitAttributes} if the
+ * {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes}
+ * are {@code null}, the {@link SplitAttributes} will be calculated with
+ * {@link SplitPresenter#computeSplitAttributes(TaskContainer.TaskProperties, SplitRule, Pair)}.
+ *
+ * @param splitContainer The {@link SplitContainer} to update
+ * @param splitAttributes Update with this {@code splitAttributes} if it is not {@code null}.
+ * Otherwise, use the value calculated by
+ * {@link SplitPresenter#computeSplitAttributes(
+ * TaskContainer.TaskProperties, SplitRule, Pair)}
+ *
+ * @return {@code true} if the update succeed. Otherwise, returns {@code false}.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ boolean updateSplitContainerIfNeeded(@NonNull SplitContainer splitContainer,
+ @NonNull WindowContainerTransaction wct, @Nullable SplitAttributes splitAttributes) {
if (!isTopMostSplit(splitContainer)) {
// Skip position update - it isn't the topmost split.
- return;
+ return false;
}
if (splitContainer.getPrimaryContainer().isFinished()
|| splitContainer.getSecondaryContainer().isFinished()) {
// Skip position update - one or both containers are finished.
- return;
+ return false;
}
- final TaskContainer taskContainer = splitContainer.getTaskContainer();
- final SplitRule splitRule = splitContainer.getSplitRule();
- final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
- final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
- taskContainer.getTaskProperties(), splitRule, minDimensionsPair);
- splitContainer.setSplitAttributes(splitAttributes);
+ if (splitAttributes == null) {
+ final TaskContainer.TaskProperties taskProperties = splitContainer.getTaskContainer()
+ .getTaskProperties();
+ final SplitRule splitRule = splitContainer.getSplitRule();
+ final SplitAttributes defaultSplitAttributes = splitContainer
+ .getDefaultSplitAttributes();
+ final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
+ splitAttributes = mPresenter.computeSplitAttributes(taskProperties, splitRule,
+ defaultSplitAttributes, minDimensionsPair);
+ }
+ splitContainer.updateCurrentSplitAttributes(splitAttributes);
if (dismissPlaceholderIfNecessary(wct, splitContainer)) {
// Placeholder was finished, the positions will be updated when its container is emptied
- return;
+ return true;
}
- mPresenter.updateSplitContainer(splitContainer, container, wct);
+ mPresenter.updateSplitContainer(splitContainer, wct);
+ return true;
}
/** Whether the given split is the topmost split in the Task. */
@@ -1559,7 +1719,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity,
placeholderRule.getPlaceholderIntent());
final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties,
- placeholderRule, minDimensionsPair);
+ placeholderRule, placeholderRule.getDefaultSplitAttributes(), minDimensionsPair);
if (!SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
@@ -1614,7 +1774,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// (not resumed yet).
if (isOnCreated || primaryActivity.isResumed()) {
// Only set trigger type if the launch happens in foreground.
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_OPEN);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_OPEN);
return null;
}
final ActivityOptions options = ActivityOptions.makeBasic();
@@ -1637,12 +1798,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// The placeholder should remain after it was first shown.
return false;
}
- final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
+ final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
if (SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
- mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(),
false /* shouldFinishDependent */);
return true;
@@ -1778,6 +1940,21 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return null;
}
+ @VisibleForTesting
+ @Nullable
+ @GuardedBy("mLock")
+ SplitContainer getSplitContainer(@NonNull IBinder token) {
+ for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
+ final List<SplitContainer> containers = mTaskContainers.valueAt(i).mSplitContainers;
+ for (SplitContainer container : containers) {
+ if (container.getToken().equals(token)) {
+ return container;
+ }
+ }
+ }
+ return null;
+ }
+
@Nullable
@GuardedBy("mLock")
TaskContainer getTaskContainer(int taskId) {
@@ -1920,6 +2097,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (!container.hasActivity(activityToken)
&& container.getTaskFragmentToken()
.equals(initialTaskFragmentToken)) {
+ if (ActivityClient.getInstance().isRequestedToLaunchInTaskFragment(
+ activityToken, initialTaskFragmentToken)) {
+ container.addPendingAppearedInRequestedTaskFragmentActivity(
+ activity);
+ }
+
// The onTaskFragmentInfoChanged callback containing this activity has
// not reached the client yet, so add the activity to the pending
// appeared activities.
@@ -1946,7 +2129,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
synchronized (mLock) {
final TransactionRecord transactionRecord = mTransactionManager
.startNewTransaction();
- transactionRecord.setOriginType(TRANSIT_OPEN);
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_OPEN);
SplitController.this.onActivityCreated(transactionRecord.getTransaction(),
activity);
// The WCT should be applied and merged to the activity launch transition.
@@ -2011,6 +2194,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// TODO(b/232042367): Consolidate the activity create handling so that we can handle
// cross-process the same as normal.
+ // Early return if the launching taskfragment is already been set.
+ if (options.getBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) {
+ synchronized (mLock) {
+ mCurrentIntent = intent;
+ }
+ return super.onStartActivity(who, intent, options);
+ }
+
final Activity launchingActivity;
if (who instanceof Activity) {
// We will check if the new activity should be split with the activity that launched
@@ -2036,7 +2227,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
synchronized (mLock) {
final TransactionRecord transactionRecord = mTransactionManager
.startNewTransaction();
- transactionRecord.setOriginType(TRANSIT_OPEN);
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_OPEN);
final WindowContainerTransaction wct = transactionRecord.getTransaction();
final TaskFragmentContainer launchedInTaskFragment;
if (launchingActivity != null) {
@@ -2053,6 +2244,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
transactionRecord.apply(false /* shouldApplyIndependently */);
// Amend the request to let the WM know that the activity should be placed in
// the dedicated container.
+ // TODO(b/229680885): skip override launching TaskFragment token by split-rule
options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
launchedInTaskFragment.getTaskFragmentToken());
mCurrentIntent = intent;
@@ -2097,21 +2289,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
/**
- * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if
- * there is any.
+ * If the two rules have the same presentation, and the calculated {@link SplitAttributes}
+ * matches the {@link SplitAttributes} of {@link SplitContainer}, we can reuse the same
+ * {@link SplitContainer} if there is any.
*/
private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2,
- @NonNull WindowMetrics parentWindowMetrics) {
+ @NonNull WindowMetrics parentWindowMetrics,
+ @NonNull SplitAttributes calculatedSplitAttributes,
+ @NonNull SplitAttributes containerSplitAttributes) {
if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) {
return false;
}
- return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2,
- parentWindowMetrics);
+ return areRulesSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2,
+ parentWindowMetrics)
+ // Besides rules, we should also check whether the SplitContainer's splitAttributes
+ // matches the current splitAttributes or not. The splitAttributes may change
+ // if the app chooses different SplitAttributes calculator function before a new
+ // activity is started even they match the same splitRule.
+ && calculatedSplitAttributes.equals(containerSplitAttributes);
}
/** Whether the two rules have the same presentation. */
@VisibleForTesting
- static boolean haveSamePresentation(@NonNull SplitPairRule rule1,
+ static boolean areRulesSamePresentation(@NonNull SplitPairRule rule1,
@NonNull SplitPairRule rule2, @NonNull WindowMetrics parentWindowMetrics) {
if (rule1.getTag() != null || rule2.getTag() != null) {
// Tag must be unique if it is set. We don't want to reuse the container if the rules
@@ -2156,30 +2356,4 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return configuration != null
&& configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED;
}
-
- @Override
- public ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options,
- @NonNull IBinder token) {
- throw new UnsupportedOperationException(
- "setLaunchingActivityStack is not supported in API_VERSION=2");
- }
-
- @Override
- public void finishActivityStacks(@NonNull Set<IBinder> activityStackTokens) {
- throw new UnsupportedOperationException(
- "finishActivityStacks is not supported in API_VERSION=2");
- }
-
- @Override
- public void invalidateTopVisibleSplitAttributes() {
- throw new UnsupportedOperationException(
- "invalidateTopVisibleSplitAttributes is not supported in API_VERSION=2");
- }
-
- @Override
- public void updateSplitAttributes(@NonNull IBinder splitInfoToken,
- @NonNull SplitAttributes splitAttributes) {
- throw new UnsupportedOperationException(
- "updateSplitAttributes is not supported in API_VERSION=2");
- }
}
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 c23ac75e696f..53d39d9fa28e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -30,6 +30,7 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
+import android.util.DisplayMetrics;
import android.util.LayoutDirection;
import android.util.Pair;
import android.util.Size;
@@ -173,28 +174,24 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull
TaskFragmentContainer createNewSplitWithEmptySideContainer(
@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
- @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) {
+ @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule,
+ @NonNull SplitAttributes splitAttributes) {
final TaskProperties taskProperties = getTaskProperties(primaryActivity);
- final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
- primaryActivity, secondaryIntent);
- final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
- minDimensionsPair);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
- primaryActivity, primaryRectBounds, splitAttributes, null /* containerToAvoid */);
+ primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */);
// Create new empty task fragment
final int taskId = primaryContainer.getTaskId();
final TaskFragmentContainer secondaryContainer = mController.newContainer(
secondaryIntent, primaryActivity, taskId);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
splitAttributes);
final int windowingMode = mController.getTaskContainer(taskId)
- .getWindowingModeForSplitTaskFragment(secondaryRectBounds);
+ .getWindowingModeForSplitTaskFragment(secondaryRelBounds);
createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
- primaryActivity.getActivityToken(), secondaryRectBounds,
- windowingMode);
+ primaryActivity.getActivityToken(), secondaryRelBounds, windowingMode);
updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
// Set adjacent to each other so that the containers below will be invisible.
@@ -217,21 +214,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
* same container as the primary activity, a new container will be
* created and the activity will be re-parented to it.
* @param rule The split rule to be applied to the container.
+ * @param splitAttributes The {@link SplitAttributes} to apply
*/
void createNewSplitContainer(@NonNull WindowContainerTransaction wct,
@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity,
- @NonNull SplitPairRule rule) {
+ @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes) {
final TaskProperties taskProperties = getTaskProperties(primaryActivity);
- final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
- secondaryActivity);
- final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
- minDimensionsPair);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
- primaryActivity, primaryRectBounds, splitAttributes, null /* containerToAvoid */);
+ primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
splitAttributes);
final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
secondaryActivity);
@@ -243,7 +237,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
containerToAvoid = curSecondaryContainer;
}
final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
- secondaryActivity, secondaryRectBounds, splitAttributes, containerToAvoid);
+ secondaryActivity, secondaryRelBounds, splitAttributes, containerToAvoid);
// Set adjacent to each other so that the containers below will be invisible.
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
@@ -260,23 +254,23 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
*/
private TaskFragmentContainer prepareContainerForActivity(
@NonNull WindowContainerTransaction wct, @NonNull Activity activity,
- @NonNull Rect bounds, @NonNull SplitAttributes splitAttributes,
+ @NonNull Rect relBounds, @NonNull SplitAttributes splitAttributes,
@Nullable TaskFragmentContainer containerToAvoid) {
TaskFragmentContainer container = mController.getContainerWithActivity(activity);
final int taskId = container != null ? container.getTaskId() : activity.getTaskId();
if (container == null || container == containerToAvoid) {
container = mController.newContainer(activity, taskId);
final int windowingMode = mController.getTaskContainer(taskId)
- .getWindowingModeForSplitTaskFragment(bounds);
+ .getWindowingModeForSplitTaskFragment(relBounds);
final IBinder reparentActivityToken = activity.getActivityToken();
createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken,
- bounds, windowingMode, reparentActivityToken);
+ relBounds, windowingMode, reparentActivityToken);
wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
reparentActivityToken);
} else {
- resizeTaskFragmentIfRegistered(wct, container, bounds);
+ resizeTaskFragmentIfRegistered(wct, container, relBounds);
final int windowingMode = mController.getTaskContainer(taskId)
- .getWindowingModeForSplitTaskFragment(bounds);
+ .getWindowingModeForSplitTaskFragment(relBounds);
updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
}
updateAnimationParams(wct, container.getTaskFragmentToken(), splitAttributes);
@@ -300,9 +294,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@Nullable Bundle activityOptions, @NonNull SplitRule rule,
@NonNull SplitAttributes splitAttributes, boolean isPlaceholder) {
final TaskProperties taskProperties = getTaskProperties(launchingActivity);
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
splitAttributes);
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
@@ -319,11 +313,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
primaryContainer);
final TaskContainer taskContainer = mController.getTaskContainer(taskId);
final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment(
- primaryRectBounds);
+ primaryRelBounds);
mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
rule, splitAttributes);
- startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
- launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds,
+ startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRelBounds,
+ launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRelBounds,
activityIntent, activityOptions, rule, windowingMode, splitAttributes);
if (isPlaceholder) {
// When placeholder is launched in split, we should keep the focus on the primary.
@@ -334,11 +328,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
/**
* Updates the positions of containers in an existing split.
* @param splitContainer The split container to be updated.
- * @param updatedContainer The task fragment that was updated and caused this split update.
* @param wct WindowContainerTransaction that this update should be performed with.
*/
void updateSplitContainer(@NonNull SplitContainer splitContainer,
- @NonNull TaskFragmentContainer updatedContainer,
@NonNull WindowContainerTransaction wct) {
// Getting the parent configuration using the updated container - it will have the recent
// value.
@@ -348,31 +340,31 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
if (activity == null) {
return;
}
- final TaskProperties taskProperties = getTaskProperties(updatedContainer);
- final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
- final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
+ final TaskContainer taskContainer = splitContainer.getTaskContainer();
+ final TaskProperties taskProperties = taskContainer.getTaskProperties();
+ final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
+ final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
- final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
+ final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
splitAttributes);
final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
// Whether the placeholder is becoming side-by-side with the primary from fullscreen.
final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer()
&& secondaryContainer.areLastRequestedBoundsEqual(null /* bounds */)
- && !secondaryRectBounds.isEmpty();
+ && !secondaryRelBounds.isEmpty();
// If the task fragments are not registered yet, the positions will be updated after they
// are created again.
- resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
- resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
+ resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRelBounds);
+ resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRelBounds);
setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
splitAttributes);
if (isPlaceholderBecomingSplit) {
// When placeholder is shown in split, we should keep the focus on the primary.
wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
}
- final TaskContainer taskContainer = updatedContainer.getTaskContainer();
final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment(
- primaryRectBounds);
+ primaryRelBounds);
updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode);
updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
@@ -387,10 +379,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// secondaryContainer could not be finished.
boolean isStacked = !shouldShowSplit(splitAttributes);
if (isStacked) {
- setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
- null /* secondary */, null /* splitRule */);
+ clearAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken());
} else {
- setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+ setAdjacentTaskFragmentsWithRule(wct, primaryContainer.getTaskFragmentToken(),
secondaryContainer.getTaskFragmentToken(), splitRule);
}
setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(),
@@ -404,11 +395,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
private void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
- @Nullable Rect bounds) {
+ @Nullable Rect relBounds) {
if (container.getInfo() == null) {
return;
}
- resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds);
+ resizeTaskFragment(wct, container.getTaskFragmentToken(), relBounds);
}
private void updateTaskFragmentWindowingModeIfRegistered(
@@ -427,30 +418,30 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
fragmentOptions.getFragmentToken());
if (container == null) {
throw new IllegalStateException(
- "Creating a task fragment that is not registered with controller.");
+ "Creating a TaskFragment that is not registered with controller.");
}
- container.setLastRequestedBounds(fragmentOptions.getInitialBounds());
+ container.setLastRequestedBounds(fragmentOptions.getInitialRelativeBounds());
container.setLastRequestedWindowingMode(fragmentOptions.getWindowingMode());
super.createTaskFragment(wct, fragmentOptions);
}
@Override
void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
- @Nullable Rect bounds) {
+ @Nullable Rect relBounds) {
TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
throw new IllegalStateException(
- "Resizing a task fragment that is not registered with controller.");
+ "Resizing a TaskFragment that is not registered with controller.");
}
- if (container.areLastRequestedBoundsEqual(bounds)) {
+ if (container.areLastRequestedBoundsEqual(relBounds)) {
// Return early if the provided bounds were already requested
return;
}
- container.setLastRequestedBounds(bounds);
- super.resizeTaskFragment(wct, fragmentToken, bounds);
+ container.setLastRequestedBounds(relBounds);
+ super.resizeTaskFragment(wct, fragmentToken, relBounds);
}
@Override
@@ -458,7 +449,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
final TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
- throw new IllegalStateException("Setting windowing mode for a task fragment that is"
+ throw new IllegalStateException("Setting windowing mode for a TaskFragment that is"
+ " not registered with controller.");
}
@@ -476,7 +467,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) {
final TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
- throw new IllegalStateException("Setting animation params for a task fragment that is"
+ throw new IllegalStateException("Setting animation params for a TaskFragment that is"
+ " not registered with controller.");
}
@@ -489,6 +480,64 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
super.updateAnimationParams(wct, fragmentToken, animationParams);
}
+ @Override
+ void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder primary, @NonNull IBinder secondary,
+ @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
+ final TaskFragmentContainer primaryContainer = mController.getContainer(primary);
+ final TaskFragmentContainer secondaryContainer = mController.getContainer(secondary);
+ if (primaryContainer == null || secondaryContainer == null) {
+ throw new IllegalStateException("setAdjacentTaskFragments on TaskFragment that is"
+ + " not registered with controller.");
+ }
+
+ if (primaryContainer.isLastAdjacentTaskFragmentEqual(secondary, adjacentParams)
+ && secondaryContainer.isLastAdjacentTaskFragmentEqual(primary, adjacentParams)) {
+ // Return early if the same adjacent TaskFragments were already requested
+ return;
+ }
+
+ primaryContainer.setLastAdjacentTaskFragment(secondary, adjacentParams);
+ secondaryContainer.setLastAdjacentTaskFragment(primary, adjacentParams);
+ super.setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
+ }
+
+ @Override
+ void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
+ final TaskFragmentContainer container = mController.getContainer(fragmentToken);
+ if (container == null) {
+ throw new IllegalStateException("clearAdjacentTaskFragments on TaskFragment that is"
+ + " not registered with controller.");
+ }
+
+ if (container.isLastAdjacentTaskFragmentEqual(null /* fragmentToken*/, null /* params */)) {
+ // Return early if no adjacent TaskFragment was yet requested
+ return;
+ }
+
+ container.clearLastAdjacentTaskFragment();
+ super.clearAdjacentTaskFragments(wct, fragmentToken);
+ }
+
+ @Override
+ void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
+ @Nullable IBinder secondary) {
+ final TaskFragmentContainer container = mController.getContainer(primary);
+ if (container == null) {
+ throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is"
+ + " not registered with controller.");
+ }
+
+ if (container.isLastCompanionTaskFragmentEqual(secondary)) {
+ // Return early if the same companion TaskFragment was already requested
+ return;
+ }
+
+ container.setLastCompanionTaskFragment(secondary);
+ super.setCompanionTaskFragment(wct, primary, secondary);
+ }
+
/**
* Expands the split container if the current split bounds are smaller than the Activity or
* Intent that is added to the container.
@@ -515,9 +564,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// Expand the splitContainer if minimum dimensions are not satisfied.
final TaskContainer taskContainer = splitContainer.getTaskContainer();
final SplitAttributes splitAttributes = sanitizeSplitAttributes(
- taskContainer.getTaskProperties(), splitContainer.getSplitAttributes(),
+ taskContainer.getTaskProperties(), splitContainer.getCurrentSplitAttributes(),
minDimensionsPair);
- splitContainer.setSplitAttributes(splitAttributes);
+ splitContainer.updateCurrentSplitAttributes(splitAttributes);
if (!shouldShowSplit(splitAttributes)) {
// If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment
// bounds. Return failure to create a new SplitContainer which fills task bounds.
@@ -540,7 +589,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
- return shouldShowSplit(splitContainer.getSplitAttributes());
+ return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
}
static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) {
@@ -549,12 +598,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull
SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties,
- @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) {
+ @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes,
+ @Nullable Pair<Size, Size> minDimensionsPair) {
final Configuration taskConfiguration = taskProperties.getConfiguration();
final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
mController.getSplitAttributesCalculator();
- final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes();
final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
if (calculator == null) {
if (!areDefaultConstraintsSatisfied) {
@@ -599,7 +648,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
@NonNull
- private static Pair<Size, Size> getActivitiesMinDimensionsPair(
+ static Pair<Size, Size> getActivitiesMinDimensionsPair(
@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) {
return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
}
@@ -657,22 +706,29 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@VisibleForTesting
@NonNull
- Rect getBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties,
+ Rect getRelBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties,
@NonNull SplitAttributes splitAttributes) {
final Configuration taskConfiguration = taskProperties.getConfiguration();
final FoldingFeature foldingFeature = getFoldingFeature(taskProperties);
if (!shouldShowSplit(splitAttributes)) {
return new Rect();
}
+ final Rect bounds;
switch (position) {
case POSITION_START:
- return getPrimaryBounds(taskConfiguration, splitAttributes, foldingFeature);
+ bounds = getPrimaryBounds(taskConfiguration, splitAttributes, foldingFeature);
+ break;
case POSITION_END:
- return getSecondaryBounds(taskConfiguration, splitAttributes, foldingFeature);
+ bounds = getSecondaryBounds(taskConfiguration, splitAttributes, foldingFeature);
+ break;
case POSITION_FILL:
default:
- return new Rect();
+ bounds = new Rect();
}
+ // Convert to relative bounds in parent coordinate. This is to avoid flicker when the Task
+ // resized before organizer requests have been applied.
+ taskProperties.translateAbsoluteBoundsToRelativeBounds(bounds);
+ return bounds;
}
@NonNull
@@ -969,11 +1025,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
@NonNull
- static TaskProperties getTaskProperties(@NonNull TaskFragmentContainer container) {
- return container.getTaskContainer().getTaskProperties();
- }
-
- @NonNull
TaskProperties getTaskProperties(@NonNull Activity activity) {
final TaskContainer taskContainer = mController.getTaskContainer(
mController.getTaskId(activity));
@@ -989,9 +1040,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
@NonNull
- private static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) {
+ static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) {
final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
// TODO(b/190433398): Supply correct insets.
- return new WindowMetrics(taskBounds, WindowInsets.CONSUMED);
+ final float density = taskConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density);
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 03f4dc9c1167..4b15bb187035 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -108,12 +108,6 @@ class TaskContainer {
}
@NonNull
- Configuration getConfiguration() {
- // Make a copy in case the config is updated unexpectedly.
- return new Configuration(mConfiguration);
- }
-
- @NonNull
TaskProperties getTaskProperties() {
return new TaskProperties(mDisplayId, mConfiguration);
}
@@ -157,7 +151,7 @@ class TaskContainer {
@WindowingMode
private int getWindowingMode() {
- return getConfiguration().windowConfiguration.getWindowingMode();
+ return mConfiguration.windowConfiguration.getWindowingMode();
}
/** Whether there is any {@link TaskFragmentContainer} below this Task. */
@@ -220,10 +214,7 @@ class TaskContainer {
}
}
- /**
- * A wrapper class which contains the display ID and {@link Configuration} of a
- * {@link TaskContainer}
- */
+ /** A wrapper class which contains the information of {@link TaskContainer} */
static final class TaskProperties {
private final int mDisplayId;
@NonNull
@@ -243,6 +234,15 @@ class TaskContainer {
return mConfiguration;
}
+ /** Translates the given absolute bounds to relative bounds in this Task coordinate. */
+ void translateAbsoluteBoundsToRelativeBounds(@NonNull Rect inOutBounds) {
+ if (inOutBounds.isEmpty()) {
+ return;
+ }
+ final Rect taskBounds = mConfiguration.windowConfiguration.getBounds();
+ inOutBounds.offset(-taskBounds.left, -taskBounds.top);
+ }
+
/**
* Obtains the {@link TaskProperties} for the task that the provided {@link Activity} is
* associated with.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index b917ac80256c..d9b73a8290f5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -83,10 +83,7 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
}
@Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {
- if (TaskFragmentAnimationController.DEBUG) {
- Log.v(TAG, "onAnimationCancelled: isKeyguardOccluded=" + isKeyguardOccluded);
- }
+ public void onAnimationCancelled() {
mHandler.post(this::cancelAnimation);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 17814c65e791..60be9d16d749 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -37,8 +37,10 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
/**
* Client-side container for a stack of activities. Corresponds to an instance of TaskFragment
@@ -84,6 +86,12 @@ class TaskFragmentContainer {
@Nullable
private Intent mPendingAppearedIntent;
+ /**
+ * The activities that were explicitly requested to be launched in its current TaskFragment,
+ * but haven't been added to {@link #mInfo} yet.
+ */
+ final ArrayList<IBinder> mPendingAppearedInRequestedTaskFragmentActivities = new ArrayList<>();
+
/** Containers that are dependent on this one and should be completely destroyed on exit. */
private final List<TaskFragmentContainer> mContainersToFinishOnExit =
new ArrayList<>();
@@ -116,6 +124,27 @@ class TaskFragmentContainer {
private TaskFragmentAnimationParams mLastAnimationParams = TaskFragmentAnimationParams.DEFAULT;
/**
+ * TaskFragment token that was requested last via
+ * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}.
+ */
+ @Nullable
+ private IBinder mLastAdjacentTaskFragment;
+
+ /**
+ * {@link WindowContainerTransaction.TaskFragmentAdjacentParams} token that was requested last
+ * via {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}.
+ */
+ @Nullable
+ private WindowContainerTransaction.TaskFragmentAdjacentParams mLastAdjacentParams;
+
+ /**
+ * TaskFragment token that was requested last via
+ * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT}.
+ */
+ @Nullable
+ private IBinder mLastCompanionTaskFragment;
+
+ /**
* When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment
* if it is still empty after the timeout.
*/
@@ -124,6 +153,11 @@ class TaskFragmentContainer {
Runnable mAppearEmptyTimeout;
/**
+ * Whether this TaskFragment contains activities of another process/package.
+ */
+ private boolean mHasCrossProcessActivities;
+
+ /**
* Creates a container with an existing activity that will be re-parented to it in a window
* container transaction.
* @param pairedPrimaryContainer when it is set, the new container will be add right above it
@@ -243,7 +277,7 @@ class TaskFragmentContainer {
@NonNull
ActivityStack toActivityStack() {
- return new ActivityStack(collectNonFinishingActivities(), isEmpty());
+ return new ActivityStack(collectNonFinishingActivities(), isEmpty(), mToken);
}
/** Adds the activity that will be reparented to this container. */
@@ -273,6 +307,8 @@ class TaskFragmentContainer {
void removePendingAppearedActivity(@NonNull IBinder activityToken) {
mPendingAppearedActivities.remove(activityToken);
+ // Also remove the activity from the mPendingInRequestedTaskFragmentActivities.
+ mPendingAppearedInRequestedTaskFragmentActivities.remove(activityToken);
}
@GuardedBy("mController.mLock")
@@ -387,10 +423,18 @@ class TaskFragmentContainer {
mAppearEmptyTimeout = null;
}
+ mHasCrossProcessActivities = false;
mInfo = info;
if (mInfo == null || mInfo.isEmpty()) {
return;
}
+
+ // Contains activities of another process if the activities size is not matched to the
+ // running activity count
+ if (mInfo.getRunningActivityCount() != mInfo.getActivities().size()) {
+ mHasCrossProcessActivities = true;
+ }
+
// Only track the pending Intent when the container is empty.
mPendingAppearedIntent = null;
if (mPendingAppearedActivities.isEmpty()) {
@@ -401,7 +445,7 @@ class TaskFragmentContainer {
for (int i = mPendingAppearedActivities.size() - 1; i >= 0; --i) {
final IBinder activityToken = mPendingAppearedActivities.get(i);
if (infoActivities.contains(activityToken)) {
- mPendingAppearedActivities.remove(i);
+ removePendingAppearedActivity(activityToken);
}
}
}
@@ -436,10 +480,17 @@ class TaskFragmentContainer {
* Removes a container that should be finished when this container is finished.
*/
void removeContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToRemove) {
+ removeContainersToFinishOnExit(Collections.singletonList(containerToRemove));
+ }
+
+ /**
+ * Removes container list that should be finished when this container is finished.
+ */
+ void removeContainersToFinishOnExit(@NonNull List<TaskFragmentContainer> containersToRemove) {
if (mIsFinished) {
return;
}
- mContainersToFinishOnExit.remove(containerToRemove);
+ mContainersToFinishOnExit.removeAll(containersToRemove);
}
/**
@@ -478,6 +529,16 @@ class TaskFragmentContainer {
@GuardedBy("mController.mLock")
void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
@NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
+ finish(shouldFinishDependent, presenter, wct, controller, true /* shouldRemoveRecord */);
+ }
+
+ /**
+ * Removes all activities that belong to this process and finishes other containers/activities
+ * configured to finish together.
+ */
+ void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
+ @NonNull WindowContainerTransaction wct, @NonNull SplitController controller,
+ boolean shouldRemoveRecord) {
if (!mIsFinished) {
mIsFinished = true;
if (mAppearEmptyTimeout != null) {
@@ -494,8 +555,10 @@ class TaskFragmentContainer {
// Cleanup the visuals
presenter.deleteTaskFragment(wct, getTaskFragmentToken());
- // Cleanup the records
- controller.removeContainer(this);
+ if (shouldRemoveRecord) {
+ // Cleanup the records
+ controller.removeContainer(this);
+ }
// Clean up task fragment information
mInfo = null;
}
@@ -570,30 +633,30 @@ class TaskFragmentContainer {
/**
* Checks if last requested bounds are equal to the provided value.
+ * The requested bounds are relative bounds in parent coordinate.
+ * @see WindowContainerTransaction#setRelativeBounds
*/
- boolean areLastRequestedBoundsEqual(@Nullable Rect bounds) {
- return (bounds == null && mLastRequestedBounds.isEmpty())
- || mLastRequestedBounds.equals(bounds);
+ boolean areLastRequestedBoundsEqual(@Nullable Rect relBounds) {
+ return (relBounds == null && mLastRequestedBounds.isEmpty())
+ || mLastRequestedBounds.equals(relBounds);
}
/**
* Updates the last requested bounds.
+ * The requested bounds are relative bounds in parent coordinate.
+ * @see WindowContainerTransaction#setRelativeBounds
*/
- void setLastRequestedBounds(@Nullable Rect bounds) {
- if (bounds == null) {
+ void setLastRequestedBounds(@Nullable Rect relBounds) {
+ if (relBounds == null) {
mLastRequestedBounds.setEmpty();
} else {
- mLastRequestedBounds.set(bounds);
+ mLastRequestedBounds.set(relBounds);
}
}
- @NonNull
- Rect getLastRequestedBounds() {
- return mLastRequestedBounds;
- }
-
/**
* Checks if last requested windowing mode is equal to the provided value.
+ * @see WindowContainerTransaction#setWindowingMode
*/
boolean isLastRequestedWindowingModeEqual(@WindowingMode int windowingMode) {
return mLastRequestedWindowingMode == windowingMode;
@@ -601,6 +664,7 @@ class TaskFragmentContainer {
/**
* Updates the last requested windowing mode.
+ * @see WindowContainerTransaction#setWindowingMode
*/
void setLastRequestedWindowingMode(@WindowingMode int windowingModes) {
mLastRequestedWindowingMode = windowingModes;
@@ -608,6 +672,7 @@ class TaskFragmentContainer {
/**
* Checks if last requested {@link TaskFragmentAnimationParams} are equal to the provided value.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS
*/
boolean areLastRequestedAnimationParamsEqual(
@NonNull TaskFragmentAnimationParams animationParams) {
@@ -616,11 +681,94 @@ class TaskFragmentContainer {
/**
* Updates the last requested {@link TaskFragmentAnimationParams}.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS
*/
void setLastRequestAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) {
mLastAnimationParams = animationParams;
}
+ /**
+ * Checks if last requested adjacent TaskFragment token and params are equal to the provided
+ * values.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS
+ * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS
+ */
+ boolean isLastAdjacentTaskFragmentEqual(@Nullable IBinder fragmentToken,
+ @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams params) {
+ return Objects.equals(mLastAdjacentTaskFragment, fragmentToken)
+ && Objects.equals(mLastAdjacentParams, params);
+ }
+
+ /**
+ * Updates the last requested adjacent TaskFragment token and params.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS
+ */
+ void setLastAdjacentTaskFragment(@NonNull IBinder fragmentToken,
+ @NonNull WindowContainerTransaction.TaskFragmentAdjacentParams params) {
+ mLastAdjacentTaskFragment = fragmentToken;
+ mLastAdjacentParams = params;
+ }
+
+ /**
+ * Clears the last requested adjacent TaskFragment token and params.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS
+ */
+ void clearLastAdjacentTaskFragment() {
+ final TaskFragmentContainer lastAdjacentTaskFragment = mLastAdjacentTaskFragment != null
+ ? mController.getContainer(mLastAdjacentTaskFragment)
+ : null;
+ mLastAdjacentTaskFragment = null;
+ mLastAdjacentParams = null;
+ if (lastAdjacentTaskFragment != null) {
+ // Clear the previous adjacent TaskFragment as well.
+ lastAdjacentTaskFragment.clearLastAdjacentTaskFragment();
+ }
+ }
+
+ /**
+ * Checks if last requested companion TaskFragment token is equal to the provided value.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT
+ */
+ boolean isLastCompanionTaskFragmentEqual(@Nullable IBinder fragmentToken) {
+ return Objects.equals(mLastCompanionTaskFragment, fragmentToken);
+ }
+
+ /**
+ * Updates the last requested companion TaskFragment token.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT
+ */
+ void setLastCompanionTaskFragment(@Nullable IBinder fragmentToken) {
+ mLastCompanionTaskFragment = fragmentToken;
+ }
+
+ /**
+ * Adds the pending appeared activity that has requested to be launched in this task fragment.
+ * @see android.app.ActivityClient#isRequestedToLaunchInTaskFragment
+ */
+ void addPendingAppearedInRequestedTaskFragmentActivity(Activity activity) {
+ final IBinder activityToken = activity.getActivityToken();
+ if (hasActivity(activityToken)) {
+ return;
+ }
+ mPendingAppearedInRequestedTaskFragmentActivities.add(activity.getActivityToken());
+ }
+
+ /**
+ * Checks if the given activity has requested to be launched in this task fragment.
+ * @see #addPendingAppearedInRequestedTaskFragmentActivity
+ */
+ boolean isActivityInRequestedTaskFragment(IBinder activityToken) {
+ if (mInfo != null && mInfo.getActivitiesRequestedInTaskFragment().contains(activityToken)) {
+ return true;
+ }
+ return mPendingAppearedInRequestedTaskFragmentActivities.contains(activityToken);
+ }
+
+ /** Whether contains activities of another process */
+ boolean hasCrossProcessActivities() {
+ return mHasCrossProcessActivities;
+ }
+
/** Gets the parent leaf Task id. */
int getTaskId() {
return mTaskContainer.getTaskId();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java
index 0071fea41aa8..396956e56bb5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java
@@ -16,12 +16,12 @@
package androidx.window.extensions.embedding;
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_NONE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_NONE;
import android.os.IBinder;
-import android.view.WindowManager.TransitionType;
import android.window.TaskFragmentOrganizer;
+import android.window.TaskFragmentOrganizer.TaskFragmentTransitionType;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
@@ -122,8 +122,8 @@ class TransactionManager {
* @see #setOriginType(int)
* @see #getTransactionTransitionType()
*/
- @TransitionType
- private int mOriginType = TRANSIT_NONE;
+ @TaskFragmentTransitionType
+ private int mOriginType = TASK_FRAGMENT_TRANSIT_NONE;
TransactionRecord(@Nullable IBinder taskFragmentTransactionToken) {
mTaskFragmentTransactionToken = taskFragmentTransactionToken;
@@ -136,12 +136,12 @@ class TransactionManager {
}
/**
- * Sets the {@link TransitionType} that triggers this transaction. If there are multiple
- * calls, only the first call will be respected as the "origin" type.
+ * Sets the {@link TaskFragmentTransitionType} that triggers this transaction. If there are
+ * multiple calls, only the first call will be respected as the "origin" type.
*/
- void setOriginType(@TransitionType int type) {
+ void setOriginType(@TaskFragmentTransitionType int type) {
ensureCurrentTransaction();
- if (mOriginType != TRANSIT_NONE) {
+ if (mOriginType != TASK_FRAGMENT_TRANSIT_NONE) {
// Skip if the origin type has already been set.
return;
}
@@ -188,14 +188,16 @@ class TransactionManager {
}
/**
- * Gets the {@link TransitionType} that we will request transition with for the
+ * Gets the {@link TaskFragmentTransitionType} that we will request transition with for the
* current {@link WindowContainerTransaction}.
*/
@VisibleForTesting
- @TransitionType
+ @TaskFragmentTransitionType
int getTransactionTransitionType() {
- // Use TRANSIT_CHANGE as default if there is not opening/closing window.
- return mOriginType != TRANSIT_NONE ? mOriginType : TRANSIT_CHANGE;
+ // Use TASK_FRAGMENT_TRANSIT_CHANGE as default if there is not opening/closing window.
+ return mOriginType != TASK_FRAGMENT_TRANSIT_NONE
+ ? mOriginType
+ : TASK_FRAGMENT_TRANSIT_CHANGE;
}
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 8386131b177d..a45a8a183ac8 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -122,16 +122,6 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
addWindowLayoutInfoListener(activity, extConsumer);
}
- @Override
- public void addWindowLayoutInfoListener(@NonNull @UiContext Context context,
- @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
- final Consumer<WindowLayoutInfo> extConsumer = consumer::accept;
- synchronized (mLock) {
- mJavaToExtConsumers.put(consumer, extConsumer);
- }
- addWindowLayoutInfoListener(context, extConsumer);
- }
-
/**
* Similar to {@link #addWindowLayoutInfoListener(Activity, java.util.function.Consumer)}, but
* takes a UI Context as a parameter.
@@ -381,24 +371,15 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
final WindowManager windowManager = Objects.requireNonNull(
context.getSystemService(WindowManager.class));
- final Rect currentBounds = windowManager.getCurrentWindowMetrics().getBounds();
final Rect maxBounds = windowManager.getMaximumWindowMetrics().getBounds();
boolean isTaskExpanded = maxBounds.equals(taskBounds);
- boolean isActivityExpanded = maxBounds.equals(currentBounds);
/*
* We need to proxy being in full screen because when a user enters PiP and exits PiP
* the task windowingMode will report multi-window/pinned until the transition is
* finished in WM Shell.
* maxBounds == taskWindowBounds is a proxy check to verify the window is full screen
- * For tasks that are letterboxed, we use currentBounds == maxBounds to filter these
- * out.
*/
- // TODO(b/262900133) remove currentBounds check when letterboxed apps report bounds.
- // currently we don't want to report to letterboxed apps since they do not update the
- // window bounds when the Activity is moved. An inaccurate fold will be reported so
- // we skip.
- return isTaskExpanded && (isActivityExpanded
- || mTaskFragmentOrganizer.isActivityEmbedded(activityToken));
+ return isTaskExpanded;
} else {
// TODO(b/242674941): use task windowing mode for window context that associates with
// activity.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
index 46c925aaf8a2..de52f0969fa8 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
@@ -51,10 +51,10 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>,
public final void addDataChangedCallback(@NonNull Consumer<T> callback) {
synchronized (mLock) {
mCallbacks.add(callback);
- Optional<T> currentData = getCurrentData();
- currentData.ifPresent(callback);
- onListenersChanged(mCallbacks);
}
+ Optional<T> currentData = getCurrentData();
+ currentData.ifPresent(callback);
+ onListenersChanged();
}
/**
@@ -67,11 +67,22 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>,
public final void removeDataChangedCallback(@NonNull Consumer<T> callback) {
synchronized (mLock) {
mCallbacks.remove(callback);
- onListenersChanged(mCallbacks);
}
+ onListenersChanged();
}
- protected void onListenersChanged(Set<Consumer<T>> callbacks) {}
+ /**
+ * Returns {@code true} if there are any registered callbacks {@code false} if there are no
+ * registered callbacks.
+ */
+ // TODO(b/278132889) Improve the structure of BaseDataProdcuer while avoiding known issues.
+ public final boolean hasListeners() {
+ synchronized (mLock) {
+ return !mCallbacks.isEmpty();
+ }
+ }
+
+ protected void onListenersChanged() {}
/**
* @return the current data if available and {@code Optional.empty()} otherwise.
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java
new file mode 100644
index 000000000000..ccb4ebe9199e
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java
@@ -0,0 +1,96 @@
+/*
+ * 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 androidx.window.extensions.area;
+
+import static org.junit.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+import android.util.DisplayMetrics;
+import android.view.Surface;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WindowAreaComponentImplTests {
+
+ private final DisplayMetrics mTestDisplayMetrics = new DisplayMetrics();
+
+ @Before
+ public void setup() {
+ mTestDisplayMetrics.widthPixels = 1;
+ mTestDisplayMetrics.heightPixels = 2;
+ mTestDisplayMetrics.noncompatWidthPixels = 3;
+ mTestDisplayMetrics.noncompatHeightPixels = 4;
+ }
+
+ /**
+ * Cases where the rear display metrics does not need to be transformed.
+ */
+ @Test
+ public void testRotateRearDisplayMetrics_noTransformNeeded() {
+ final DisplayMetrics originalMetrics = new DisplayMetrics();
+ originalMetrics.setTo(mTestDisplayMetrics);
+
+ WindowAreaComponentImpl.rotateRearDisplayMetricsIfNeeded(
+ Surface.ROTATION_0, Surface.ROTATION_0, mTestDisplayMetrics);
+ assertEquals(originalMetrics, mTestDisplayMetrics);
+
+ WindowAreaComponentImpl.rotateRearDisplayMetricsIfNeeded(
+ Surface.ROTATION_180, Surface.ROTATION_180, mTestDisplayMetrics);
+ assertEquals(originalMetrics, mTestDisplayMetrics);
+
+ WindowAreaComponentImpl.rotateRearDisplayMetricsIfNeeded(
+ Surface.ROTATION_0, Surface.ROTATION_180, mTestDisplayMetrics);
+ assertEquals(originalMetrics, mTestDisplayMetrics);
+
+ WindowAreaComponentImpl.rotateRearDisplayMetricsIfNeeded(
+ Surface.ROTATION_180, Surface.ROTATION_0, mTestDisplayMetrics);
+ assertEquals(originalMetrics, mTestDisplayMetrics);
+ }
+
+ /**
+ * Cases where the rear display metrics need to be transformed.
+ */
+ @Test
+ public void testRotateRearDisplayMetrics_transformNeeded() {
+ DisplayMetrics originalMetrics = new DisplayMetrics();
+ originalMetrics.setTo(mTestDisplayMetrics);
+
+ DisplayMetrics expectedMetrics = new DisplayMetrics();
+ expectedMetrics.setTo(mTestDisplayMetrics);
+ expectedMetrics.widthPixels = mTestDisplayMetrics.heightPixels;
+ expectedMetrics.heightPixels = mTestDisplayMetrics.widthPixels;
+ expectedMetrics.noncompatWidthPixels = mTestDisplayMetrics.noncompatHeightPixels;
+ expectedMetrics.noncompatHeightPixels = mTestDisplayMetrics.noncompatWidthPixels;
+
+ WindowAreaComponentImpl.rotateRearDisplayMetricsIfNeeded(
+ Surface.ROTATION_90, Surface.ROTATION_0, mTestDisplayMetrics);
+ assertEquals(expectedMetrics, mTestDisplayMetrics);
+
+ mTestDisplayMetrics.setTo(originalMetrics);
+ WindowAreaComponentImpl.rotateRearDisplayMetricsIfNeeded(
+ Surface.ROTATION_270, Surface.ROTATION_0, mTestDisplayMetrics);
+ assertEquals(expectedMetrics, mTestDisplayMetrics);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index 459ec9f89c4a..a069ac7256d6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -177,6 +177,7 @@ public class EmbeddingTestUtils {
1,
isVisible,
Collections.singletonList(activity.getActivityToken()),
+ new ArrayList<>(),
new Point(),
false /* isTaskClearedForReuse */,
false /* isTaskFragmentClearedForPip */,
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index bbb454d31c38..dd087e8eb7c9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -124,7 +124,7 @@ public class JetpackTaskFragmentOrganizerTest {
private TaskFragmentInfo createMockInfo(TaskFragmentContainer container) {
return new TaskFragmentInfo(container.getTaskFragmentToken(),
mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */,
- false /* isVisible */, new ArrayList<>(), new Point(),
+ false /* isVisible */, new ArrayList<>(), new ArrayList<>(), new Point(),
false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */,
false /* isClearedForReorderActivityToFront */, new Point());
}
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 0bf0bc85b511..ff08782e8cd8 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
@@ -20,13 +20,13 @@ import static android.app.ActivityManager.START_CANCELED;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
-import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY;
@@ -67,6 +67,7 @@ import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import android.annotation.NonNull;
import android.app.Activity;
@@ -82,6 +83,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.TaskFragmentInfo;
@@ -100,12 +102,14 @@ import androidx.window.extensions.layout.WindowLayoutInfo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import java.util.function.Consumer;
/**
@@ -275,7 +279,8 @@ public class SplitControllerTest {
assertNotNull(tf);
assertNotNull(taskContainer);
- assertEquals(TASK_BOUNDS, taskContainer.getConfiguration().windowConfiguration.getBounds());
+ assertEquals(TASK_BOUNDS, taskContainer.getTaskProperties().getConfiguration()
+ .windowConfiguration.getBounds());
}
@Test
@@ -288,7 +293,7 @@ public class SplitControllerTest {
doReturn(true).when(tf).isEmpty();
doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction,
mActivity, false /* isOnCreated */);
- doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any());
+ doNothing().when(mSplitPresenter).updateSplitContainer(any(), any());
mSplitController.updateContainer(mTransaction, tf);
@@ -341,7 +346,7 @@ public class SplitControllerTest {
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+ verify(mSplitPresenter, never()).updateSplitContainer(any(), any());
// Verify if the top active split is updated if both of its containers are not finished.
doReturn(false).when(mSplitController)
@@ -349,7 +354,7 @@ public class SplitControllerTest {
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction);
+ verify(mSplitPresenter).updateSplitContainer(splitContainer, mTransaction);
}
@Test
@@ -366,14 +371,14 @@ public class SplitControllerTest {
doReturn(false).when(taskContainer).isVisible();
mSplitController.updateContainer(mTransaction, taskFragmentContainer);
- verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+ verify(mSplitPresenter, never()).updateSplitContainer(any(), any());
// Update the split when the Task is visible.
doReturn(true).when(taskContainer).isVisible();
mSplitController.updateContainer(mTransaction, taskFragmentContainer);
verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
- taskFragmentContainer, mTransaction);
+ mTransaction);
}
@Test
@@ -560,7 +565,6 @@ public class SplitControllerTest {
assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container));
assertTrue(primaryContainer.areLastRequestedBoundsEqual(null));
assertTrue(container.areLastRequestedBoundsEqual(null));
- assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity));
}
@Test
@@ -695,7 +699,7 @@ public class SplitControllerTest {
final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
- assertFalse(result);
+ assertTrue(result);
verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
any(), anyBoolean());
}
@@ -729,7 +733,7 @@ public class SplitControllerTest {
final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
- assertFalse(result);
+ assertTrue(result);
verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
any(), anyBoolean());
}
@@ -803,7 +807,7 @@ public class SplitControllerTest {
final Activity launchedActivity = createMockActivity();
primaryContainer.addPendingAppearedActivity(launchedActivity);
- assertFalse(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity,
+ assertTrue(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity,
false /* isOnReparent */));
}
@@ -939,7 +943,7 @@ public class SplitControllerTest {
boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
- assertFalse(result);
+ assertTrue(result);
assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity));
@@ -1003,9 +1007,27 @@ public class SplitControllerTest {
assertTrue(result);
assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */);
- assertEquals(mSplitController.getContainerWithActivity(secondaryActivity),
- mSplitController.getContainerWithActivity(mActivity));
- verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any(), any());
+ assertTrue(mSplitController.getContainerWithActivity(mActivity)
+ .areLastRequestedBoundsEqual(new Rect()));
+ }
+
+ @Test
+ public void testFindActivityBelow() {
+ // Create a container with two activities
+ final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity);
+ final Activity pendingAppearedActivity = createMockActivity();
+ container.addPendingAppearedActivity(pendingAppearedActivity);
+
+ // Ensure the activity below matches
+ assertEquals(mActivity,
+ mSplitController.findActivityBelow(pendingAppearedActivity));
+
+ // Ensure that the activity look up won't search for the in-process activities and should
+ // IPC to WM core to get the activity below. It should be `null` for this mock test.
+ spyOn(container);
+ doReturn(true).when(container).hasCrossProcessActivities();
+ assertNotEquals(mActivity,
+ mSplitController.findActivityBelow(pendingAppearedActivity));
}
@Test
@@ -1139,7 +1161,7 @@ public class SplitControllerTest {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
final IBinder errorToken = new Binder();
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
- final int opType = HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
+ final int opType = OP_TYPE_CREATE_TASK_FRAGMENT;
final Exception exception = new SecurityException("test");
final Bundle errorBundle = TaskFragmentOrganizer.putErrorInfoInBundle(exception, info,
opType);
@@ -1191,7 +1213,7 @@ public class SplitControllerTest {
.build();
assertTrue("Rules must have same presentation if tags are null and has same properties.",
- SplitController.haveSamePresentation(splitRule1, splitRule2,
+ SplitController.areRulesSamePresentation(splitRule1, splitRule2,
new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
splitRule2 = createSplitPairRuleBuilder(
@@ -1206,7 +1228,7 @@ public class SplitControllerTest {
assertFalse("Rules must have different presentations if tags are not equal regardless"
+ "of other properties",
- SplitController.haveSamePresentation(splitRule1, splitRule2,
+ SplitController.areRulesSamePresentation(splitRule1, splitRule2,
new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
}
@@ -1322,6 +1344,125 @@ public class SplitControllerTest {
verify(mTransaction).startActivityInTaskFragment(any(), any(), any(), any());
}
+ @Test
+ public void testFinishActivityStacks_emptySet_earlyReturn() {
+ mSplitController.finishActivityStacks(Collections.emptySet());
+
+ verify(mSplitController, never()).updateContainersInTaskIfVisible(any(), anyInt());
+ }
+
+ @Test
+ public void testFinishActivityStacks_invalidStacks_earlyReturn() {
+ mSplitController.finishActivityStacks(Collections.singleton(new Binder()));
+
+ verify(mSplitController, never()).updateContainersInTaskIfVisible(any(), anyInt());
+ }
+
+ @Test
+ public void testFinishActivityStacks_finishSingleActivityStack() {
+ TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+ tf.setInfo(mTransaction, createMockTaskFragmentInfo(tf, mActivity));
+
+ List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID)
+ .mContainers;
+
+ assertEquals(containers.get(0), tf);
+
+ mSplitController.finishActivityStacks(Collections.singleton(tf.getTaskFragmentToken()));
+
+ verify(mSplitPresenter).deleteTaskFragment(any(), eq(tf.getTaskFragmentToken()));
+ assertTrue(containers.isEmpty());
+ }
+
+ @Test
+ public void testFinishActivityStacks_finishActivityStacksInOrder() {
+ TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
+ TaskFragmentContainer topTf = mSplitController.newContainer(mActivity, TASK_ID);
+ bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity));
+ topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
+
+ List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID)
+ .mContainers;
+
+ assertEquals(containers.size(), 2);
+
+ Set<IBinder> activityStackTokens = new ArraySet<>(new IBinder[]{
+ topTf.getTaskFragmentToken(), bottomTf.getTaskFragmentToken()});
+
+ mSplitController.finishActivityStacks(activityStackTokens);
+
+ ArgumentCaptor<IBinder> argumentCaptor = ArgumentCaptor.forClass(IBinder.class);
+
+ verify(mSplitPresenter, times(2)).deleteTaskFragment(any(), argumentCaptor.capture());
+
+ List<IBinder> fragmentTokens = argumentCaptor.getAllValues();
+ assertEquals("The ActivityStack must be deleted from the lowest z-order "
+ + "regardless of the order in ActivityStack set",
+ bottomTf.getTaskFragmentToken(), fragmentTokens.get(0));
+ assertEquals("The ActivityStack must be deleted from the lowest z-order "
+ + "regardless of the order in ActivityStack set",
+ topTf.getTaskFragmentToken(), fragmentTokens.get(1));
+
+ assertTrue(containers.isEmpty());
+ }
+
+ @Test
+ public void testUpdateSplitAttributes_invalidSplitContainerToken_earlyReturn() {
+ mSplitController.updateSplitAttributes(new Binder(), SPLIT_ATTRIBUTES);
+
+ verify(mTransactionManager, never()).startNewTransaction();
+ }
+
+ @Test
+ public void testUpdateSplitAttributes_nullParams_throwException() {
+ assertThrows(NullPointerException.class,
+ () -> mSplitController.updateSplitAttributes(null, SPLIT_ATTRIBUTES));
+
+ final SplitContainer splitContainer = mock(SplitContainer.class);
+ final IBinder token = new Binder();
+ doReturn(token).when(splitContainer).getToken();
+ doReturn(splitContainer).when(mSplitController).getSplitContainer(eq(token));
+
+ assertThrows(NullPointerException.class,
+ () -> mSplitController.updateSplitAttributes(token, null));
+ }
+
+ @Test
+ public void testUpdateSplitAttributes_doNotNeedToUpdateSplitContainer_doNotApplyTransaction() {
+ final SplitContainer splitContainer = mock(SplitContainer.class);
+ final IBinder token = new Binder();
+ doReturn(token).when(splitContainer).getToken();
+ doReturn(splitContainer).when(mSplitController).getSplitContainer(eq(token));
+ doReturn(false).when(mSplitController).updateSplitContainerIfNeeded(
+ eq(splitContainer), any(), eq(SPLIT_ATTRIBUTES));
+ TransactionManager.TransactionRecord testRecord =
+ mock(TransactionManager.TransactionRecord.class);
+ doReturn(testRecord).when(mTransactionManager).startNewTransaction();
+
+ mSplitController.updateSplitAttributes(token, SPLIT_ATTRIBUTES);
+
+ verify(splitContainer).updateDefaultSplitAttributes(eq(SPLIT_ATTRIBUTES));
+ verify(testRecord).abort();
+ }
+
+ @Test
+ public void testUpdateSplitAttributes_splitContainerUpdated_updateAttrs() {
+ final SplitContainer splitContainer = mock(SplitContainer.class);
+ final IBinder token = new Binder();
+ doReturn(token).when(splitContainer).getToken();
+ doReturn(splitContainer).when(mSplitController).getSplitContainer(eq(token));
+ doReturn(true).when(mSplitController).updateSplitContainerIfNeeded(
+ eq(splitContainer), any(), eq(SPLIT_ATTRIBUTES));
+ TransactionManager.TransactionRecord testRecord =
+ mock(TransactionManager.TransactionRecord.class);
+ doReturn(testRecord).when(mTransactionManager).startNewTransaction();
+
+ mSplitController.updateSplitAttributes(token, SPLIT_ATTRIBUTES);
+
+ verify(splitContainer).updateDefaultSplitAttributes(eq(SPLIT_ATTRIBUTES));
+ verify(testRecord).apply(eq(false));
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
return createMockActivity(TASK_ID);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index d286d23da750..6981d9d7ebb8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -66,13 +66,16 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
+import android.util.DisplayMetrics;
import android.util.Pair;
import android.util.Size;
+import android.view.WindowMetrics;
import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOperation;
import android.window.WindowContainerTransaction;
+import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -102,7 +105,6 @@ import java.util.ArrayList;
@RunWith(AndroidJUnit4.class)
public class SplitPresenterTest {
- @Mock
private Activity mActivity;
@Mock
private Resources mActivityResources;
@@ -147,13 +149,13 @@ public class SplitPresenterTest {
mPresenter.resizeTaskFragment(mTransaction, container.getTaskFragmentToken(), TASK_BOUNDS);
assertTrue(container.areLastRequestedBoundsEqual(TASK_BOUNDS));
- verify(mTransaction).setBounds(any(), eq(TASK_BOUNDS));
+ verify(mTransaction).setRelativeBounds(any(), eq(TASK_BOUNDS));
// No request to set the same bounds.
clearInvocations(mTransaction);
mPresenter.resizeTaskFragment(mTransaction, container.getTaskFragmentToken(), TASK_BOUNDS);
- verify(mTransaction, never()).setBounds(any(), any());
+ verify(mTransaction, never()).setRelativeBounds(any(), any());
}
@Test
@@ -175,6 +177,64 @@ public class SplitPresenterTest {
}
@Test
+ public void testSetAdjacentTaskFragments() {
+ final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+ mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+ verify(mTransaction).setAdjacentTaskFragments(container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+
+ // No request to set the same adjacent TaskFragments.
+ clearInvocations(mTransaction);
+ mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+
+ verify(mTransaction, never()).setAdjacentTaskFragments(any(), any(), any());
+ }
+
+ @Test
+ public void testClearAdjacentTaskFragments() {
+ final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+ // No request to clear as it is not set by default.
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+ verify(mTransaction, never()).clearAdjacentTaskFragments(any());
+
+ mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+ verify(mTransaction).clearAdjacentTaskFragments(container0.getTaskFragmentToken());
+
+ // No request to clear on either of the previous cleared TasKFragments.
+ clearInvocations(mTransaction);
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container1.getTaskFragmentToken());
+
+ verify(mTransaction, never()).clearAdjacentTaskFragments(any());
+ }
+
+ @Test
+ public void testSetCompanionTaskFragment() {
+ final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+ mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken());
+ verify(mTransaction).setCompanionTaskFragment(container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken());
+
+ // No request to set the same adjacent TaskFragments.
+ clearInvocations(mTransaction);
+ mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken());
+
+ verify(mTransaction, never()).setCompanionTaskFragment(any(), any());
+ }
+
+ @Test
public void testUpdateAnimationParams() {
final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
@@ -194,7 +254,7 @@ public class SplitPresenterTest {
OP_TYPE_SET_ANIMATION_PARAMS)
.setAnimationParams(animationParams)
.build();
- verify(mTransaction).setTaskFragmentOperation(container.getTaskFragmentToken(),
+ verify(mTransaction).addTaskFragmentOperation(container.getTaskFragmentToken(),
expectedOperation);
assertTrue(container.areLastRequestedAnimationParamsEqual(animationParams));
@@ -203,7 +263,7 @@ public class SplitPresenterTest {
mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(),
animationParams);
- verify(mTransaction, never()).setTaskFragmentOperation(any(), any());
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
}
@Test
@@ -225,31 +285,53 @@ public class SplitPresenterTest {
}
@Test
- public void testGetBoundsForPosition_expandContainers() {
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ public void testGetRelBoundsForPosition_expandContainers() {
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
.build();
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
- public void testGetBoundsForPosition_splitVertically() {
+ public void testGetRelBoundsForPosition_expandContainers_isRelativeToParent() {
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties(
+ new Rect(100, 100, 500, 1000));
+ final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
+ .build();
+
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
+
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ }
+
+ @Test
+ public void testGetRelBoundsForPosition_splitVertically() {
final Rect primaryBounds = getSplitBounds(true /* isPrimary */,
false /* splitHorizontally */);
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
false /* splitHorizontally */);
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
@@ -257,14 +339,15 @@ public class SplitPresenterTest {
assertEquals("Primary bounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("Secondary bounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
@@ -273,14 +356,15 @@ public class SplitPresenterTest {
assertEquals("Secondary bounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("Primary bounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
@@ -291,23 +375,90 @@ public class SplitPresenterTest {
assertEquals("Secondary bounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("Primary bounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
- public void testGetBoundsForPosition_splitHorizontally() {
+ public void testGetRelBoundsForPosition_splitVertically_isRelativeToParent() {
+ // Calculate based on TASK_BOUNDS.
+ final Rect primaryBounds = getSplitBounds(true /* isPrimary */,
+ false /* splitHorizontally */);
+ final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
+ false /* splitHorizontally */);
+
+ // Offset TaskBounds to 100, 100. The returned rel bounds shouldn't be affected.
+ final Rect taskBounds = new Rect(TASK_BOUNDS);
+ taskBounds.offset(100, 100);
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties(taskBounds);
+ SplitAttributes splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+ .build();
+
+ assertEquals("Primary bounds must be reported.",
+ primaryBounds,
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
+
+ assertEquals("Secondary bounds must be reported.",
+ secondaryBounds,
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+
+ splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.RIGHT_TO_LEFT)
+ .build();
+
+ assertEquals("Secondary bounds must be reported.",
+ secondaryBounds,
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
+
+ assertEquals("Primary bounds must be reported.",
+ primaryBounds,
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+
+ splitAttributes = new SplitAttributes.Builder()
+ .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .build();
+ // Layout direction should follow screen layout for SplitAttributes.LayoutDirection.LOCALE.
+ taskProperties.getConfiguration().screenLayout |= Configuration.SCREENLAYOUT_LAYOUTDIR_RTL;
+
+ assertEquals("Secondary bounds must be reported.",
+ secondaryBounds,
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
+
+ assertEquals("Primary bounds must be reported.",
+ primaryBounds,
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ assertEquals("Task bounds must be reported.",
+ new Rect(),
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ }
+
+ @Test
+ public void testGetRelBoundsForPosition_splitHorizontally() {
final Rect primaryBounds = getSplitBounds(true /* isPrimary */,
true /* splitHorizontally */);
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
true /* splitHorizontally */);
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
.setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
@@ -315,14 +466,15 @@ public class SplitPresenterTest {
assertEquals("Primary bounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("Secondary bounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
@@ -331,23 +483,24 @@ public class SplitPresenterTest {
assertEquals("Secondary bounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("Primary bounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
- public void testGetBoundsForPosition_useHingeFallback() {
+ public void testGetRelBoundsForPosition_useHingeFallback() {
final Rect primaryBounds = getSplitBounds(true /* isPrimary */,
false /* splitHorizontally */);
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
false /* splitHorizontally */);
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(
SplitAttributes.SplitType.RatioSplitType.splitEqually()
@@ -360,14 +513,15 @@ public class SplitPresenterTest {
assertEquals("PrimaryBounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("SecondaryBounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
// Hinge is reported, but the host task is in multi-window mode. Still use fallback
// splitType.
@@ -378,14 +532,15 @@ public class SplitPresenterTest {
assertEquals("PrimaryBounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("SecondaryBounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
// Hinge is reported, and the host task is in fullscreen, but layout direction doesn't match
// folding area orientation. Still use fallback splitType.
@@ -396,19 +551,20 @@ public class SplitPresenterTest {
assertEquals("PrimaryBounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("SecondaryBounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
- public void testGetBoundsForPosition_fallbackToExpandContainers() {
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ public void testGetRelBoundsForPosition_fallbackToExpandContainers() {
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(
new SplitAttributes.SplitType.ExpandContainersSplitType()
@@ -417,19 +573,20 @@ public class SplitPresenterTest {
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
- public void testGetBoundsForPosition_useHingeSplitType() {
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ public void testGetRelBoundsForPosition_useHingeSplitType() {
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(
new SplitAttributes.SplitType.ExpandContainersSplitType()
@@ -454,14 +611,15 @@ public class SplitPresenterTest {
assertEquals("PrimaryBounds must be reported.",
primaryBounds,
- mPresenter.getBoundsForPosition(POSITION_START, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_START, taskProperties,
+ splitAttributes));
assertEquals("SecondaryBounds must be reported.",
secondaryBounds,
- mPresenter.getBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_END, taskProperties, splitAttributes));
assertEquals("Task bounds must be reported.",
new Rect(),
- mPresenter.getBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
+ mPresenter.getRelBoundsForPosition(POSITION_FILL, taskProperties, splitAttributes));
}
@Test
@@ -481,13 +639,13 @@ public class SplitPresenterTest {
splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
verify(mPresenter, never()).expandTaskFragment(any(), any());
- splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+ splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo();
assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded(
mTransaction, splitContainer, mActivity, secondaryActivity,
null /* secondaryIntent */));
- splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+ splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity));
secondaryTf.setInfo(mTransaction,
createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
@@ -497,7 +655,7 @@ public class SplitPresenterTest {
verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
- splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+ splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
clearInvocations(mPresenter);
assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
@@ -520,7 +678,8 @@ public class SplitPresenterTest {
.setShouldClearTop(false)
.build();
- mPresenter.createNewSplitContainer(mTransaction, mActivity, secondaryActivity, rule);
+ mPresenter.createNewSplitContainer(mTransaction, mActivity, secondaryActivity, rule,
+ SPLIT_ATTRIBUTES);
assertEquals(primaryTf, mController.getContainerWithActivity(mActivity));
final TaskFragmentContainer secondaryTf = mController.getContainerWithActivity(
@@ -539,37 +698,35 @@ public class SplitPresenterTest {
.setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
.setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
.build();
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
assertEquals(SPLIT_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, null /* minDimensionsPair */));
+ splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
final Pair<Size, Size> minDimensionsPair = new Pair<>(
new Size(TASK_BOUNDS.width(), TASK_BOUNDS.height()), null);
assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, minDimensionsPair));
+ splitPairRule, SPLIT_ATTRIBUTES, minDimensionsPair));
taskProperties.getConfiguration().windowConfiguration.setBounds(new Rect(
TASK_BOUNDS.left + 1, TASK_BOUNDS.top + 1, TASK_BOUNDS.right + 1,
TASK_BOUNDS.bottom + 1));
assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, null /* minDimensionsPair */));
+ splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
- .setSplitType(
- new SplitAttributes.SplitType.HingeSplitType(
- SplitAttributes.SplitType.RatioSplitType.splitEqually()
- )
- ).build();
+ .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
+ SplitAttributes.SplitType.RatioSplitType.splitEqually()))
+ .build();
final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
params -> splitAttributes;
mController.setSplitAttributesCalculator(calculator);
assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, null /* minDimensionsPair */));
+ splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
}
@Test
@@ -586,11 +743,26 @@ public class SplitPresenterTest {
.setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
.setDefaultSplitAttributes(hingeSplitAttrs)
.build();
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
doReturn(null).when(mPresenter).getFoldingFeature(any());
assertEquals(hingeSplitAttrs, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, null /* minDimensionsPair */));
+ splitPairRule, hingeSplitAttrs, null /* minDimensionsPair */));
+ }
+
+ @Test
+ public void testGetTaskWindowMetrics() {
+ final Configuration taskConfig = new Configuration();
+ taskConfig.windowConfiguration.setBounds(TASK_BOUNDS);
+ taskConfig.densityDpi = 123;
+ final TaskContainer.TaskProperties taskProperties = new TaskContainer.TaskProperties(
+ DEFAULT_DISPLAY, taskConfig);
+ doReturn(taskProperties).when(mPresenter).getTaskProperties(mActivity);
+
+ final WindowMetrics windowMetrics = mPresenter.getTaskWindowMetrics(mActivity);
+ assertEquals(TASK_BOUNDS, windowMetrics.getBounds());
+ assertEquals(123 * DisplayMetrics.DENSITY_DEFAULT_SCALE,
+ windowMetrics.getDensity(), 0f);
}
private Activity createMockActivity() {
@@ -602,12 +774,17 @@ public class SplitPresenterTest {
doReturn(activityConfig).when(mActivityResources).getConfiguration();
doReturn(new ActivityInfo()).when(activity).getActivityInfo();
doReturn(mock(IBinder.class)).when(activity).getActivityToken();
+ doReturn(TASK_ID).when(activity).getTaskId();
return activity;
}
- private static TaskContainer.TaskProperties getTaskProperty() {
+ private static TaskContainer.TaskProperties getTaskProperties() {
+ return getTaskProperties(TASK_BOUNDS);
+ }
+
+ private static TaskContainer.TaskProperties getTaskProperties(@NonNull Rect taskBounds) {
final Configuration configuration = new Configuration();
- configuration.windowConfiguration.setBounds(TASK_BOUNDS);
+ configuration.windowConfiguration.setBounds(taskBounds);
return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration);
}
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
index 62006bd51399..459b6d2c31f9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
@@ -16,9 +16,9 @@
package androidx.window.extensions.embedding;
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
@@ -86,27 +86,29 @@ public class TransactionManagerTest {
@Test
public void testSetTransactionOriginType() {
- // Return TRANSIT_CHANGE if there is no trigger type set.
+ // Return TASK_FRAGMENT_TRANSIT_CHANGE if there is no trigger type set.
TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
- assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType());
+ assertEquals(TASK_FRAGMENT_TRANSIT_CHANGE,
+ transactionRecord.getTransactionTransitionType());
// Return the first set type.
mTransactionManager.getCurrentTransactionRecord().abort();
transactionRecord = mTransactionManager.startNewTransaction();
- transactionRecord.setOriginType(TRANSIT_OPEN);
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_OPEN);
- assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType());
+ assertEquals(TASK_FRAGMENT_TRANSIT_OPEN, transactionRecord.getTransactionTransitionType());
- transactionRecord.setOriginType(TRANSIT_CLOSE);
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
- assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType());
+ assertEquals(TASK_FRAGMENT_TRANSIT_OPEN, transactionRecord.getTransactionTransitionType());
// Reset when #startNewTransaction().
transactionRecord.abort();
transactionRecord = mTransactionManager.startNewTransaction();
- assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType());
+ assertEquals(TASK_FRAGMENT_TRANSIT_CHANGE,
+ transactionRecord.getTransactionTransitionType());
}
@Test
diff --git a/libs/WindowManager/Jetpack/window-extensions-core-release.aar b/libs/WindowManager/Jetpack/window-extensions-core-release.aar
deleted file mode 100644
index 96ff840b984b..000000000000
--- a/libs/WindowManager/Jetpack/window-extensions-core-release.aar
+++ /dev/null
Binary files differ
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
deleted file mode 100644
index c3b6916121d0..000000000000
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ /dev/null
Binary files differ
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index c00a749b7671..b232555c64dd 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -46,6 +46,8 @@ filegroup {
"src/com/android/wm/shell/common/split/SplitScreenConstants.java",
"src/com/android/wm/shell/sysui/ShellSharedConstants.java",
"src/com/android/wm/shell/common/TransactionPool.java",
+ "src/com/android/wm/shell/common/bubbles/*.java",
+ "src/com/android/wm/shell/common/TriangleShape.java",
"src/com/android/wm/shell/animation/Interpolators.java",
"src/com/android/wm/shell/pip/PipContentOverlay.java",
"src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java",
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 8881be7326a9..36d3313a9f3b 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -21,5 +21,6 @@
<uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
+ <uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
</manifest>
diff --git a/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
new file mode 100644
index 000000000000..c82a70c9a44e
--- /dev/null
+++ b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.wm.shell;
+
+option java_multiple_files = true;
+
+/* Represents a file full of transition entries.
+ Encoded, it should start with 0x09 0x57 0x4D 0x53 0x54 0x52 0x41 0x43 0x45 (.WMSTRACE), such
+ that it can be easily identified. */
+message WmShellTransitionTraceProto {
+ /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+ (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+ constants into .proto files. */
+ enum MagicNumber {
+ INVALID = 0;
+ MAGIC_NUMBER_L = 0x54534D57; /* WMST (little-endian ASCII) */
+ MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */
+ }
+
+ // Must be the first field, set to value in MagicNumber
+ required fixed64 magic_number = 1;
+ repeated Transition transitions = 2;
+ repeated HandlerMapping handlerMappings = 3;
+ /* offset between real-time clock and elapsed time clock in nanoseconds.
+ Calculated as: 1000000 * System.currentTimeMillis() - SystemClock.elapsedRealtimeNanos() */
+ optional fixed64 real_to_elapsed_time_offset_nanos = 4;
+}
+
+message Transition {
+ required int32 id = 1;
+ optional int64 dispatch_time_ns = 2;
+ optional int32 handler = 3;
+ optional int64 merge_time_ns = 4;
+ optional int64 merge_request_time_ns = 5;
+ optional int32 merged_into = 6;
+ optional int64 abort_time_ns = 7;
+}
+
+message HandlerMapping {
+ required int32 id = 1;
+ required string name = 2;
+}
diff --git a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml
index 7475abac4695..b2d59396d58d 100644
--- a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml
+++ b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml
@@ -18,6 +18,20 @@
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <set>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="scaleX"
+ android:valueTo="1.0"
+ android:valueType="floatType"/>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="scaleY"
+ android:valueTo="1.0"
+ android:valueType="floatType"/>
+ </set>
+ </item>
<item android:state_focused="true">
<set>
<objectAnimator
diff --git a/libs/WindowManager/Shell/res/color-night/taskbar_background.xml b/libs/WindowManager/Shell/res/color/taskbar_background_dark.xml
index 01df006f1bd2..f75d842c0d57 100644
--- a/libs/WindowManager/Shell/res/color-night/taskbar_background.xml
+++ b/libs/WindowManager/Shell/res/color/taskbar_background_dark.xml
@@ -16,5 +16,5 @@
-->
<!-- Should be the same as in packages/apps/Launcher3/res/color-night-v31/taskbar_background.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="20" />
+ <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml
index ce8640df0093..67467bbc72ae 100644
--- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml
+++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml
@@ -15,5 +15,5 @@
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@color/tv_pip_menu_icon_unfocused" />
+ <item android:color="@color/tv_window_menu_icon_unfocused" />
</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml
index 4f5e63dac5c0..4182bfeefa1b 100644
--- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml
+++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ 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.
@@ -16,6 +16,6 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
- android:color="@color/tv_pip_menu_icon_bg_focused" />
- <item android:color="@color/tv_pip_menu_icon_bg_unfocused" />
+ android:color="@color/tv_window_menu_close_icon_bg_focused" />
+ <item android:color="@color/tv_window_menu_close_icon_bg_unfocused" />
</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml
index 275870450493..45205d2a7138 100644
--- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml
+++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ 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.
@@ -16,8 +16,8 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
- android:color="@color/tv_pip_menu_icon_focused" />
+ android:color="@color/tv_window_menu_icon_focused" />
<item android:state_enabled="false"
- android:color="@color/tv_pip_menu_icon_disabled" />
- <item android:color="@color/tv_pip_menu_icon_unfocused" />
+ android:color="@color/tv_window_menu_icon_disabled" />
+ <item android:color="@color/tv_window_menu_icon_unfocused" />
</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml
index 6cbf66f00df7..1bd26e1d6583 100644
--- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml
+++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml
@@ -16,6 +16,6 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
- android:color="@color/tv_pip_menu_close_icon_bg_focused" />
- <item android:color="@color/tv_pip_menu_close_icon_bg_unfocused" />
+ android:color="@color/tv_window_menu_icon_bg_focused" />
+ <item android:color="@color/tv_window_menu_icon_bg_unfocused" />
</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_left_hand.xml b/libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_left_hand.xml
new file mode 100644
index 000000000000..fbcf6d70f62d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_left_hand.xml
@@ -0,0 +1,27 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <group android:scaleX="-1" android:translateX="960">
+ <path
+ android:fillColor="?android:attr/textColorSecondary"
+ android:pathData="M432.46,48Q522,48 585,110.92Q648,173.83 648,264Q648,314 627.5,358.5Q607,403 566,432L528,432L528,370Q551,349 563.5,321.5Q576,294 576,263.78Q576,204.39 534,162.2Q492,120 432,120Q372,120 330,162Q288,204 288,264.31Q288,295 300,323Q312,351 336,370L336,456Q280,430 248,378Q216,326 216,264Q216,173.83 278.97,110.92Q341.94,48 432.46,48ZM414,864Q399.53,864 386.77,859Q374,854 363,843L144,624L211,557Q225,543 243,538Q261,533 279,538L336,552L336,288Q337,248 364.57,220Q392.14,192 432.07,192Q472,192 500,219.84Q528,247.68 528,288L528,432L576,432Q576,432 576,432Q576,432 576,432L715,497Q744,511 758,538Q772,565 767,596L737,802Q732,828 711.76,846Q691.52,864 666,864L414,864ZM414,792L666,792L698,569Q698,569 698,569Q698,569 698,569L559,504L456,504L456,288Q456,278 449,271Q442,264 432,264Q422,264 415,271Q408,278 408,288L408,644L262,608L246,624L414,792ZM666,792L414,792L414,792L414,792L414,792L414,792Q414,792 422,792Q430,792 439.5,792Q449,792 454.43,792Q459.86,792 459.86,792L459.86,792L529,792L666,792Q666,792 666,792Q666,792 666,792L666,792Z"/>
+ </group>
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_right_hand.xml b/libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_right_hand.xml
new file mode 100644
index 000000000000..d36df4ba533e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable-night/reachability_education_ic_right_hand.xml
@@ -0,0 +1,25 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="?android:attr/textColorSecondary"
+ android:pathData="M432.46,48Q522,48 585,110.92Q648,173.83 648,264Q648,314 627.5,358.5Q607,403 566,432L528,432L528,370Q551,349 563.5,321.5Q576,294 576,263.78Q576,204.39 534,162.2Q492,120 432,120Q372,120 330,162Q288,204 288,264.31Q288,295 300,323Q312,351 336,370L336,456Q280,430 248,378Q216,326 216,264Q216,173.83 278.97,110.92Q341.94,48 432.46,48ZM414,864Q399.53,864 386.77,859Q374,854 363,843L144,624L211,557Q225,543 243,538Q261,533 279,538L336,552L336,288Q337,248 364.57,220Q392.14,192 432.07,192Q472,192 500,219.84Q528,247.68 528,288L528,432L576,432Q576,432 576,432Q576,432 576,432L715,497Q744,511 758,538Q772,565 767,596L737,802Q732,828 711.76,846Q691.52,864 666,864L414,864ZM414,792L666,792L698,569Q698,569 698,569Q698,569 698,569L559,504L456,504L456,288Q456,278 449,271Q442,264 432,264Q422,264 415,271Q408,278 408,288L408,644L262,608L246,624L414,792ZM666,792L414,792L414,792L414,792L414,792L414,792Q414,792 422,792Q430,792 439.5,792Q449,792 454.43,792Q459.86,792 459.86,792L459.86,792L529,792L666,792Q666,792 666,792Q666,792 666,792L666,792Z"/>
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
index 96d2d7c954d8..d2360e9b1ae0 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
@@ -16,9 +16,10 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
<solid
- android:color="@android:color/system_neutral1_800"
+ android:color="?androidprv:attr/materialColorSurfaceContainerHigh"
/>
<corners android:radius="20dp" />
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_bg.xml
index 4bd2f134d027..8fd2e68f6451 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_bg.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_bg.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="?androidprv:attr/colorSurface" />
+ <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
<corners
android:bottomLeftRadius="?android:attr/dialogCornerRadius"
android:topLeftRadius="?android:attr/dialogCornerRadius"
diff --git a/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml
deleted file mode 100644
index 8779cc09715b..000000000000
--- a/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml
+++ /dev/null
@@ -1,31 +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.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group android:scaleX="0.5"
- android:scaleY="0.5"
- android:translateX="6.0"
- android:translateY="6.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M5.958,37.708Q4.458,37.708 3.354,36.604Q2.25,35.5 2.25,34V18.292Q2.25,16.792 3.354,15.688Q4.458,14.583 5.958,14.583H9.5V5.958Q9.5,4.458 10.625,3.354Q11.75,2.25 13.208,2.25H34Q35.542,2.25 36.646,3.354Q37.75,4.458 37.75,5.958V21.667Q37.75,23.167 36.646,24.271Q35.542,25.375 34,25.375H30.5V34Q30.5,35.5 29.396,36.604Q28.292,37.708 26.792,37.708ZM5.958,34H26.792Q26.792,34 26.792,34Q26.792,34 26.792,34V21.542H5.958V34Q5.958,34 5.958,34Q5.958,34 5.958,34ZM30.5,21.667H34Q34,21.667 34,21.667Q34,21.667 34,21.667V9.208H13.208V14.583H26.833Q28.375,14.583 29.438,15.667Q30.5,16.75 30.5,18.25Z"/>
- </group>
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml
deleted file mode 100644
index ea0fbb0e5d33..000000000000
--- a/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml
+++ /dev/null
@@ -1,31 +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.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group android:scaleX="0.5"
- android:scaleY="0.5"
- android:translateX="6.0"
- android:translateY="6.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M18.167,21.875H29.833V10.208H18.167ZM7.875,35.833Q6.375,35.833 5.271,34.729Q4.167,33.625 4.167,32.125V7.875Q4.167,6.375 5.271,5.271Q6.375,4.167 7.875,4.167H32.125Q33.625,4.167 34.729,5.271Q35.833,6.375 35.833,7.875V32.125Q35.833,33.625 34.729,34.729Q33.625,35.833 32.125,35.833ZM7.875,32.125H32.125Q32.125,32.125 32.125,32.125Q32.125,32.125 32.125,32.125V7.875Q32.125,7.875 32.125,7.875Q32.125,7.875 32.125,7.875H7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125ZM7.875,7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125V7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875Z"/>
- </group>
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml
deleted file mode 100644
index c55cbe2d054c..000000000000
--- a/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml
+++ /dev/null
@@ -1,31 +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.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group android:scaleX="0.5"
- android:scaleY="0.5"
- android:translateX="6.0"
- android:translateY="6.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M34.042,14.625V9.333Q34.042,9.333 34.042,9.333Q34.042,9.333 34.042,9.333H28.708V5.708H33.917Q35.458,5.708 36.562,6.833Q37.667,7.958 37.667,9.458V14.625ZM2.375,14.625V9.458Q2.375,7.958 3.479,6.833Q4.583,5.708 6.125,5.708H11.292V9.333H6Q6,9.333 6,9.333Q6,9.333 6,9.333V14.625ZM28.708,34.25V30.667H34.042Q34.042,30.667 34.042,30.667Q34.042,30.667 34.042,30.667V25.333H37.667V30.542Q37.667,32 36.562,33.125Q35.458,34.25 33.917,34.25ZM6.125,34.25Q4.583,34.25 3.479,33.125Q2.375,32 2.375,30.542V25.333H6V30.667Q6,30.667 6,30.667Q6,30.667 6,30.667H11.292V34.25ZM9.333,27.292V12.667H30.708V27.292ZM12.917,23.708H27.125V16.25H12.917ZM12.917,23.708V16.25V23.708Z"/>
- </group>
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_more_button.xml b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml
deleted file mode 100644
index 447df43dfddd..000000000000
--- a/libs/WindowManager/Shell/res/drawable/caption_more_button.xml
+++ /dev/null
@@ -1,31 +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.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group android:scaleX="0.5"
- android:scaleY="0.5"
- android:translateX="6.0"
- android:translateY="6.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M8.083,22.833Q6.917,22.833 6.104,22Q5.292,21.167 5.292,20Q5.292,18.833 6.125,18Q6.958,17.167 8.125,17.167Q9.292,17.167 10.125,18Q10.958,18.833 10.958,20Q10.958,21.167 10.125,22Q9.292,22.833 8.083,22.833ZM20,22.833Q18.833,22.833 18,22Q17.167,21.167 17.167,20Q17.167,18.833 18,18Q18.833,17.167 20,17.167Q21.167,17.167 22,18Q22.833,18.833 22.833,20Q22.833,21.167 22,22Q21.167,22.833 20,22.833ZM31.875,22.833Q30.708,22.833 29.875,22Q29.042,21.167 29.042,20Q29.042,18.833 29.875,18Q30.708,17.167 31.917,17.167Q33.083,17.167 33.896,18Q34.708,18.833 34.708,20Q34.708,21.167 33.875,22Q33.042,22.833 31.875,22.833Z"/>
- </group>
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_select_button.xml b/libs/WindowManager/Shell/res/drawable/caption_select_button.xml
deleted file mode 100644
index 8c60c8407174..000000000000
--- a/libs/WindowManager/Shell/res/drawable/caption_select_button.xml
+++ /dev/null
@@ -1,30 +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.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group
- android:translateX="4.0"
- android:translateY="6.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M13.7021 12.5833L16.5676 15.5L15.426 16.7333L12.526 13.8333L10.4426 15.9167V10.5H15.9176L13.7021 12.5833ZM13.8343 3.83333H15.501V5.5H13.8343V3.83333ZM15.501 2.16667H13.8343V0.566667C14.751 0.566667 15.501 1.33333 15.501 2.16667ZM10.501 0.5H12.1676V2.16667H10.501V0.5ZM13.8343 7.16667H15.501V8.83333H13.8343V7.16667ZM5.50098 15.5H3.83431V13.8333H5.50098V15.5ZM2.16764 5.5H0.500977V3.83333H2.16764V5.5ZM2.16764 0.566667V2.16667H0.500977C0.500977 1.33333 1.33431 0.566667 2.16764 0.566667ZM2.16764 12.1667H0.500977V10.5H2.16764V12.1667ZM5.50098 2.16667H3.83431V0.5H5.50098V2.16667ZM8.83431 2.16667H7.16764V0.5H8.83431V2.16667ZM8.83431 15.5H7.16764V13.8333H8.83431V15.5ZM2.16764 8.83333H0.500977V7.16667H2.16764V8.83333ZM2.16764 15.5667C1.25098 15.5667 0.500977 14.6667 0.500977 13.8333H2.16764V15.5667Z"/>
- </group>
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml
deleted file mode 100644
index c334a543a86a..000000000000
--- a/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml
+++ /dev/null
@@ -1,28 +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.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group android:translateX="6.0"
- android:translateY="8.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M18 14L13 14L13 2L18 2L18 14ZM20 14L20 2C20 0.9 19.1 -3.93402e-08 18 -8.74228e-08L13 -3.0598e-07C11.9 -3.54062e-07 11 0.9 11 2L11 14C11 15.1 11.9 16 13 16L18 16C19.1 16 20 15.1 20 14ZM7 14L2 14L2 2L7 2L7 14ZM9 14L9 2C9 0.9 8.1 -5.20166e-07 7 -5.68248e-07L2 -7.86805e-07C0.9 -8.34888e-07 -3.93403e-08 0.9 -8.74228e-08 2L-6.11959e-07 14C-6.60042e-07 15.1 0.9 16 2 16L7 16C8.1 16 9 15.1 9 14Z"/> </group>
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
index 27e0b184f427..5d7771366bec 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
@@ -14,13 +14,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24"
- android:tint="@color/decor_button_dark_color">
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<group android:translateY="8.0">
<path
- android:fillColor="@android:color/white" android:pathData="M3,5V3H21V5Z"/>
+ android:fillColor="@android:color/black" android:pathData="M3,5V3H21V5Z"/>
</group>
</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
index c6e634c6622c..4ee10f429b37 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
@@ -17,6 +17,5 @@
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
- <corners android:radius="@dimen/caption_menu_corner_radius" />
- <stroke android:width="1dp" android:color="#b3b3b3"/>
+ <corners android:radius="@dimen/desktop_mode_handle_menu_corner_radius" />
</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_close.xml
index 3e0011c65942..b7521d4200c0 100644
--- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_close.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ 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.
@@ -15,11 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
+ android:height="20dp"
+ android:tint="#000000"
+ android:viewportHeight="24"
+ android:viewportWidth="24"
+ android:width="20dp">
<path
- android:fillColor="@color/tv_pip_menu_focus_border"
- android:pathData="M14,7l-5,5 5,5V7z"/>
-</vector> \ No newline at end of file
+ android:fillColor="@android:color/white"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml
new file mode 100644
index 000000000000..e2b724b8abfd
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:pathData="M16.667,15H3.333V5H16.667V15ZM16.667,16.667C17.583,16.667 18.333,15.917 18.333,15V5C18.333,4.083 17.583,3.333 16.667,3.333H3.333C2.417,3.333 1.667,4.083 1.667,5V15C1.667,15.917 2.417,16.667 3.333,16.667H16.667ZM15,6.667H9.167V8.333H13.333V10H15V6.667ZM5,9.167H12.5V13.333H5V9.167Z"
+ android:fillColor="#1C1C14"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_collapse_menu_button.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_floating.xml
index 166552dcb9e8..b0ea98e5f788 100644
--- a/libs/WindowManager/Shell/res/drawable/caption_collapse_menu_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_floating.xml
@@ -15,16 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24.0dp"
- android:height="24.0dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0"
->
- <group android:scaleX="1.25"
- android:scaleY="1.75"
- android:translateY="6.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M10.3937 6.93935L11.3337 5.99935L6.00033 0.666016L0.666992 5.99935L1.60699 6.93935L6.00033 2.55268"/>
- </group>
+ android:width="21dp"
+ android:height="20dp"
+ android:viewportWidth="21"
+ android:viewportHeight="20">
+ <path
+ android:pathData="M3.667,15H17V5H3.667V15ZM18.667,15C18.667,15.917 17.917,16.667 17,16.667H3.667C2.75,16.667 2,15.917 2,15V5C2,4.083 2.75,3.333 3.667,3.333H17C17.917,3.333 18.667,4.083 18.667,5V15ZM11.167,6.667H15.333V11.667H11.167V6.667Z"
+ android:fillColor="#1C1C14"
+ android:fillType="evenOdd"/>
</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_close_button.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml
index e258564c70f7..99e1d268c97c 100644
--- a/libs/WindowManager/Shell/res/drawable/caption_close_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml
@@ -15,16 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group android:scaleX="0.5"
- android:scaleY="0.5"
- android:translateY="4.0">
- <path
- android:fillColor="#FFFF0000"
- android:pathData="M12.45,38.35 L9.65,35.55 21.2,24 9.65,12.45 12.45,9.65 24,21.2 35.55,9.65 38.35,12.45 26.8,24 38.35,35.55 35.55,38.35 24,26.8Z"/>
- </group>
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:pathData="M3.333,15H16.667V5H3.333V15ZM18.333,15C18.333,15.917 17.583,16.667 16.667,16.667H3.333C2.417,16.667 1.667,15.917 1.667,15V5C1.667,4.083 2.417,3.333 3.333,3.333H16.667C17.583,3.333 18.333,4.083 18.333,5V15ZM5,6.667H15V13.333H5V6.667Z"
+ android:fillColor="#1C1C14"
+ android:fillType="evenOdd"/>
</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml
new file mode 100644
index 000000000000..79a91250bb78
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml
@@ -0,0 +1,34 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:pathData="M18.333,5.833L18.333,8.333L16.667,8.333L16.667,5.833L13.333,5.833L13.333,4.167L16.667,4.167C17.587,4.167 18.333,4.913 18.333,5.833Z"
+ android:fillColor="#1C1C14"/>
+ <path
+ android:pathData="M6.667,4.167L3.333,4.167C2.413,4.167 1.667,4.913 1.667,5.833L1.667,8.333L3.333,8.333L3.333,5.833L6.667,5.833L6.667,4.167Z"
+ android:fillColor="#1C1C14"/>
+ <path
+ android:pathData="M6.667,14.167L3.333,14.167L3.333,11.667L1.667,11.667L1.667,14.167C1.667,15.087 2.413,15.833 3.333,15.833L6.667,15.833L6.667,14.167Z"
+ android:fillColor="#1C1C14"/>
+ <path
+ android:pathData="M13.333,15.833L16.667,15.833C17.587,15.833 18.333,15.087 18.333,14.167L18.333,11.667L16.667,11.667L16.667,14.167L13.333,14.167L13.333,15.833Z"
+ android:fillColor="#1C1C14"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml
new file mode 100644
index 000000000000..7c4f49979455
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml
@@ -0,0 +1,25 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:pathData="M15.701,14.583L18.567,17.5L17.425,18.733L14.525,15.833L12.442,17.917V12.5H17.917L15.701,14.583ZM15.833,5.833H17.5V7.5H15.833V5.833ZM17.5,4.167H15.833V2.567C16.75,2.567 17.5,3.333 17.5,4.167ZM12.5,2.5H14.167V4.167H12.5V2.5ZM15.833,9.167H17.5V10.833H15.833V9.167ZM7.5,17.5H5.833V15.833H7.5V17.5ZM4.167,7.5H2.5V5.833H4.167V7.5ZM4.167,2.567V4.167H2.5C2.5,3.333 3.333,2.567 4.167,2.567ZM4.167,14.167H2.5V12.5H4.167V14.167ZM7.5,4.167H5.833V2.5H7.5V4.167ZM10.833,4.167H9.167V2.5H10.833V4.167ZM10.833,17.5H9.167V15.833H10.833V17.5ZM4.167,10.833H2.5V9.167H4.167V10.833ZM4.167,17.567C3.25,17.567 2.5,16.667 2.5,15.833H4.167V17.567Z"
+ android:fillColor="#1C1C14"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_screenshot_button.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml
index 7c86888f5226..853ab60e046f 100644
--- a/libs/WindowManager/Shell/res/drawable/caption_screenshot_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml
@@ -15,16 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32.0dp"
- android:height="32.0dp"
- android:viewportWidth="32.0"
- android:viewportHeight="32.0"
->
- <group android:scaleX="0.5"
- android:scaleY="0.5"
- android:translateY="4.0">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M10,38V28.35H13V35H19.65V38ZM10,19.65V10H19.65V13H13V19.65ZM28.35,38V35H35V28.35H38V38ZM35,19.65V13H28.35V10H38V19.65Z"/>
- </group>
+ android:width="21dp"
+ android:height="20dp"
+ android:viewportWidth="21"
+ android:viewportHeight="20">
+ <path
+ android:pathData="M17.333,15H4V5H17.333V15ZM17.333,16.667C18.25,16.667 19,15.917 19,15V5C19,4.083 18.25,3.333 17.333,3.333H4C3.083,3.333 2.333,4.083 2.333,5V15C2.333,15.917 3.083,16.667 4,16.667H17.333ZM9.833,6.667H5.667V13.333H9.833V6.667ZM11.5,6.667H15.667V13.333H11.5V6.667Z"
+ android:fillColor="#1C1C14"
+ android:fillType="evenOdd"/>
</vector>
diff --git a/libs/WindowManager/Shell/res/color/taskbar_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml
index 876ee02a8adf..1f3e3a4c5b22 100644
--- a/libs/WindowManager/Shell/res/color/taskbar_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ 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.
@@ -14,7 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<!-- Should be the same as in packages/apps/Launcher3/res/color-v31/taskbar_background.xml -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector> \ No newline at end of file
+<shape android:shape="rectangle"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@android:color/white" />
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml
new file mode 100644
index 000000000000..022594982ca3
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.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.
+ -->
+<shape android:shape="rectangle"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#bf309fb5" />
+ <corners android:radius="20dp" />
+ <stroke android:width="1dp" color="#A00080FF"/>
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_24.xml b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_24.xml
new file mode 100644
index 000000000000..3e0297ab612b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_24.xml
@@ -0,0 +1,21 @@
+<?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
+ -->
+<vector android:height="24dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/black" android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml
index 3e1a2bce2393..e7c89d1f9c76 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml
@@ -15,7 +15,8 @@
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="@color/compat_controls_background"/>
+ <solid android:color="?androidprv:attr/colorSurface"/>
<corners android:radius="@dimen/letterbox_education_dialog_corner_radius"/>
</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml
index a2699681e656..72ebef625ffc 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml
@@ -15,6 +15,7 @@
~ limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:insetTop="@dimen/letterbox_education_dialog_vertical_inset"
android:insetBottom="@dimen/letterbox_education_dialog_vertical_inset">
<ripple android:color="@color/letterbox_education_dismiss_button_background_ripple">
@@ -31,7 +32,7 @@
</item>
<item>
<shape android:shape="rectangle">
- <solid android:color="@color/letterbox_education_accent_primary"/>
+ <solid android:color="?androidprv:attr/colorAccentPrimaryVariant"/>
<corners android:radius="@dimen/letterbox_education_dialog_button_radius"/>
<padding android:left="@dimen/letterbox_education_dialog_horizontal_padding"
android:top="@dimen/letterbox_education_dialog_vertical_padding"
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml
index ddfb5c27e701..4a1e7485ed19 100644
--- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml
@@ -15,12 +15,13 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:width="@dimen/letterbox_education_dialog_title_icon_width"
android:height="@dimen/letterbox_education_dialog_title_icon_height"
android:viewportWidth="45"
android:viewportHeight="44">
<path
- android:fillColor="@color/letterbox_education_accent_primary"
+ android:fillColor="?androidprv:attr/colorAccentPrimaryVariant"
android:pathData="M11 40H19C19 42.2 17.2 44 15 44C12.8 44 11 42.2 11 40ZM7 38L23 38V34L7 34L7 38ZM30 19C30 26.64 24.68 30.72 22.46 32L7.54 32C5.32 30.72 0 26.64 0 19C0 10.72 6.72 4 15 4C23.28 4 30 10.72 30 19ZM26 19C26 12.94 21.06 8 15 8C8.94 8 4 12.94 4 19C4 23.94 6.98 26.78 8.7 28L21.3 28C23.02 26.78 26 23.94 26 19ZM39.74 14.74L37 16L39.74 17.26L41 20L42.26 17.26L45 16L42.26 14.74L41 12L39.74 14.74ZM35 12L36.88 7.88L41 6L36.88 4.12L35 0L33.12 4.12L29 6L33.12 7.88L35 12Z" />
</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml
deleted file mode 100644
index 1a3446249573..000000000000
--- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2021 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:fillColor="@color/tv_pip_menu_focus_border"
- android:pathData="M7,14l5,-5 5,5H7z"/>
-</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml
index 029d83881165..05d243dfd5f3 100644
--- a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml
+++ b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml
@@ -18,11 +18,10 @@
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
- android:viewportHeight="960"
- android:tint="?attr/colorControlNormal">
+ android:viewportHeight="960">
<group android:scaleX="-1" android:translateX="960">
<path
- android:fillColor="?android:attr/textColorSecondary"
+ android:fillColor="?android:attr/textColorSecondaryInverse"
android:pathData="M432.46,48Q522,48 585,110.92Q648,173.83 648,264Q648,314 627.5,358.5Q607,403 566,432L528,432L528,370Q551,349 563.5,321.5Q576,294 576,263.78Q576,204.39 534,162.2Q492,120 432,120Q372,120 330,162Q288,204 288,264.31Q288,295 300,323Q312,351 336,370L336,456Q280,430 248,378Q216,326 216,264Q216,173.83 278.97,110.92Q341.94,48 432.46,48ZM414,864Q399.53,864 386.77,859Q374,854 363,843L144,624L211,557Q225,543 243,538Q261,533 279,538L336,552L336,288Q337,248 364.57,220Q392.14,192 432.07,192Q472,192 500,219.84Q528,247.68 528,288L528,432L576,432Q576,432 576,432Q576,432 576,432L715,497Q744,511 758,538Q772,565 767,596L737,802Q732,828 711.76,846Q691.52,864 666,864L414,864ZM414,792L666,792L698,569Q698,569 698,569Q698,569 698,569L559,504L456,504L456,288Q456,278 449,271Q442,264 432,264Q422,264 415,271Q408,278 408,288L408,644L262,608L246,624L414,792ZM666,792L414,792L414,792L414,792L414,792L414,792Q414,792 422,792Q430,792 439.5,792Q449,792 454.43,792Q459.86,792 459.86,792L459.86,792L529,792L666,792Q666,792 666,792Q666,792 666,792L666,792Z"/>
</group>
</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml
index 592f899d2ecc..7bf243fe7f33 100644
--- a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml
+++ b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml
@@ -18,9 +18,8 @@
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
- android:viewportHeight="960"
- android:tint="?attr/colorControlNormal">
+ android:viewportHeight="960">
<path
- android:fillColor="?android:attr/textColorSecondary"
+ android:fillColor="?android:attr/textColorSecondaryInverse"
android:pathData="M432.46,48Q522,48 585,110.92Q648,173.83 648,264Q648,314 627.5,358.5Q607,403 566,432L528,432L528,370Q551,349 563.5,321.5Q576,294 576,263.78Q576,204.39 534,162.2Q492,120 432,120Q372,120 330,162Q288,204 288,264.31Q288,295 300,323Q312,351 336,370L336,456Q280,430 248,378Q216,326 216,264Q216,173.83 278.97,110.92Q341.94,48 432.46,48ZM414,864Q399.53,864 386.77,859Q374,854 363,843L144,624L211,557Q225,543 243,538Q261,533 279,538L336,552L336,288Q337,248 364.57,220Q392.14,192 432.07,192Q472,192 500,219.84Q528,247.68 528,288L528,432L576,432Q576,432 576,432Q576,432 576,432L715,497Q744,511 758,538Q772,565 767,596L737,802Q732,828 711.76,846Q691.52,864 666,864L414,864ZM414,792L666,792L698,569Q698,569 698,569Q698,569 698,569L559,504L456,504L456,288Q456,278 449,271Q442,264 432,264Q422,264 415,271Q408,278 408,288L408,644L262,608L246,624L414,792ZM666,792L414,792L414,792L414,792L414,792L414,792Q414,792 422,792Q430,792 439.5,792Q449,792 454.43,792Q459.86,792 459.86,792L459.86,792L529,792L666,792Q666,792 666,792Q666,792 666,792L666,792Z"/>
</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml
deleted file mode 100644
index 1938f4562e97..000000000000
--- a/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml
+++ /dev/null
@@ -1,21 +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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <corners android:radius="@dimen/pip_menu_button_radius" />
- <solid android:color="@color/tv_pip_menu_icon_bg" />
-</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml
index 846fdb3e8a58..7085a2c72c86 100644
--- a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml
+++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml
@@ -15,7 +15,7 @@
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
- android:exitFadeDuration="@integer/pip_menu_fade_animation_duration">
+ android:exitFadeDuration="@integer/tv_window_menu_fade_animation_duration">
<item android:state_activated="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/pip_menu_border_corner_radius" />
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_focus.xml
index f6b3c72e3cb5..a348b148afb4 100644
--- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml
+++ b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_focus.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ 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.
@@ -14,12 +13,14 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
+ android:width="@dimen/tv_window_menu_icon_size"
+ android:height="@dimen/tv_window_menu_icon_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+
<path
- android:fillColor="@color/tv_pip_menu_focus_border"
- android:pathData="M10,17l5,-5 -5,-5v10z"/>
-</vector> \ No newline at end of file
+ android:fillColor="#FFFFFF"
+ android:pathData="M17,4h3c1.1,0 2,0.9 2,2v2h-2L20,6h-3L17,4zM4,8L4,6h3L7,4L4,4c-1.1,0 -2,0.9 -2,2v2h2zM20,16v2h-3v2h3c1.1,0 2,-0.9 2,-2v-2h-2zM7,18L4,18v-2L2,16v2c0,1.1 0.9,2 2,2h3v-2zM18,8L6,8v8h12L18,8z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_swap.xml
index d8f356164358..c5d54c5fa4f2 100644
--- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml
+++ b/libs/WindowManager/Shell/res/drawable/tv_split_menu_ic_swap.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ 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.
@@ -14,12 +13,13 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
+ android:width="@dimen/tv_window_menu_icon_size"
+ android:height="@dimen/tv_window_menu_icon_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
<path
- android:fillColor="@color/tv_pip_menu_focus_border"
- android:pathData="M7,10l5,5 5,-5H7z"/>
+ android:fillColor="#FFFFFF"
+ android:pathData="M6.99,11L3,15l3.99,4v-3H14v-2H6.99v-3zM21,9l-3.99,-4v3H10v2h7.01v3L21,9z"/>
</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml
new file mode 100644
index 000000000000..4c28e519afa7
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/tv_window_menu_button_radius" />
+ <solid android:color="@android:color/white" />
+</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
new file mode 100644
index 000000000000..681a52bea2b2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
@@ -0,0 +1,24 @@
+<?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.
+ -->
+<com.android.wm.shell.bubbles.bar.BubbleBarExpandedView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:orientation="vertical"
+ android:id="@+id/bubble_bar_expanded_view">
+
+</com.android.wm.shell.bubbles.bar.BubbleBarExpandedView>
diff --git a/libs/WindowManager/Shell/res/layout/bubble_flyout.xml b/libs/WindowManager/Shell/res/layout/bubble_flyout.xml
index 7fdf290efd09..65a07a718677 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_flyout.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_flyout.xml
@@ -13,7 +13,8 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<LinearLayout
android:id="@+id/bubble_flyout_text_container"
@@ -48,6 +49,7 @@
android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
android:maxLines="1"
android:ellipsize="end"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/>
<TextView
@@ -57,6 +59,7 @@
android:fontFamily="@*android:string/config_bodyFontFamily"
android:maxLines="2"
android:ellipsize="end"
+ android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/>
</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
index 0cf6d73162d2..f88d63d796ea 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml
@@ -16,6 +16,7 @@
-->
<com.android.wm.shell.common.AlphaOptimizedButton
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
style="@android:style/Widget.DeviceDefault.Button.Borderless"
android:id="@+id/manage_button"
android:layout_gravity="start"
@@ -27,6 +28,6 @@
android:focusable="true"
android:text="@string/manage_bubbles_text"
android:textSize="@*android:dimen/text_size_body_2_material"
- android:textColor="@*android:color/system_neutral1_50"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
android:background="@drawable/bubble_manage_btn_bg"
/> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
index 298ad3025b00..10c9562cf651 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml
@@ -15,6 +15,7 @@
~ limitations under the License
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bubble_manage_menu_bg"
@@ -41,6 +42,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault"
android:text="@string/bubble_dismiss_text" />
@@ -66,6 +68,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault"
android:text="@string/bubbles_dont_bubble_conversation" />
@@ -92,6 +95,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
</LinearLayout>
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
new file mode 100644
index 000000000000..fb1980a52601
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
@@ -0,0 +1,92 @@
+<?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.
+ -->
+<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/desktop_mode_caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:background="@drawable/desktop_mode_decor_title">
+
+ <LinearLayout
+ android:id="@+id/open_menu_button"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:paddingStart="8dp">
+
+ <ImageView
+ android:id="@+id/application_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_margin="4dp"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@string/app_icon_text" />
+
+ <TextView
+ android:id="@+id/application_name"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:minWidth="80dp"
+ android:textColor="@color/desktop_mode_caption_app_name_dark"
+ android:textSize="14sp"
+ android:textFontWeight="500"
+ android:gravity="center_vertical"
+ android:layout_weight="1"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ tools:text="Gmail"/>
+
+ <ImageButton
+ android:id="@+id/expand_menu_button"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:padding="4dp"
+ 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"
+ android:focusable="false"
+ android:layout_gravity="center_vertical"/>
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/caption_handle"
+ android:layout_width="wrap_content"
+ android:layout_height="40dp"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/close_window"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:padding="4dp"
+ android:layout_marginEnd="8dp"
+ android:contentDescription="@string/close_button_text"
+ 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"/>
+</com.android.wm.shell.windowdecor.WindowDecorLinearLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
deleted file mode 100644
index f9aeb6a8448a..000000000000
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
+++ /dev/null
@@ -1,136 +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.
- -->
-<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/handle_menu"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@drawable/desktop_mode_decor_menu_background"
- android:divider="?android:attr/dividerHorizontal"
- android:showDividers="middle"
- android:dividerPadding="18dip">
- <RelativeLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
- <ImageView
- android:id="@+id/application_icon"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_margin="12dp"
- android:contentDescription="@string/app_icon_text"
- android:layout_alignParentStart="true"
- android:layout_centerVertical="true"/>
- <TextView
- android:id="@+id/application_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_toEndOf="@+id/application_icon"
- android:layout_toStartOf="@+id/collapse_menu_button"
- android:textColor="#FF000000"
- android:layout_centerVertical="true"/>
- <Button
- android:id="@+id/collapse_menu_button"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_marginEnd="10dp"
- android:contentDescription="@string/collapse_menu_text"
- android:layout_alignParentEnd="true"
- android:background="@drawable/caption_collapse_menu_button"
- android:layout_centerVertical="true"/>
- </RelativeLayout>
- <LinearLayout
- android:id="@+id/windowing_mode_buttons"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal">
- <Space
- android:layout_width="0dp"
- android:layout_height="1dp"
- android:layout_weight="0.5" />
- <ImageButton
- style="@style/CaptionWindowingButtonStyle"
- android:id="@+id/fullscreen_button"
- android:contentDescription="@string/fullscreen_text"
- android:src="@drawable/caption_fullscreen_button"
- android:scaleType="fitCenter"
- android:background="?android:selectableItemBackgroundBorderless"/>
- <Space
- android:layout_width="0dp"
- android:layout_height="1dp"
- android:layout_weight="1" />
- <ImageButton
- style="@style/CaptionWindowingButtonStyle"
- android:id="@+id/split_screen_button"
- android:contentDescription="@string/split_screen_text"
- android:src="@drawable/caption_split_screen_button"
- android:scaleType="fitCenter"
- android:background="?android:selectableItemBackgroundBorderless"/>
- <Space
- android:layout_width="0dp"
- android:layout_height="1dp"
- android:layout_weight="1" />
- <ImageButton
- style="@style/CaptionWindowingButtonStyle"
- android:id="@+id/floating_button"
- android:contentDescription="@string/float_button_text"
- android:src="@drawable/caption_floating_button"
- android:scaleType="fitCenter"
- android:background="?android:selectableItemBackgroundBorderless"/>
- <Space
- android:layout_width="0dp"
- android:layout_height="1dp"
- android:layout_weight="1" />
- <ImageButton
- style="@style/CaptionWindowingButtonStyle"
- android:id="@+id/desktop_button"
- android:contentDescription="@string/desktop_text"
- android:src="@drawable/caption_desktop_button"
- android:scaleType="fitCenter"
- android:background="?android:selectableItemBackgroundBorderless"/>
- <Space
- android:layout_width="0dp"
- android:layout_height="1dp"
- android:layout_weight="0.5" />
-
- </LinearLayout>
- <LinearLayout
- android:id="@+id/menu_buttons_misc"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
- <Button
- style="@style/CaptionMenuButtonStyle"
- android:id="@+id/screenshot_button"
- android:contentDescription="@string/screenshot_text"
- android:text="@string/screenshot_text"
- android:drawableStart="@drawable/caption_screenshot_button"/>
- <Button
- style="@style/CaptionMenuButtonStyle"
- android:id="@+id/select_button"
- android:contentDescription="@string/select_text"
- android:text="@string/select_text"
- android:drawableStart="@drawable/caption_select_button"/>
- <Button
- style="@style/CaptionMenuButtonStyle"
- android:id="@+id/close_button"
- android:contentDescription="@string/close_text"
- android:text="@string/close_text"
- android:drawableStart="@drawable/caption_close_button"
- android:textColor="#FFFF0000"/>
- </LinearLayout>
-</com.android.wm.shell.windowdecor.WindowDecorLinearLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
index 29cf1512e2e5..1d6864c152c2 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
@@ -16,26 +16,22 @@
-->
<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
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">
- <Button
- style="@style/CaptionButtonStyle"
- android:id="@+id/back_button"
- android:contentDescription="@string/back_button_text"
- android:background="@drawable/decor_back_button_dark"/>
- <Button
+
+ <ImageButton
android:id="@+id/caption_handle"
- android:layout_width="128dp"
- android:layout_height="32dp"
- android:layout_margin="5dp"
+ android:layout_width="176dp"
+ android:layout_height="42dp"
+ android:paddingHorizontal="24dp"
android:contentDescription="@string/handle_text"
- android:background="@drawable/decor_handle_dark"/>
- <Button
- style="@style/CaptionButtonStyle"
- android:id="@+id/close_window"
- android:contentDescription="@string/close_button_text"
- android:background="@drawable/decor_close_button_dark"/>
+ android:src="@drawable/decor_handle_dark"
+ 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/desktop_mode_resize_veil.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml
new file mode 100644
index 000000000000..a4bbd8998cc5
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml
@@ -0,0 +1,28 @@
+<?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.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/desktop_mode_resize_veil_background">
+
+ <ImageView
+ android:id="@+id/veil_application_icon"
+ android:layout_width="96dp"
+ android:layout_height="96dp"
+ android:layout_gravity="center"
+ android:contentDescription="@string/app_icon_text" />
+</FrameLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_app_info_pill.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_app_info_pill.xml
new file mode 100644
index 000000000000..167a003932d6
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_app_info_pill.xml
@@ -0,0 +1,57 @@
+<?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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="@dimen/desktop_mode_handle_menu_width"
+ android:layout_height="@dimen/desktop_mode_handle_menu_app_info_pill_height"
+ android:orientation="horizontal"
+ android:background="@drawable/desktop_mode_decor_menu_background"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/application_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginStart="14dp"
+ android:layout_marginEnd="14dp"
+ android:contentDescription="@string/app_icon_text"/>
+
+ <TextView
+ android:id="@+id/application_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ tools:text="Gmail"
+ android:textColor="@color/desktop_mode_caption_menu_text_color"
+ android:textSize="14sp"
+ android:textFontWeight="500"
+ android:lineHeight="20dp"
+ android:textStyle="normal"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/collapse_menu_button"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:padding="4dp"
+ android:layout_marginEnd="14dp"
+ android:layout_marginStart="14dp"
+ android:contentDescription="@string/collapse_menu_text"
+ android:src="@drawable/ic_baseline_expand_more_24"
+ android:rotation="180"
+ android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive"
+ android:background="?android:selectableItemBackgroundBorderless"/>
+</LinearLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_more_actions_pill.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_more_actions_pill.xml
new file mode 100644
index 000000000000..40a4b53f3e1d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_more_actions_pill.xml
@@ -0,0 +1,47 @@
+<?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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/desktop_mode_handle_menu_width"
+ android:layout_height="@dimen/desktop_mode_handle_menu_more_actions_pill_height"
+ android:orientation="vertical"
+ android:background="@drawable/desktop_mode_decor_menu_background">
+
+ <Button
+ android:id="@+id/screenshot_button"
+ android:contentDescription="@string/screenshot_text"
+ android:text="@string/screenshot_text"
+ android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot"
+ android:drawableTint="@color/desktop_mode_caption_menu_buttons_color_inactive"
+ style="@style/DesktopModeHandleMenuActionButton"/>
+
+ <Button
+ android:id="@+id/select_button"
+ android:contentDescription="@string/select_text"
+ android:text="@string/select_text"
+ android:drawableStart="@drawable/desktop_mode_ic_handle_menu_select"
+ android:drawableTint="@color/desktop_mode_caption_menu_buttons_color_inactive"
+ style="@style/DesktopModeHandleMenuActionButton"/>
+
+ <Button
+ android:id="@+id/close_button"
+ android:contentDescription="@string/close_text"
+ android:text="@string/close_text"
+ android:drawableStart="@drawable/desktop_mode_ic_handle_menu_close"
+ android:drawableTint="@color/desktop_mode_caption_menu_buttons_color_inactive"
+ style="@style/DesktopModeHandleMenuActionButton"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_windowing_pill.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_windowing_pill.xml
new file mode 100644
index 000000000000..95283b9e214a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_windowing_pill.xml
@@ -0,0 +1,62 @@
+<?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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/desktop_mode_handle_menu_width"
+ android:layout_height="@dimen/desktop_mode_handle_menu_windowing_pill_height"
+ android:orientation="horizontal"
+ android:background="@drawable/desktop_mode_decor_menu_background"
+ android:gravity="center_vertical">
+
+ <ImageButton
+ android:id="@+id/fullscreen_button"
+ android:layout_marginEnd="4dp"
+ android:contentDescription="@string/fullscreen_text"
+ android:src="@drawable/desktop_mode_ic_handle_menu_fullscreen"
+ android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive"
+ android:layout_weight="1"
+ style="@style/DesktopModeHandleMenuWindowingButton"/>
+
+ <ImageButton
+ android:id="@+id/split_screen_button"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:contentDescription="@string/split_screen_text"
+ android:src="@drawable/desktop_mode_ic_handle_menu_splitscreen"
+ android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive"
+ android:layout_weight="1"
+ style="@style/DesktopModeHandleMenuWindowingButton"/>
+
+ <ImageButton
+ android:id="@+id/floating_button"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:contentDescription="@string/float_button_text"
+ android:src="@drawable/desktop_mode_ic_handle_menu_floating"
+ android:tint="@color/desktop_mode_caption_menu_buttons_color_inactive"
+ android:layout_weight="1"
+ style="@style/DesktopModeHandleMenuWindowingButton"/>
+
+ <ImageButton
+ android:id="@+id/desktop_button"
+ android:layout_marginStart="4dp"
+ android:contentDescription="@string/desktop_text"
+ android:src="@drawable/desktop_mode_ic_handle_menu_desktop"
+ android:tint="@color/desktop_mode_caption_menu_buttons_color_active"
+ android:layout_weight="1"
+ style="@style/DesktopModeHandleMenuWindowingButton"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
index 095576b581df..c77a4fdcfa79 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml
@@ -16,6 +16,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="@dimen/letterbox_education_dialog_action_width"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
@@ -36,7 +37,7 @@
android:layout_height="wrap_content"
android:lineSpacingExtra="4sp"
android:textAlignment="center"
- android:textColor="@color/compat_controls_text"
+ android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"/>
</LinearLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
index a993469aaccf..4d5256777018 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml
@@ -15,6 +15,7 @@
-->
<com.android.wm.shell.compatui.LetterboxEduDialogLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/LetterboxDialog">
@@ -66,7 +67,7 @@
android:lineSpacingExtra="4sp"
android:text="@string/letterbox_education_dialog_title"
android:textAlignment="center"
- android:textColor="@color/compat_controls_text"
+ android:textColor="?android:attr/textColorPrimary"
android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline"
android:textSize="24sp"/>
@@ -108,7 +109,7 @@
android:background=
"@drawable/letterbox_education_dismiss_button_background_ripple"
android:text="@string/letterbox_education_got_it"
- android:textColor="@android:color/system_neutral1_900"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textAlignment="center"
android:contentDescription="@string/letterbox_education_got_it"/>
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
index 5aff4159e135..7f1aac3551b6 100644
--- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
@@ -73,11 +73,13 @@
android:textAlignment="center"/>
<LinearLayout
+ android:id="@+id/letterbox_restart_dialog_checkbox_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:paddingVertical="14dp"
android:orientation="horizontal"
android:layout_gravity="center_vertical"
- android:layout_marginVertical="32dp">
+ android:layout_marginVertical="18dp">
<CheckBox
android:id="@+id/letterbox_restart_dialog_checkbox"
diff --git a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml
index 49491a7b572c..69b339ad77fb 100644
--- a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml
@@ -20,47 +20,48 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:theme="@style/ReachabilityEduHandLayout">
<androidx.appcompat.widget.AppCompatTextView
- style="@style/ReachabilityEduHandLayout"
android:text="@string/letterbox_reachability_reposition_text"
app:drawableTopCompat="@drawable/reachability_education_ic_right_hand"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin"
android:id="@+id/reachability_move_up_button"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ android:visibility="invisible"/>
<androidx.appcompat.widget.AppCompatTextView
- style="@style/ReachabilityEduHandLayout"
android:text="@string/letterbox_reachability_reposition_text"
app:drawableTopCompat="@drawable/reachability_education_ic_right_hand"
android:layout_gravity="center_vertical|right"
android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin"
android:id="@+id/reachability_move_right_button"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ android:visibility="invisible"/>
<androidx.appcompat.widget.AppCompatTextView
- style="@style/ReachabilityEduHandLayout"
android:text="@string/letterbox_reachability_reposition_text"
app:drawableTopCompat="@drawable/reachability_education_ic_left_hand"
android:layout_gravity="center_vertical|left"
android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin"
android:id="@+id/reachability_move_left_button"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ android:visibility="invisible"/>
<androidx.appcompat.widget.AppCompatTextView
- style="@style/ReachabilityEduHandLayout"
android:text="@string/letterbox_reachability_reposition_text"
app:drawableTopCompat="@drawable/reachability_education_ic_right_hand"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin"
android:id="@+id/reachability_move_down_button"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ android:visibility="invisible"/>
</com.android.wm.shell.compatui.ReachabilityEduLayout>
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index 7a3ee23d8cdc..82a358cf68d6 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -16,92 +16,46 @@
-->
<!-- Layout for TvPipMenuView -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/tv_pip_menu"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center|top">
+ android:id="@+id/tv_pip_menu"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center|top">
<!-- Matches the PiP app content -->
- <View
+ <FrameLayout
android:id="@+id/tv_pip"
android:layout_width="0dp"
android:layout_height="0dp"
- android:alpha="0"
- android:background="@color/tv_pip_menu_background"
android:layout_marginTop="@dimen/pip_menu_outer_space"
android:layout_marginStart="@dimen/pip_menu_outer_space"
- android:layout_marginEnd="@dimen/pip_menu_outer_space"/>
+ android:layout_marginEnd="@dimen/pip_menu_outer_space">
- <ScrollView
- android:id="@+id/tv_pip_menu_scroll"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_alignTop="@+id/tv_pip"
- android:layout_alignStart="@+id/tv_pip"
- android:layout_alignEnd="@+id/tv_pip"
- android:layout_alignBottom="@+id/tv_pip"
- android:scrollbars="none"
- android:visibility="gone"/>
+ <View
+ android:id="@+id/tv_pip_menu_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/tv_pip_menu_background"
+ android:alpha="0"/>
- <HorizontalScrollView
- android:id="@+id/tv_pip_menu_horizontal_scroll"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_alignTop="@+id/tv_pip"
- android:layout_alignStart="@+id/tv_pip"
- android:layout_alignEnd="@+id/tv_pip"
- android:layout_alignBottom="@+id/tv_pip"
- android:scrollbars="none">
+ <View
+ android:id="@+id/tv_pip_menu_dim_layer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/tv_pip_menu_dim_layer"
+ android:alpha="0"/>
- <LinearLayout
+ <com.android.internal.widget.RecyclerView
android:id="@+id/tv_pip_menu_action_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:alpha="0">
-
- <Space
- android:layout_width="@dimen/pip_menu_button_wrapper_margin"
- android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
-
- <com.android.wm.shell.pip.tv.TvPipMenuActionButton
- android:id="@+id/tv_pip_menu_fullscreen_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:src="@drawable/pip_ic_fullscreen_white"
- android:text="@string/pip_fullscreen" />
-
- <com.android.wm.shell.pip.tv.TvPipMenuActionButton
- android:id="@+id/tv_pip_menu_close_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:src="@drawable/pip_ic_close_white"
- android:text="@string/pip_close" />
-
- <!-- More TvPipMenuActionButtons may be added here at runtime. -->
-
- <com.android.wm.shell.pip.tv.TvPipMenuActionButton
- android:id="@+id/tv_pip_menu_move_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:src="@drawable/pip_ic_move_white"
- android:text="@string/pip_move" />
-
- <com.android.wm.shell.pip.tv.TvPipMenuActionButton
- android:id="@+id/tv_pip_menu_expand_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:src="@drawable/pip_ic_collapse"
- android:visibility="gone"
- android:text="@string/pip_collapse" />
-
- <Space
- android:layout_width="@dimen/pip_menu_button_wrapper_margin"
- android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
-
- </LinearLayout>
- </HorizontalScrollView>
+ android:layout_gravity="center"
+ android:padding="@dimen/pip_menu_button_start_end_offset"
+ android:clipToPadding="false"
+ android:alpha="0"
+ android:contentDescription="@string/a11y_pip_menu_entered"/>
+ </FrameLayout>
+ <!-- Frame around the content, just overlapping the corners to make them round -->
<View
android:id="@+id/tv_pip_border"
android:layout_width="0dp"
@@ -111,6 +65,7 @@
android:layout_marginEnd="@dimen/pip_menu_outer_space_frame"
android:background="@drawable/tv_pip_menu_border"/>
+ <!-- Temporarily extending the background to show an edu text hint for opening the menu -->
<FrameLayout
android:id="@+id/tv_pip_menu_edu_text_container"
android:layout_width="match_parent"
@@ -120,23 +75,10 @@
android:layout_alignStart="@+id/tv_pip"
android:layout_alignEnd="@+id/tv_pip"
android:background="@color/tv_pip_menu_background"
- android:clipChildren="true">
-
- <TextView
- android:id="@+id/tv_pip_menu_edu_text"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/pip_menu_edu_text_view_height"
- android:layout_gravity="bottom|center"
- android:gravity="center"
- android:paddingBottom="@dimen/pip_menu_border_width"
- android:text="@string/pip_edu_text"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:marqueeRepeatLimit="1"
- android:scrollHorizontally="true"
- android:textAppearance="@style/TvPipEduText"/>
- </FrameLayout>
+ android:paddingBottom="@dimen/pip_menu_border_width"
+ android:paddingTop="@dimen/pip_menu_border_width"/>
+ <!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
<View
android:id="@+id/tv_pip_menu_frame"
android:layout_width="match_parent"
@@ -144,6 +86,17 @@
android:layout_margin="@dimen/pip_menu_outer_space_frame"
android:background="@drawable/tv_pip_menu_border"/>
+ <!-- Move menu -->
+ <com.android.wm.shell.common.TvWindowMenuActionButton
+ android:id="@+id/tv_pip_menu_done_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/pip_ic_close_white"
+ android:visibility="gone"
+ android:text="@string/a11y_action_pip_move_done" />
+
<ImageView
android:id="@+id/tv_pip_menu_arrow_up"
android:layout_width="@dimen/pip_menu_arrow_size"
@@ -151,8 +104,7 @@
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:alpha="0"
- android:elevation="@dimen/pip_menu_arrow_elevation"
- android:src="@drawable/pip_ic_move_up" />
+ android:contentDescription="@string/a11y_action_pip_move_up"/>
<ImageView
android:id="@+id/tv_pip_menu_arrow_right"
@@ -161,8 +113,7 @@
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:alpha="0"
- android:elevation="@dimen/pip_menu_arrow_elevation"
- android:src="@drawable/pip_ic_move_right" />
+ android:contentDescription="@string/a11y_action_pip_move_right"/>
<ImageView
android:id="@+id/tv_pip_menu_arrow_down"
@@ -171,8 +122,7 @@
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:alpha="0"
- android:elevation="@dimen/pip_menu_arrow_elevation"
- android:src="@drawable/pip_ic_move_down" />
+ android:contentDescription="@string/a11y_action_pip_move_down"/>
<ImageView
android:id="@+id/tv_pip_menu_arrow_left"
@@ -181,6 +131,5 @@
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:alpha="0"
- android:elevation="@dimen/pip_menu_arrow_elevation"
- android:src="@drawable/pip_ic_move_left" />
+ android:contentDescription="@string/a11y_action_pip_move_left"/>
</RelativeLayout>
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml
deleted file mode 100644
index db96d8de4094..000000000000
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml
+++ /dev/null
@@ -1,40 +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.
--->
-<!-- Layout for TvPipMenuActionButton -->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/button"
- android:layout_width="@dimen/pip_menu_button_size"
- android:layout_height="@dimen/pip_menu_button_size"
- android:padding="@dimen/pip_menu_button_margin"
- android:stateListAnimator="@animator/tv_pip_menu_action_button_animator"
- android:focusable="true">
-
- <View android:id="@+id/background"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:duplicateParentState="true"
- android:background="@drawable/tv_pip_button_bg"/>
-
- <ImageView android:id="@+id/icon"
- android:layout_width="@dimen/pip_menu_icon_size"
- android:layout_height="@dimen/pip_menu_icon_size"
- android:layout_gravity="center"
- android:duplicateParentState="true"
- android:tint="@color/tv_pip_menu_icon" />
-</FrameLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml
index 5af40200d240..bd48ad2cef44 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml
@@ -19,10 +19,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
+ android:id="@+id/background_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/pip_menu_outer_space_frame"
android:background="@drawable/tv_pip_menu_background"
- android:elevation="@dimen/pip_menu_elevation"/>
+ android:elevation="@dimen/pip_menu_elevation_no_menu"/>
</FrameLayout>
diff --git a/libs/WindowManager/Shell/res/layout/tv_split_menu_view.xml b/libs/WindowManager/Shell/res/layout/tv_split_menu_view.xml
new file mode 100644
index 000000000000..e0fa59c9f157
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tv_split_menu_view.xml
@@ -0,0 +1,125 @@
+<!--
+ ~ 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.
+ -->
+<!-- Layout for TvSplitMenuView -->
+<com.android.wm.shell.splitscreen.tv.TvSplitMenuView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+
+ <LinearLayout
+ android:id="@+id/tv_split_main_menu"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <com.android.wm.shell.common.TvWindowMenuActionButton
+ android:id="@+id/tv_split_main_menu_focus_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/tv_split_menu_ic_focus" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <com.android.wm.shell.common.TvWindowMenuActionButton
+ android:id="@+id/tv_split_main_menu_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/pip_ic_close_white" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <com.android.wm.shell.common.TvWindowMenuActionButton
+ android:id="@+id/tv_split_menu_swap_stages"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/tv_split_menu_ic_swap" />
+
+ <LinearLayout
+ android:id="@+id/tv_split_side_menu"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <com.android.wm.shell.common.TvWindowMenuActionButton
+ android:id="@+id/tv_split_side_menu_focus_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/tv_split_menu_ic_focus" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <com.android.wm.shell.common.TvWindowMenuActionButton
+ android:id="@+id/tv_split_side_menu_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/pip_ic_close_white" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </LinearLayout>
+</com.android.wm.shell.splitscreen.tv.TvSplitMenuView>
diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml
new file mode 100644
index 000000000000..b2ac85b018be
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml
@@ -0,0 +1,41 @@
+<?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.
+ -->
+<!-- Layout for TvWindowMenuActionButton -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/button"
+ android:layout_width="@dimen/tv_window_menu_button_size"
+ android:layout_height="@dimen/tv_window_menu_button_size"
+ android:padding="@dimen/tv_window_menu_button_margin"
+ android:duplicateParentState="true"
+ android:stateListAnimator="@animator/tv_window_menu_action_button_animator"
+ android:focusable="true">
+
+ <View android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:duplicateParentState="true"
+ android:background="@drawable/tv_window_button_bg"/>
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="@dimen/tv_window_menu_icon_size"
+ android:layout_height="@dimen/tv_window_menu_icon_size"
+ android:layout_gravity="center"
+ android:duplicateParentState="true"
+ android:tint="@color/tv_window_menu_icon" />
+</FrameLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml
index 74497bfa2816..de4a225e41a7 100644
--- a/libs/WindowManager/Shell/res/values-af/strings.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Instellings"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Gaan by verdeelde skerm in"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Kieslys"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Prent-in-prent-kieslys"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in beeld-in-beeld"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"As jy nie wil hê dat <xliff:g id="NAME">%s</xliff:g> hierdie kenmerk moet gebruik nie, tik om instellings oop te maak en skakel dit af."</string>
<string name="pip_play" msgid="3496151081459417097">"Speel"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Verander grootte"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Hou vas"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Laat los"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Program sal dalk nie met verdeelde skerm werk nie."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Program steun nie verdeelde skerm nie."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Skermverdeler"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Skermverdeler"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Skermverdeler"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Volskerm links"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Links 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Links 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nie opgelos nie?\nTik om terug te stel"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Geen kamerakwessies nie? Tik om toe te maak."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Sien en doen meer"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Sleep ’n ander program in vir verdeelde skerm"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Sleep ’n ander app in vir verdeelde skerm"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dubbeltik buite ’n program om dit te herposisioneer"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Het dit"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Vou uit vir meer inligting."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Skermskoot"</string>
<string name="close_text" msgid="4986518933445178928">"Maak toe"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Maak kieslys toe"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Maak kieslys oop"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-af/strings_tv.xml b/libs/WindowManager/Shell/res/values-af/strings_tv.xml
index 6187ea46769c..2254fc9beb11 100644
--- a/libs/WindowManager/Shell/res/values-af/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Skuif"</string>
<string name="pip_expand" msgid="1051966011679297308">"Vou uit"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Vou in"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Dubbeldruk "<annotation icon="home_icon">" TUIS "</annotation>" vir kontroles"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Dubbeldruk "<annotation icon="home_icon">"TUIS"</annotation>" vir kontroles"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Prent-in-prent-kieslys"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Skuif links"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Skuif regs"</string>
diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml
index feca5052981a..21172e2267bc 100644
--- a/libs/WindowManager/Shell/res/values-am/strings.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ቅንብሮች"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"የተከፈለ ማያ ገጽን አስገባ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"ምናሌ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"የሥዕል-ላይ-ሥዕል ምናሌ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> በሥዕል-ላይ-ሥዕል ውስጥ ነው"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ይህን ባህሪ እንዲጠቀም ካልፈለጉ ቅንብሮችን ለመክፈት መታ ያድርጉና ያጥፉት።"</string>
<string name="pip_play" msgid="3496151081459417097">"አጫውት"</string>
@@ -31,22 +32,23 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"መጠን ይቀይሩ"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"መተግበሪያ ከተከፈለ ማያ ገፅ ጋር ላይሠራ ይችላል"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"መተግበሪያው የተከፈለ ማያ ገጽን አይደግፍም።"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"መተግበሪያ ከተከፈለ ማያ ገጽ ጋር ላይሠራ ይችላል"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"መተግበሪያው የተከፈለ ማያ ገጽን አይደግፍም"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ይህ መተግበሪያ መከፈት የሚችለው በ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="703810061635792791">"የተከፈለ የማያ ገፅ ከፋይ"</string>
- <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"የግራ ሙሉ ማያ ገፅ"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"የተከፈለ የማያ ገጽ ከፋይ"</string>
+ <string name="divider_title" msgid="1963391955593749442">"የተከፈለ የማያ ገጽ ከፋይ"</string>
+ <string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"የግራ ሙሉ ማያ ገጽ"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ግራ 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ግራ 50%"</string>
<string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ግራ 30%"</string>
- <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"የቀኝ ሙሉ ማያ ገፅ"</string>
- <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"የላይ ሙሉ ማያ ገፅ"</string>
+ <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"የቀኝ ሙሉ ማያ ገጽ"</string>
+ <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"የላይ ሙሉ ማያ ገጽ"</string>
<string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ከላይ 70%"</string>
<string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ከላይ 50%"</string>
<string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"ከላይ 30%"</string>
- <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"የታች ሙሉ ማያ ገፅ"</string>
+ <string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"የታች ሙሉ ማያ ገጽ"</string>
<string name="accessibility_split_left" msgid="1713683765575562458">"ወደ ግራ ከፋፍል"</string>
<string name="accessibility_split_right" msgid="8441001008181296837">"ወደ ቀኝ ከፋፍል"</string>
<string name="accessibility_split_top" msgid="2789329702027147146">"ወደ ላይ ከፋፍል"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"አልተስተካከለም?\nለማህደር መታ ያድርጉ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ምንም የካሜራ ችግሮች የሉም? ለማሰናበት መታ ያድርጉ።"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ተጨማሪ ይመልከቱ እና ያድርጉ"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"ለተከፈለ ማያ ገፅ ሌላ መተግበሪያ ይጎትቱ"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ለተከፈለ ማያ ገጽ ሌላ መተግበሪያ ይጎትቱ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ቦታውን ለመቀየር ከመተግበሪያው ውጪ ሁለቴ መታ ያድርጉ"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ገባኝ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ለተጨማሪ መረጃ ይዘርጉ።"</string>
@@ -100,11 +102,12 @@
<string name="app_icon_text" msgid="2823268023931811747">"የመተግበሪያ አዶ"</string>
<string name="fullscreen_text" msgid="1162316685217676079">"ሙሉ ማያ"</string>
<string name="desktop_text" msgid="1077633567027630454">"የዴስክቶፕ ሁነታ"</string>
- <string name="split_screen_text" msgid="1396336058129570886">"የተከፈለ ማያ ገፅ"</string>
+ <string name="split_screen_text" msgid="1396336058129570886">"የተከፈለ ማያ ገጽ"</string>
<string name="more_button_text" msgid="3655388105592893530">"ተጨማሪ"</string>
<string name="float_button_text" msgid="9221657008391364581">"ተንሳፋፊ"</string>
<string name="select_text" msgid="5139083974039906583">"ምረጥ"</string>
- <string name="screenshot_text" msgid="1477704010087786671">"ቅጽበታዊ ገፅ እይታ"</string>
+ <string name="screenshot_text" msgid="1477704010087786671">"ቅጽበታዊ ገጽ እይታ"</string>
<string name="close_text" msgid="4986518933445178928">"ዝጋ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ምናሌ ዝጋ"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"ምናሌን ክፈት"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-am/strings_tv.xml b/libs/WindowManager/Shell/res/values-am/strings_tv.xml
index d2d287327336..a6be57889a4e 100644
--- a/libs/WindowManager/Shell/res/values-am/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings_tv.xml
@@ -20,11 +20,11 @@
<string name="notification_channel_tv_pip" msgid="2576686079160402435">"ሥዕል-ላይ-ሥዕል"</string>
<string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ርዕስ የሌለው ፕሮግራም)"</string>
<string name="pip_close" msgid="2955969519031223530">"ዝጋ"</string>
- <string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገፅ"</string>
+ <string name="pip_fullscreen" msgid="7278047353591302554">"ሙሉ ማያ ገጽ"</string>
<string name="pip_move" msgid="158770205886688553">"ውሰድ"</string>
<string name="pip_expand" msgid="1051966011679297308">"ዘርጋ"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ሰብስብ"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ለመቆጣጠሪያዎች "<annotation icon="home_icon">"መነሻ"</annotation>"ን ሁለቴ ይጫኑ"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"ለመቆጣጠሪያዎች "<annotation icon="home_icon">"መነሻ"</annotation>"ን ሁለቴ ይጫኑ"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"የሥዕል-ላይ-ሥዕል ምናሌ።"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ወደ ግራ ውሰድ"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ወደ ቀኝ ውሰድ"</string>
diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml
index 095233e28cc6..a714b49d1305 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"الإعدادات"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"الدخول في وضع تقسيم الشاشة"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"القائمة"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"قائمة نافذة ضمن النافذة"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> يظهر في صورة داخل صورة"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"إذا كنت لا تريد أن يستخدم <xliff:g id="NAME">%s</xliff:g> هذه الميزة، فانقر لفتح الإعدادات، ثم أوقِف تفعيل هذه الميزة."</string>
<string name="pip_play" msgid="3496151081459417097">"تشغيل"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"تغيير الحجم"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"إخفاء"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"إظهار"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"قد لا يعمل التطبيق بشكل سليم في وضع \"تقسيم الشاشة\"."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"التطبيق لا يتيح تقسيم الشاشة."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"قد لا يعمل التطبيق بشكل سليم في وضع تقسيم الشاشة."</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"لا يعمل التطبيق في وضع تقسيم الشاشة."</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"لا يمكن فتح هذا التطبيق إلا في نافذة واحدة."</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="703810061635792791">"أداة تقسيم الشاشة"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"أداة تقسيم الشاشة"</string>
+ <string name="divider_title" msgid="1963391955593749442">"أداة تقسيم الشاشة"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"عرض النافذة اليسرى بملء الشاشة"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ضبط حجم النافذة اليسرى ليكون ٧٠%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ضبط حجم النافذة اليسرى ليكون ٥٠%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ألم يتم حل المشكلة؟\nانقر للعودة"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"أليس هناك مشاكل في الكاميرا؟ انقر للإغلاق."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"استخدام تطبيقات متعدّدة في وقت واحد"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"اسحب تطبيقًا آخر لاستخدام وضع تقسيم الشاشة."</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"اسحب تطبيقًا آخر لاستخدام وضع تقسيم الشاشة."</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"انقر مرّتين خارج تطبيق لتغيير موضعه."</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"حسنًا"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"التوسيع للحصول على مزيد من المعلومات"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"لقطة شاشة"</string>
<string name="close_text" msgid="4986518933445178928">"إغلاق"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"إغلاق القائمة"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"فتح القائمة"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml
index 9c195a7386a9..82ab8e9ee15b 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"نقل"</string>
<string name="pip_expand" msgid="1051966011679297308">"توسيع"</string>
<string name="pip_collapse" msgid="3903295106641385962">"تصغير"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" انقر مرتين على "<annotation icon="home_icon">" الصفحة الرئيسية "</annotation>" للوصول لعناصر التحكم."</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"انقر مرتين على "<annotation icon="home_icon">" الرئيسية "</annotation>" للوصول لعناصر التحكم."</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"قائمة نافذة ضمن النافذة"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"نقل لليسار"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"نقل لليمين"</string>
diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml
index 1685c8472fdc..6d86747ef336 100644
--- a/libs/WindowManager/Shell/res/values-as/strings.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ছেটিং"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"বিভাজিত স্ক্ৰীন ম’ডলৈ যাওক"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"চিত্ৰৰ ভিতৰৰ চিত্ৰ মেনু"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> চিত্ৰৰ ভিতৰৰ চিত্ৰত আছে"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"আপুনি যদি <xliff:g id="NAME">%s</xliff:g> সুবিধাটো ব্যৱহাৰ কৰিব নোখোজে, তেন্তে ছেটিং খুলিবলৈ টিপক আৰু তালৈ গৈ ইয়াক অফ কৰক।"</string>
<string name="pip_play" msgid="3496151081459417097">"প্লে কৰক"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"আকাৰ সলনি কৰক"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"লুকুৱাওক"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"দেখুৱাওক"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"এপ্‌টোৱে বিভাজিত স্ক্ৰীনৰ সৈতে কাম নকৰিব পাৰে।"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"এপ্‌টোৱে বিভাজিত স্ক্ৰীন সমৰ্থন নকৰে।"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"এপ্‌টোৱে বিভাজিত স্ক্ৰীনৰ সৈতে কাম নকৰিব পাৰে"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"এপ্‌টোৱে বিভাজিত স্ক্ৰীন সমৰ্থন নকৰে"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"এই এপ্‌টো কেৱল ১ খন ৱিণ্ড’ত খুলিব পাৰি।"</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="703810061635792791">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string>
+ <string name="divider_title" msgid="1963391955593749442">"স্প্লিট স্ক্ৰীনৰ বিভাজক"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাওঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"বাওঁফালৰ স্ক্ৰীনখন ৭০% কৰক"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীনখন ৫০% কৰক"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"এইটো সমাধান কৰা নাই নেকি?\nপূৰ্বাৱস্থালৈ নিবলৈ টিপক"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"কেমেৰাৰ কোনো সমস্যা নাই নেকি? অগ্ৰাহ্য কৰিবলৈ টিপক।"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"চাওক আৰু অধিক কৰক"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"বিভাজিত স্ক্ৰীনৰ বাবে অন্য এটা এপ্‌ টানি আনি এৰক"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"বিভাজিত স্ক্ৰীনৰ বাবে অন্য এটা এপ্‌ টানি আনি এৰক"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"এপ্‌টোৰ স্থান সলনি কৰিবলৈ ইয়াৰ বাহিৰত দুবাৰ টিপক"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"বুজি পালোঁ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"অধিক তথ্যৰ বাবে বিস্তাৰ কৰক।"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"স্ক্ৰীনশ্বট"</string>
<string name="close_text" msgid="4986518933445178928">"বন্ধ কৰক"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"মেনু বন্ধ কৰক"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"মেনু খোলক"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-as/strings_tv.xml b/libs/WindowManager/Shell/res/values-as/strings_tv.xml
index 816b5b1c79dc..34eaaea33609 100644
--- a/libs/WindowManager/Shell/res/values-as/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"স্থানান্তৰ কৰক"</string>
<string name="pip_expand" msgid="1051966011679297308">"বিস্তাৰ কৰক"</string>
<string name="pip_collapse" msgid="3903295106641385962">"সংকোচন কৰক"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" নিয়ন্ত্ৰণৰ বাবে "<annotation icon="home_icon">" গৃহপৃষ্ঠা "</annotation>" বুটামত দুবাৰ হেঁচক"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"নিয়ন্ত্ৰণৰ বাবে "<annotation icon="home_icon">"গৃহপৃষ্ঠা"</annotation>" বুটামত দুবাৰ টিপক"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"চিত্ৰৰ ভিতৰৰ চিত্ৰ মেনু।"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাওঁফাললৈ নিয়ক"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"সোঁফাললৈ নিয়ক"</string>
diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml
index fd725d17a6bd..7c662827cca7 100644
--- a/libs/WindowManager/Shell/res/values-az/strings.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana daxil olun"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Şəkildə Şəkil Menyusu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> şəkil içində şəkildədir"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> tətbiqinin bu funksiyadan istifadə etməyini istəmirsinizsə, ayarları açmaq və deaktiv etmək üçün klikləyin."</string>
<string name="pip_play" msgid="3496151081459417097">"Oxudun"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ölçüsünü dəyişin"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Güvənli məkanda saxlayın"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Güvənli məkandan çıxarın"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Tətbiq bölünmüş ekran ilə işləməyə bilər."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Tətbiq ekran bölünməsini dəstəkləmir."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Bölünmüş ekran ayırıcısı"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Bölünmüş ekran ayırıcısı"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Bölünmüş ekran ayırıcısı"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Sol tam ekran"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Sol 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sol 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Düzəltməmisiniz?\nGeri qaytarmaq üçün toxunun"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kamera problemi yoxdur? Qapatmaq üçün toxunun."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Ardını görün və edin"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Bölünmüş ekrandan istifadə etmək üçün başqa tətbiqi sürüşdürüb gətirin"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Bölünmüş ekran üçün başqa tətbiq sürüşdürün"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tətbiqin yerini dəyişmək üçün kənarına iki dəfə toxunun"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Anladım"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ətraflı məlumat üçün genişləndirin."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Skrinşot"</string>
<string name="close_text" msgid="4986518933445178928">"Bağlayın"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menyunu bağlayın"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Menyunu açın"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-az/strings_tv.xml b/libs/WindowManager/Shell/res/values-az/strings_tv.xml
index ccb7a7069ad8..c45a09645075 100644
--- a/libs/WindowManager/Shell/res/values-az/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Köçürün"</string>
<string name="pip_expand" msgid="1051966011679297308">"Genişləndirin"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Yığcamlaşdırın"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Nizamlayıcılar üçün "<annotation icon="home_icon">" ƏSAS SƏHİFƏ "</annotation>" süçimini iki dəfə basın"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Nizamlayıcılar üçün "<annotation icon="home_icon">"ƏSAS SƏHİFƏ "</annotation>" seçiminə iki dəfə basın"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Şəkildə şəkil menyusu."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola köçürün"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa köçürün"</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 edbd7a3613b0..8de9d11def2b 100644
--- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Podešavanja"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Uđi na podeljeni ekran"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Meni slike u slici."</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je slika u slici"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da biste otvorili podešavanja i isključili je."</string>
<string name="pip_play" msgid="3496151081459417097">"Pusti"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promenite veličinu"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stavite u tajnu memoriju"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Uklonite iz tajne memorije"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi sa podeljenim ekranom."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podeljeni ekran."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Razdelnik podeljenog ekrana"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Razdelnik podeljenog ekrana"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Razdelnik podeljenog ekrana"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Režim celog ekrana za levi ekran"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Levi ekran 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi ekran 50%"</string>
@@ -82,12 +84,12 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problem nije rešen?\nDodirnite da biste vratili"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nemate problema sa kamerom? Dodirnite da biste odbacili."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Vidite i uradite više"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Prevucite drugu aplikaciju da biste koristili podeljeni ekran"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Prevucite drugu aplikaciju da biste koristili podeljeni ekran"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvaput dodirnite izvan aplikacije da biste promenili njenu poziciju"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Važi"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Proširite za još informacija."</string>
<string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Želite li da restartujete radi boljeg prikaza?"</string>
- <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Možete da restartujete aplikaciju da bi izgledala bolje na ekranu, ali možete da izgubite napredak ili nesačuvane promene"</string>
+ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Možete da restartujete aplikaciju da bi izgledala bolje na ekranu, s tim što možete da izgubite ono što ste uradili ili nesačuvane promene, ako ih ima"</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"Otkaži"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"Restartuj"</string>
<string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne prikazuj ponovo"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Snimak ekrana"</string>
<string name="close_text" msgid="4986518933445178928">"Zatvorite"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite meni"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Otvorite meni"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml
index 51a1262b1de7..6dc4ab1cea79 100644
--- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Premesti"</string>
<string name="pip_expand" msgid="1051966011679297308">"Proširi"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Skupi"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" HOME "</annotation>" za kontrole"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Dvaput pritisnite "<annotation icon="home_icon">" HOME "</annotation>" za kontrole"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni Slika u slici."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomerite nalevo"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomerite nadesno"</string>
diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml
index 1bdf16525c67..3d99514e2172 100644
--- a/libs/WindowManager/Shell/res/values-be/strings.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Налады"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Падзяліць экран"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню рэжыму \"Відарыс у відарысе\""</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> з’яўляецца відарысам у відарысе"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Калі вы не хочаце, каб праграма <xliff:g id="NAME">%s</xliff:g> выкарыстоўвала гэту функцыю, дакраніцеся, каб адкрыць налады і адключыць яе."</string>
<string name="pip_play" msgid="3496151081459417097">"Прайграць"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Змяніць памер"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Схаваць"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Паказаць"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Праграма можа не працаваць у рэжыме падзеленага экрана."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Праграма не падтрымлівае функцыю дзялення экрана."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Праграма можа не працаваць у рэжыме падзеленага экрана"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Праграма не падтрымлівае рэжым падзеленага экрана"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Гэту праграму можна адкрыць толькі ў адным акне."</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="703810061635792791">"Раздзяляльнік падзеленага экрана"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Раздзяляльнік падзеленага экрана"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Раздзяляльнік падзеленага экрана"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левы экран – поўнаэкранны рэжым"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левы экран – 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левы экран – 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Не ўдалося выправіць?\nНацісніце, каб аднавіць"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ніякіх праблем з камерай? Націсніце, каб адхіліць."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Адначасова выконвайце розныя задачы"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Перацягніце іншую праграму, каб выкарыстоўваць падзелены экран"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Перацягніце іншую праграму, каб выкарыстоўваць падзелены экран"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Двойчы націсніце экран па-за праграмай, каб перамясціць яе"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Зразумела"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Разгарнуць для дадатковай інфармацыі"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Здымак экрана"</string>
<string name="close_text" msgid="4986518933445178928">"Закрыць"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Закрыць меню"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Адкрыць меню"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-be/strings_tv.xml b/libs/WindowManager/Shell/res/values-be/strings_tv.xml
index 15a353c649d6..20e725f1aa09 100644
--- a/libs/WindowManager/Shell/res/values-be/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Перамясціць"</string>
<string name="pip_expand" msgid="1051966011679297308">"Разгарнуць"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Згарнуць"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Двойчы націсніце "<annotation icon="home_icon">" ГАЛОЎНЫ ЭКРАН "</annotation>" для пераходу ў налады"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Двойчы націсніце "<annotation icon="home_icon">"ГАЛОЎНЫ"</annotation>" для пераходу ў налады"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню рэжыму \"Відарыс у відарысе\"."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перамясціць улева"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перамясціць управа"</string>
diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml
index d78435080aaf..0473f270239a 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Преминаване към разделен екран"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню за режима „Картина в картината“"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е в режима „Картина в картината“"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ако не искате <xliff:g id="NAME">%s</xliff:g> да използва тази функция, докоснете, за да отворите настройките, и я изключете."</string>
<string name="pip_play" msgid="3496151081459417097">"Пускане"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Преоразмеряване"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Съхраняване"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Отмяна на съхраняването"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Приложението може да не работи в режим на разделен екран."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложението не поддържа разделен екран."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Приложението може да не работи в режим на разделен екран"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Приложението не поддържа разделен екран"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Това приложение може да се отвори само в 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="703810061635792791">"Разделител в режима за разделен екран"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Разделител в режима за разделен екран"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Разделител в режима за разделен екран"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ляв екран: Показване на цял екран"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ляв екран: 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ляв екран: 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Проблемът не се отстрани?\nДокоснете за връщане в предишното състояние"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Нямате проблеми с камерата? Докоснете, за да отхвърлите."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Преглеждайте и правете повече неща"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Преместете друго приложение с плъзгане, за да преминете в режим за разделен екран"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Преместете друго приложение с плъзгане, за да преминете в режим за разделен екран"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Докоснете два пъти извън дадено приложение, за да промените позицията му"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Разбрах"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Разгъване за още информация."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Екранна снимка"</string>
<string name="close_text" msgid="4986518933445178928">"Затваряне"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Затваряне на менюто"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Отваряне на менюто"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml
index 2b27a6927077..e9906f952455 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Преместване"</string>
<string name="pip_expand" msgid="1051966011679297308">"Разгъване"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Свиване"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" За достъп до контролите натиснете 2 пъти "<annotation icon="home_icon">"НАЧАЛО"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"За достъп до контролите натиснете два пъти "<annotation icon="home_icon">"НАЧАЛО"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню за функцията „Картина в картината“."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Преместване наляво"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Преместване надясно"</string>
diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml
index b587511d5383..4fe1be0c455e 100644
--- a/libs/WindowManager/Shell/res/values-bn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bn/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"সেটিংস"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"\'স্প্লিট স্ক্রিন\' মোড চালু করুন"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ছবির-মধ্যে-ছবি মেনু"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"ছবির-মধ্যে-ছবি তে <xliff:g id="NAME">%s</xliff:g> আছেন"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> কে এই বৈশিষ্ট্যটি ব্যবহার করতে দিতে না চাইলে ট্যাপ করে সেটিংসে গিয়ে সেটি বন্ধ করে দিন।"</string>
<string name="pip_play" msgid="3496151081459417097">"চালান"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"রিসাইজ করুন"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"স্ট্যাস করুন"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"আনস্ট্যাস করুন"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"অ্যাপটি স্প্লিট স্ক্রিনে কাজ নাও করতে পারে।"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"অ্যাপ্লিকেশান বিভক্ত-স্ক্রিন সমর্থন করে না৷"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"স্প্লিট স্ক্রিনে এই অ্যাপ নাও কাজ করতে পারে"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"স্প্লিট স্ক্রিনে এই অ্যাপ কাজ করে না"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"এই অ্যাপটি শুধু ১টি উইন্ডোয় খোলা যেতে পারে।"</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="703810061635792791">"বিভক্ত-স্ক্রিন বিভাজক"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"স্প্লিট স্ক্রিন বিভাজক"</string>
+ <string name="divider_title" msgid="1963391955593749442">"স্প্লিট স্ক্রিন বিভাজক"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"বাঁ দিকের অংশ নিয়ে পূর্ণ স্ক্রিন"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"৭০% বাকি আছে"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"৫০% বাকি আছে"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"এখনও সমাধান হয়নি?\nরিভার্ট করার জন্য ট্যাপ করুন"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ক্যামেরা সংক্রান্ত সমস্যা নেই? বাতিল করতে ট্যাপ করুন।"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"দেখুন ও আরও অনেক কিছু করুন"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"স্প্লিট স্ক্রিনের জন্য অন্য অ্যাপে টেনে আনুন"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"স্প্লিট স্ক্রিনের ক্ষেত্রে অন্য কোনও অ্যাপ টেনে আনুন"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"কোনও অ্যাপের স্থান পরিবর্তন করতে তার বাইরে ডবল ট্যাপ করুন"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"বুঝেছি"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"আরও তথ্যের জন্য বড় করুন।"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"স্ক্রিনশট"</string>
<string name="close_text" msgid="4986518933445178928">"বন্ধ করুন"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"\'মেনু\' বন্ধ করুন"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"মেনু খুলুন"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml
index 23c8ffabeede..b515154ec3e0 100644
--- a/libs/WindowManager/Shell/res/values-bn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-bn/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"সরান"</string>
<string name="pip_expand" msgid="1051966011679297308">"বড় করুন"</string>
<string name="pip_collapse" msgid="3903295106641385962">"আড়াল করুন"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" কন্ট্রোলের জন্য "<annotation icon="home_icon">" হোম "</annotation>" বোতামে ডবল প্রেস করুন"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"কন্ট্রোলের জন্য "<annotation icon="home_icon">" হোম "</annotation>" বোতামে ডবল প্রেস করুন"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ছবির-মধ্যে-ছবি মেনু।"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"বাঁদিকে সরান"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ডানদিকে সরান"</string>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml
index cd1011338975..b39b497f5c66 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvori podijeljeni ekran"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Meni načina rada slike u slici"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je u načinu priakza Slika u slici"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da otvorite postavke i isključite je."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproduciraj"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stavljanje u stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vađenje iz stasha"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće raditi na podijeljenom ekranu."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava dijeljenje ekrana."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Razdjelnik podijeljenog ekrana"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Razdjelnik podijeljenog ekrana"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Razdjelnik podijeljenog ekrana"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevo cijeli ekran"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevo 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevo 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nije popravljeno?\nDodirnite da vratite"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nema problema s kamerom? Dodirnite da odbacite."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Pogledajte i učinite više"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Prevucite još jednu aplikaciju za podijeljeni ekran"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Prevucite još jednu aplikaciju za podijeljeni ekran"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvaput dodirnite izvan aplikacije da promijenite njen položaj"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Razumijem"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Proširite za više informacija."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Snimak ekrana"</string>
<string name="close_text" msgid="4986518933445178928">"Zatvaranje"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zatvaranje menija"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje menija"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml
index 443fd620fd65..99e076b31180 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Premjesti"</string>
<string name="pip_expand" msgid="1051966011679297308">"Proširi"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Suzi"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">" POČETNI EKRAN "</annotation>" za kontrole"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Dvaput pritisnite "<annotation icon="home_icon">"POČETNI EKRAN"</annotation>" za kontrole"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za način rada slika u slici."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomjeranje ulijevo"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomjeranje udesno"</string>
diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml
index 43781fd4ba9c..fe76e73135c9 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Configuració"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Entra al mode de pantalla dividida"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú d\'imatge sobre imatge"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> està en mode d\'imatge sobre imatge"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Si no vols que <xliff:g id="NAME">%s</xliff:g> utilitzi aquesta funció, toca per obrir la configuració i desactiva-la."</string>
<string name="pip_play" msgid="3496151081459417097">"Reprodueix"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Canvia la mida"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Amaga"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Deixa d\'amagar"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"És possible que l\'aplicació no funcioni amb la pantalla dividida."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'aplicació no admet la pantalla dividida."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Divisor de pantalles"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Separador de pantalla dividida"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Separador de pantalla dividida"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla esquerra completa"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Pantalla esquerra al 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pantalla esquerra al 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"El problema no s\'ha resolt?\nToca per desfer els canvis"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No tens cap problema amb la càmera? Toca per ignorar."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Consulta i fes més coses"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Arrossega una altra aplicació per utilitzar la pantalla dividida"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arrossega una altra aplicació per utilitzar la pantalla dividida"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Fes doble toc fora d\'una aplicació per canviar-ne la posició"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Entesos"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Desplega per obtenir més informació."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string>
<string name="close_text" msgid="4986518933445178928">"Tanca"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Tanca el menú"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Obre el menú"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml
index daa8c1d1d812..d51a78b8e92d 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mou"</string>
<string name="pip_expand" msgid="1051966011679297308">"Desplega"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Replega"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Prem dos cops "<annotation icon="home_icon">" INICI "</annotation>" per accedir als controls"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Prem dos cops "<annotation icon="home_icon">"INICI"</annotation>" per accedir als controls"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú d\'imatge sobre imatge."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mou cap a l\'esquerra"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mou cap a la dreta"</string>
diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml
index 7d9ffc4f646f..70e29705806f 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Nastavení"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivovat rozdělenou obrazovku"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Nabídka"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Nabídka režimu obrazu v obraze"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"Aplikace <xliff:g id="NAME">%s</xliff:g> je v režimu obraz v obraze"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Pokud nechcete, aby aplikace <xliff:g id="NAME">%s</xliff:g> tuto funkci používala, klepnutím otevřete nastavení a funkci vypněte."</string>
<string name="pip_play" msgid="3496151081459417097">"Přehrát"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Změnit velikost"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Uložit"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušit uložení"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikace v režimu rozdělené obrazovky nemusí fungovat."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikace nepodporuje režim rozdělené obrazovky."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Tuto aplikaci lze otevřít jen na 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="703810061635792791">"Čára rozdělující obrazovku"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Čára rozdělující obrazovku"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Čára rozdělující obrazovku"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Levá část na celou obrazovku"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % vlevo"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % vlevo"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovat"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina byla zavřena."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Pokud je problém se zobrazením aplikace, klepněte na ni a restartujte ji."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Klepnutím tuto aplikaci kvůli lepšímu zobrazení restartujete."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problémy s fotoaparátem?\nKlepnutím vyřešíte"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nepomohlo to?\nKlepnutím se vrátíte"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Žádné problémy s fotoaparátem? Klepnutím zavřete."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Lepší zobrazení a více možností"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Přetáhnutím druhé aplikace použijete rozdělenou obrazovku"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Přetáhnutím druhé aplikace použijete rozdělenou obrazovku"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvojitým klepnutím mimo aplikaci změníte její umístění"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Rozbalením zobrazíte další informace."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Snímek obrazovky"</string>
<string name="close_text" msgid="4986518933445178928">"Zavřít"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zavřít nabídku"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Otevřít nabídku"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml
index 3ed85dce0433..72e1ae907cfb 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Přesunout"</string>
<string name="pip_expand" msgid="1051966011679297308">"Rozbalit"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Sbalit"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"tlačítka plochy"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Ovládací prvky zobrazíte dvojitým stisknutím "<annotation icon="home_icon">"HOME"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Nabídka režimu obrazu v obraze"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Přesunout doleva"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Přesunout doprava"</string>
diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml
index e3ba7875a045..c91cd7a956aa 100644
--- a/libs/WindowManager/Shell/res/values-da/strings.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Indstillinger"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Åbn opdelt skærm"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu for integreret billede"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> vises som integreret billede"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke ønsker, at <xliff:g id="NAME">%s</xliff:g> skal benytte denne funktion, kan du åbne indstillingerne og deaktivere den."</string>
<string name="pip_play" msgid="3496151081459417097">"Afspil"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Rediger størrelse"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Skjul"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Vis"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen fungerer muligvis ikke i opdelt skærm."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen understøtter ikke opdelt skærm."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Adskiller til opdelt skærm"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Adskiller til opdelt skærm"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Adskiller til opdelt skærm"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vis venstre del i fuld skærm"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Venstre 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Venstre 50 %"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Løste det ikke problemet?\nTryk for at fortryde"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Har du ingen problemer med dit kamera? Tryk for at afvise."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Se og gør mere"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Træk en anden app hertil for at bruge opdelt skærm"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Træk en anden app hertil for at bruge opdelt skærm"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tryk to gange uden for en app for at justere dens placering"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Udvid for at få flere oplysninger."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Luk"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Luk menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Åbn menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-da/strings_tv.xml b/libs/WindowManager/Shell/res/values-da/strings_tv.xml
index 09024428a825..5881b0674ad2 100644
--- a/libs/WindowManager/Shell/res/values-da/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Flyt"</string>
<string name="pip_expand" msgid="1051966011679297308">"Udvid"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Tryk to gange på "<annotation icon="home_icon">" HJEM "</annotation>" for at se indstillinger"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Tryk to gange på "<annotation icon="home_icon">"HJEM"</annotation>" for at se indstillinger"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu for integreret billede."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flyt til venstre"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flyt til højre"</string>
diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml
index e196b22bb800..6ce475aa3c84 100644
--- a/libs/WindowManager/Shell/res/values-de/strings.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings.xml
@@ -20,8 +20,9 @@
<string name="pip_phone_close" msgid="5783752637260411309">"Schließen"</string>
<string name="pip_phone_expand" msgid="2579292903468287504">"Maximieren"</string>
<string name="pip_phone_settings" msgid="5468987116750491918">"Einstellungen"</string>
- <string name="pip_phone_enter_split" msgid="7042877263880641911">"Splitscreen aktivieren"</string>
+ <string name="pip_phone_enter_split" msgid="7042877263880641911">"„Geteilter Bildschirm“ aktivieren"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menü „Bild im Bild“"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ist in Bild im Bild"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Wenn du nicht möchtest, dass <xliff:g id="NAME">%s</xliff:g> diese Funktion verwendet, tippe, um die Einstellungen zu öffnen und die Funktion zu deaktivieren."</string>
<string name="pip_play" msgid="3496151081459417097">"Wiedergeben"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Größe anpassen"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"In Stash legen"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Aus Stash entfernen"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Die App funktioniert unter Umständen im Splitscreen-Modus nicht."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Splitscreen-Modus wird in dieser App nicht unterstützt."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Die App funktioniert bei geteiltem Bildschirm unter Umständen nicht"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"„Geteilter Bildschirm“ wird in dieser App nicht unterstützt"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Bildschirmteiler"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Bildschirmteiler"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Bildschirmteiler"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vollbild links"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % links"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % links"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Das Problem ist nicht behoben?\nZum Rückgängigmachen tippen."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Keine Probleme mit der Kamera? Zum Schließen tippen."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Mehr sehen und erledigen"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Für Splitscreen-Modus weitere App hineinziehen"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Weitere App hineinziehen, um den Bildschirm zu teilen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Außerhalb einer App doppeltippen, um die Position zu ändern"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Ok"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Für weitere Informationen maximieren."</string>
@@ -100,11 +102,13 @@
<string name="app_icon_text" msgid="2823268023931811747">"App-Symbol"</string>
<string name="fullscreen_text" msgid="1162316685217676079">"Vollbild"</string>
<string name="desktop_text" msgid="1077633567027630454">"Desktopmodus"</string>
- <string name="split_screen_text" msgid="1396336058129570886">"Splitscreen"</string>
+ <string name="split_screen_text" msgid="1396336058129570886">"Geteilter Bildschirm"</string>
<string name="more_button_text" msgid="3655388105592893530">"Mehr"</string>
<string name="float_button_text" msgid="9221657008391364581">"Frei schwebend"</string>
<string name="select_text" msgid="5139083974039906583">"Auswählen"</string>
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Schließen"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menü schließen"</string>
+ <!-- no translation found for expand_menu_text (3847736164494181168) -->
+ <skip />
</resources>
diff --git a/libs/WindowManager/Shell/res/values-de/strings_tv.xml b/libs/WindowManager/Shell/res/values-de/strings_tv.xml
index 18535c9d9338..5c5cbda296a1 100644
--- a/libs/WindowManager/Shell/res/values-de/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Bewegen"</string>
<string name="pip_expand" msgid="1051966011679297308">"Maximieren"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Minimieren"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Für Steuerelemente zweimal "<annotation icon="home_icon">"STARTBILDSCHIRMTASTE"</annotation>" drücken"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Für Steuerelemente 2× "<annotation icon="home_icon">"STARTBILDSCHIRMTASTE"</annotation>" drücken"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menü „Bild im Bild“."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Nach links bewegen"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Nach rechts bewegen"</string>
diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml
index ef14084ab56c..ab5c44e9e07f 100644
--- a/libs/WindowManager/Shell/res/values-el/strings.xml
+++ b/libs/WindowManager/Shell/res/values-el/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Ρυθμίσεις"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Μετάβαση σε διαχωρισμό οθόνης"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Μενού"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Μενού λειτουργίας Picture-in-Picture"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"Η λειτουργία picture-in-picture είναι ενεργή σε <xliff:g id="NAME">%s</xliff:g>."</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Εάν δεν θέλετε να χρησιμοποιείται αυτή η λειτουργία από την εφαρμογή <xliff:g id="NAME">%s</xliff:g>, πατήστε για να ανοίξετε τις ρυθμίσεις και απενεργοποιήστε την."</string>
<string name="pip_play" msgid="3496151081459417097">"Αναπαραγωγή"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Αλλαγή μεγέθους"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Απόκρυψη"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Κατάργηση απόκρυψης"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Η εφαρμογή ενδέχεται να μην λειτουργεί με διαχωρισμό οθόνης."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Η εφαρμογή δεν υποστηρίζει διαχωρισμό οθόνης."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Η εφαρμογή ενδέχεται να μην λειτουργεί με διαχωρισμό οθόνης."</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Η εφαρμογή δεν υποστηρίζει διαχωρισμό οθόνης."</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Αυτή η εφαρμογή μπορεί να ανοιχθεί μόνο σε 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="703810061635792791">"Διαχωριστικό οθόνης"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Διαχωριστικό οθόνης"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Διαχωριστικό οθόνης"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Αριστερή πλήρης οθόνη"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Αριστερή 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Αριστερή 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Δεν διορθώθηκε;\nΠατήστε για επαναφορά."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Δεν αντιμετωπίζετε προβλήματα με την κάμερα; Πατήστε για παράβλεψη."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Δείτε και κάντε περισσότερα"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Σύρετε σε μια άλλη εφαρμογή για διαχωρισμό οθόνης"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Σύρετε σε μια άλλη εφαρμογή για διαχωρισμό οθόνης."</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Πατήστε δύο φορές έξω από μια εφαρμογή για να αλλάξετε τη θέση της"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Το κατάλαβα"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ανάπτυξη για περισσότερες πληροφορίες."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Στιγμιότυπο οθόνης"</string>
<string name="close_text" msgid="4986518933445178928">"Κλείσιμο"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Κλείσιμο μενού"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Άνοιγμα μενού"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-el/strings_tv.xml b/libs/WindowManager/Shell/res/values-el/strings_tv.xml
index 5f8a004b0a1f..a80e2c72de7e 100644
--- a/libs/WindowManager/Shell/res/values-el/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-el/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Μετακίνηση"</string>
<string name="pip_expand" msgid="1051966011679297308">"Ανάπτυξη"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Σύμπτυξη"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Πατήστε δύο φορές "<annotation icon="home_icon">" ΑΡΧΙΚΗ ΟΘΟΝΗ "</annotation>" για στοιχεία ελέγχου"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Πατ. δύο φορές το κουμπί "<annotation icon="home_icon">"αρχ. οθ."</annotation>" για στ. ελέγχου"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Μενού λειτουργίας Picture-in-Picture."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Μετακίνηση αριστερά"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Μετακίνηση δεξιά"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
index 7367a7af35e9..ea91014298df 100644
--- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-picture menu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string>
<string name="pip_play" msgid="3496151081459417097">"Play"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Split screen divider"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Split screen divider"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Drag in another app for split screen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml
index 839789b22a1c..71d02271090d 100644
--- a/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rAU/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Move"</string>
<string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Double-press "<annotation icon="home_icon">"HOME"</annotation>" for controls"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
index 4c311936b0ed..01bdf95da918 100644
--- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-Picture Menu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string>
<string name="pip_play" msgid="3496151081459417097">"Play"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Split-screen divider"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Split screen divider"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Drag in another app for split screen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml
index 5a811e193cb4..09def6b69f06 100644
--- a/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rCA/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Move"</string>
<string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Double press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Double press "<annotation icon="home_icon">"HOME"</annotation>" for controls"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-Picture menu."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
index 7367a7af35e9..ea91014298df 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-picture menu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string>
<string name="pip_play" msgid="3496151081459417097">"Play"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Split screen divider"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Split screen divider"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Drag in another app for split screen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml
index 839789b22a1c..71d02271090d 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Move"</string>
<string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Double-press "<annotation icon="home_icon">"HOME"</annotation>" for controls"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
index 7367a7af35e9..ea91014298df 100644
--- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Picture-in-picture menu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string>
<string name="pip_play" msgid="3496151081459417097">"Play"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Resize"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"App may not work with split-screen."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App does not support split-screen."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Split screen divider"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Split screen divider"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Split screen divider"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Left full screen"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Left 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Drag in another app for split-screen"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Drag in another app for split screen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Got it"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expand for more information."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml
index 839789b22a1c..71d02271090d 100644
--- a/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rIN/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Move"</string>
<string name="pip_expand" msgid="1051966011679297308">"Expand"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Collapse"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Double-press "<annotation icon="home_icon">" HOME "</annotation>" for controls"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Double-press "<annotation icon="home_icon">"HOME"</annotation>" for controls"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Picture-in-picture menu"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Move left"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Move right"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
index eb61cf97d6b1..f6dac79d0379 100644
--- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‎‏‎‏‏‎‏‏‏‏‏‎‏‏‎‎‏‏‏‎‏‎‎‎‎‏‏‏‎‎‎‏‏‎‎‏‏‏‎‏‎‎‎‏‎‎‎‎‏‏‏‎‎Settings‎‏‎‎‏‎"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‏‏‎‏‎‏‎‏‎‎‏‏‏‎‎‎‏‏‏‎‏‎‎‎‎‏‏‎‎‏‎‎‎‏‏‏‏‏‎‏‏‏‎‏‎‏‏‏‎‏‏‏‎Enter split screen‎‏‎‎‏‎"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‏‏‏‏‏‏‏‎‎‎‎‏‏‎‎‏‎‎‏‏‎‎‏‎‎‎‎‏‎‏‎‎‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎‎‎‎Menu‎‏‎‎‏‎"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‎‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‎‎‎‏‎‎‎‏‏‏‎‎‏‎‏‏‏‏‎‎‏‎Picture-in-Picture Menu‎‏‎‎‏‎"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‏‎‏‏‎‎‎‏‏‏‏‎‎‎‎‎‏‎‎‏‏‏‎‎‏‏‎‎‏‏‏‎‎‏‎‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‎‎‏‏‎<xliff:g id="NAME">%s</xliff:g>‎‏‎‎‏‏‏‎ is in picture-in-picture‎‏‎‎‏‎"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‏‏‎‏‏‎‎‎‏‎‏‏‏‏‎‎‎‎‏‏‏‏‎‎‏‎‏‏‎‎‎‏‏‏‎‎‎If you don\'t want ‎‏‎‎‏‏‎<xliff:g id="NAME">%s</xliff:g>‎‏‎‎‏‏‏‎ to use this feature, tap to open settings and turn it off.‎‏‎‎‏‎"</string>
<string name="pip_play" msgid="3496151081459417097">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‎‏‎‎‎‎‏‎‎‏‏‎‏‎‎‏‎‏‏‏‎‎‎‏‎‏‎‎‏‏‎‏‏‎‏‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‎‎‎‏‎‎‏‎Play‎‏‎‎‏‎"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‎‏‎‏‎‏‏‏‎‏‎‎‎‎‎‏‎‏‎‏‏‎‎‎‏‏‎‎‏‏‏‎‎‎‎‏‎‏‏‎‎‎‎‏‏‏‏‎‎‎‎‏‏‏‎‎Resize‎‏‎‎‏‎"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‎‏‏‎‏‎‏‏‎‎‎‏‎‏‎‏‏‎‎‎‎‏‏‎‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‎‎‎‏‎‏‏‎‏‏‏‎‎‎‏‎Stash‎‏‎‎‏‎"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‎‎‎‎‏‏‏‏‎‎‎‏‏‎‎‎‏‎‏‎‏‏‎‏‏‏‎‎‏‏‏‏‏‎‎‎‏‏‎‏‏‏‎‎‎‎‎‎‎‏‏‏‎‎Unstash‎‏‎‎‏‎"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‏‎‎‏‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‎‎‎‎‏‎‎‎‏‎‎‏‏‎‏‏‏‎‏‏‎‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎App may not work with split-screen.‎‏‎‎‏‎"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‎‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‏‎‏‎‎‏‏‏‎‎‏‎‏‎‏‏‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‏‏‎‏‎App does not support split-screen.‎‏‎‎‏‎"</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‎‎‎‎‏‎‎‏‏‎‏‎‎‏‎‎‎‏‏‎‎‎‏‎‎‏‏‏‏‎‎‎‏‎‏‎‎‏‏‏‎‎‎‎‎‏‏‎‏‏‎‎‎‏‎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="703810061635792791">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‎‎‎‏‎‏‏‎‏‎‏‎‎‏‎‎‏‎‎‏‎‏‏‎‏‎‏‏‏‏‏‎‎‏‎‏‏‏‎Split-screen divider‎‏‎‎‏‎"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‎‏‏‏‎‏‎‎‏‎‏‏‏‏‏‏‏‎‎‎‎‎‎‏‏‎‎‎‏‎Split screen divider‎‏‎‎‏‎"</string>
+ <string name="divider_title" msgid="1963391955593749442">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‏‏‏‎‏‎‎‏‎‏‎‎‏‏‎‎‎‏‎‎‎‎‎‎‎‏‏‎‎‏‏‎‎‎‏‏‏‏‎‎‎‎‏‎‎Split screen divider‎‏‎‎‏‎"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‏‏‏‏‎‎‎‏‏‎‎‎‏‏‏‎‏‎‎‎‏‎‏‎‎‏‎‎‏‎‎‎‎‏‏‎‎‏‏‎‎‎‎Left full screen‎‏‎‎‏‎"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‏‎‏‎‎‎‏‏‏‏‏‏‎‎‎‏‎‎‎‎‎‏‎‎‏‎‏‎‏‏‎‏‏‏‎‏‎‎‎‎‎‏‎‏‏‎‎‏‏‎‎‏‎‎Left 70%‎‏‎‎‏‎"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‎‎‏‏‎‏‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‎‎‎‎‏‏‎‎‎‏‎‏‏‎‎‏‎‏‏‎‏‏‎‏‎‏‎‎‏‏‎‎‏‎Left 50%‎‏‎‎‏‎"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‏‏‎‏‏‎‏‏‏‎‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‎‏‏‎‏‏‏‎‎‎‎‎‏‎‎‎‎‎‏‏‏‏‎‎‎‎‏‏‎‎‏‎Didn’t fix it?‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎Tap to revert‎‏‎‎‏‎"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‎‎‏‎‏‏‎‎‏‎‎‎‎‏‎‎‏‎‎‏‎‎‎‏‎‎‏‏‎‎‏‏‎‎‎‎‎‏‏‎‎‎‏‏‏‏‎‎‏‎‎‏‏‏‎No camera issues? Tap to dismiss.‎‏‎‎‏‎"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‏‎‎‏‏‎‏‎‎‎‎‏‏‏‎‏‎‎‎‏‎‎‎‏‏‏‏‎‎‏‏‎‏‎‎‎‎‏‎‎‎‏‏‏‎‏‏‏‏‏‏‎‎See and do more‎‏‎‎‏‎"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‎‎‎‎‏‎‏‎‏‏‎‎‎‏‏‏‎‎‎‎‎‏‏‎‎‏‏‏‏‎‏‏‎‎‏‎‎‏‏‏‎‏‏‎‏‎‏‏‎‏‏‏‎‎Drag in another app for split-screen‎‏‎‎‏‎"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‎‎‎‏‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‎‏‎‏‎‏‏‎‏‏‎‏‎‎‏‏‏‎‎‎‎‏‏‎Drag in another app for split screen‎‏‎‎‏‎"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‎‏‏‎‏‏‏‎‏‏‎‎‎‎‏‏‏‎‏‎‏‏‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‎‎‏‏‎‏‎‏‎Double-tap outside an app to reposition it‎‏‎‎‏‎"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‎‎‏‏‏‏‏‎‎‏‏‏‎‏‎‎‏‎‎‏‎‎‏‏‎‏‏‎‎‎‏‏‎‏‎‎‎‎‏‏‎‎‏‎‎‎‎‏‏‎‏‎‎‏‎Got it‎‏‎‎‏‎"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‎‎‎‎‎‎‏‎‏‏‏‏‎‎‎‏‏‎‏‎‎‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‎‎‏‏‎‎‎‏‎‎‎‏‏‎‏‎‏‎‎Expand for more information.‎‏‎‎‏‎"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‎‎‏‏‏‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‏‎‎‏‏‏‏‏‎‏‎‎‎‏‎‏‎‏‏‏‏‎Screenshot‎‏‎‎‏‎"</string>
<string name="close_text" msgid="4986518933445178928">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎‎‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‏‎‎‏‏‎‏‏‏‎‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‏‎‎‎‏‏‎‎‎‎‎Close‎‏‎‎‏‎"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‏‎‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‏‎‏‏‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‏‎‏‎Close Menu‎‏‎‎‏‎"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‏‎‎‏‎‏‏‏‏‎‎‏‏‏‏‎‏‎‎‏‏‏‏‏‎‎‏‎‏‎‎‎‎‏‏‎‎‏‏‎‎‎‎‎‏‏‎‎‏‏‎‎‎‎‎Open Menu‎‏‎‎‏‎"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml
index 507e066e3812..405770166274 100644
--- a/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-en-rXC/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‎‎‏‏‎‏‎‎‎‎‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‎‏‏‏‎‏‎‏‏‎‎‏‎‏‎‏‎‏‎‏‏‎‏‎‎‏‎‏‎‎‏‎Move‎‏‎‎‏‎"</string>
<string name="pip_expand" msgid="1051966011679297308">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‎‏‎‏‎‏‎‏‏‎‎‎‎‎‏‏‏‎‎‏‏‎‎‏‏‏‏‎‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‏‎‎‎Expand‎‏‎‎‏‎"</string>
<string name="pip_collapse" msgid="3903295106641385962">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‎‎‏‎‏‎‏‏‎‏‎‎‏‎‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‎‎‏‏‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‏‎‏‎‏‎‎Collapse‎‏‎‎‏‎"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‎‎‏‎‏‎‎‏‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‎‎‎‎‎‎‏‎‏‏‎‏‏‏‎‏‎ Double press ‎‏‎‎‏‏‎"<annotation icon="home_icon">"‎‏‎‎‏‏‏‎ HOME ‎‏‎‎‏‏‎"</annotation>"‎‏‎‎‏‏‏‎ for controls‎‏‎‎‏‎"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‏‎‏‏‏‏‎‏‏‎‎‎‏‎‏‏‎‏‏‎‏‎‏‏‏‎‏‏‏‏‎‏‎‏‏‏‏‏‎‏‎‏‎‎‎‎‏‏‏‎‎Double press ‎‏‎‎‏‏‎"<annotation icon="home_icon">"‎‏‎‎‏‏‏‎HOME‎‏‎‎‏‏‎"</annotation>"‎‏‎‎‏‏‏‎ for controls‎‏‎‎‏‎"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‎‏‏‏‎‏‎‏‏‎‎‎‎‎‎‎‎‏‏‎‎‏‎‏‏‎‏‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‏‎‏‎‎‏‏‏‎‎Picture-in-Picture menu.‎‏‎‎‏‎"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‏‎‎‎‎‎‎‏‏‏‎‎‏‎‎‎‎‎‎‎‏‏‏‎‎‏‎‎‎‎‎‏‎‏‎‎‏‏‎‎‎‏‎‏‎‎‏‏‏‏‎Move left‎‏‎‎‏‎"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‏‏‎‎‎‏‎‎‎‏‏‏‏‎‎‎‎‏‎‏‎‏‎‏‎‏‎‎‎‏‎‎‏‏‎‎‏‏‏‏‎‎‏‎‏‎‏‎‏‎‏‎‏‎‎‎‎‎Move right‎‏‎‎‏‎"</string>
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
index d11cdbdc2979..0190ea85a30b 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú de pantalla en pantalla"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en modo de Pantalla en pantalla"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> use esta función, presiona para abrir la configuración y desactivarla."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproducir"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar el tamaño"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Almacenar de manera segura"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Dejar de almacenar de manera segura"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la app no funcione en el modo de pantalla dividida."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La app no es compatible con la función de pantalla dividida."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Divisor de pantalla dividida"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de pantalla dividida"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Divisor de pantalla dividida"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla izquierda completa"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Izquierda: 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda: 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"¿No se resolvió?\nPresiona para revertir los cambios"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"¿No tienes problemas con la cámara? Presionar para descartar."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Aprovecha más"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Arrastra otra app para el modo de pantalla dividida"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arrastra otra app para el modo de pantalla dividida"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Presiona dos veces fuera de una app para cambiar su ubicación"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expande para obtener más información."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string>
<string name="close_text" msgid="4986518933445178928">"Cerrar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Abrir el menú"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml
index a2c27b79e04c..e0f3297ff966 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mover"</string>
<string name="pip_expand" msgid="1051966011679297308">"Expandir"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Presiona dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Presiona dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla en pantalla"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string>
diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml
index 227f5f428102..ea44bead6766 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Ajustes"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú de imagen en imagen"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en imagen en imagen"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca la notificación para abrir los ajustes y desactivarla."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproducir"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Esconder"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"No esconder"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Es posible que la aplicación no funcione con la pantalla dividida."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"La aplicación no admite la pantalla dividida."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Dividir la pantalla"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de pantalla dividida"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Divisor de pantalla dividida"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla izquierda completa"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Izquierda 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"¿No se ha solucionado?\nToca para revertir"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"¿No hay problemas con la cámara? Toca para cerrar."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Consulta más información y haz más"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Arrastra otra aplicación para activar la pantalla dividida"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arrastra otra aplicación para activar la pantalla dividida"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toca dos veces fuera de una aplicación para cambiarla de posición"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Mostrar más información"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string>
<string name="close_text" msgid="4986518933445178928">"Cerrar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-es/strings_tv.xml b/libs/WindowManager/Shell/res/values-es/strings_tv.xml
index 75db421ec405..38be3effe356 100644
--- a/libs/WindowManager/Shell/res/values-es/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mover"</string>
<string name="pip_expand" msgid="1051966011679297308">"Mostrar"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Pulsa dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Pulsa dos veces "<annotation icon="home_icon">"INICIO"</annotation>" para ver los controles"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de imagen en imagen."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover hacia la izquierda"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover hacia la derecha"</string>
diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml
index 2ed0fdf25f18..90feff32cc2b 100644
--- a/libs/WindowManager/Shell/res/values-et/strings.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Seaded"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Ava jagatud ekraanikuva"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menüü"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menüü Pilt pildis"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on režiimis Pilt pildis"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Kui te ei soovi, et rakendus <xliff:g id="NAME">%s</xliff:g> seda funktsiooni kasutaks, puudutage seadete avamiseks ja lülitage see välja."</string>
<string name="pip_play" msgid="3496151081459417097">"Esita"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Suuruse muutmine"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Pane hoidlasse"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Eemalda hoidlast"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Rakendus ei pruugi poolitatud ekraaniga töötada."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Rakendus ei toeta jagatud ekraani."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Ekraanijagaja"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Jagatud ekraanikuva jaotur"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Jagatud ekraanikuva jaotur"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vasak täisekraan"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vasak: 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasak: 50%"</string>
@@ -82,12 +84,12 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Kas probleemi ei lahendatud?\nPuudutage ennistamiseks."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kas kaameraprobleeme pole? Puudutage loobumiseks."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Vaadake ja tehke rohkem"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Lohistage muusse rakendusse, et jagatud ekraanikuva kasutada"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Lohistage muusse rakendusse, et jagatud ekraanikuva kasutada"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Topeltpuudutage rakendusest väljaspool, et selle asendit muuta"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Selge"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Laiendage lisateabe saamiseks."</string>
<string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Kas taaskäivitada parema vaate saavutamiseks?"</string>
- <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Saate rakenduse taaskäivitada, et see näeks ekraanikuval parem välja, kuid võite kaotada edenemise või salvestamata muudatused."</string>
+ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Saate rakenduse taaskäivitada, et see näeks ekraanikuval parem välja, kuid võite kaotada edenemise või salvestamata muudatused"</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"Tühista"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"Taaskäivita"</string>
<string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ära kuva uuesti"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Ekraanipilt"</string>
<string name="close_text" msgid="4986518933445178928">"Sule"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Sule menüü"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Ava menüü"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-et/strings_tv.xml b/libs/WindowManager/Shell/res/values-et/strings_tv.xml
index e8fcb180c0c4..a93cee51ce07 100644
--- a/libs/WindowManager/Shell/res/values-et/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Teisalda"</string>
<string name="pip_expand" msgid="1051966011679297308">"Laienda"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Ahenda"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Nuppude nägemiseks vajutage 2 korda nuppu "<annotation icon="home_icon">"AVAKUVA"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Nuppude nägemiseks vajutage 2 korda nuppu "<annotation icon="home_icon">"AVAKUVA"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menüü Pilt pildis."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Teisalda vasakule"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Teisalda paremale"</string>
diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml
index 4aeb7e3ddf7f..de27a8045e85 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Ezarpenak"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Sartu pantaila zatituan"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Pantaila txiki gainjarriaren menua"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"Pantaila txiki gainjarrian dago <xliff:g id="NAME">%s</xliff:g>"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea nahi ez baduzu, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string>
<string name="pip_play" msgid="3496151081459417097">"Erreproduzitu"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Aldatu tamaina"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Gorde"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ez gorde"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Baliteke aplikazioak ez funtzionatzea pantaila zatituan."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikazioak ez du onartzen pantaila zatitua"</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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 abiarazi bigarren mailako pantailatan."</string>
- <string name="accessibility_divider" msgid="703810061635792791">"Pantaila-zatitzailea"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Pantaila-zatitzailea"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Pantaila-zatitzailea"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ezarri ezkerraldea pantaila osoan"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ezarri ezkerraldea % 70en"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ezarri ezkerraldea % 50en"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ez al da konpondu?\nLeheneratzeko, sakatu hau."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ez daukazu arazorik kamerarekin? Baztertzeko, sakatu hau."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Ikusi eta egin gauza gehiago"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Pantaila zatituta ikusteko, arrastatu beste aplikazio bat"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Pantaila zatitua ikusteko, arrastatu beste aplikazio bat"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Aplikazioaren posizioa aldatzeko, sakatu birritan haren kanpoaldea"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Ados"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Informazio gehiago lortzeko, zabaldu hau."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Pantaila-argazkia"</string>
<string name="close_text" msgid="4986518933445178928">"Itxi"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Itxi menua"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Ireki menua"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml
index 07d75d2de9cd..4b752fc9d1c4 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mugitu"</string>
<string name="pip_expand" msgid="1051966011679297308">"Zabaldu"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Tolestu"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Kontrolatzeko aukerak atzitzeko, sakatu birritan "<annotation icon="home_icon">" HASIERA "</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Kontrolatzeko aukerak atzitzeko, sakatu birritan "<annotation icon="home_icon">"HASIERA"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pantaila txiki gainjarriaren menua."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Eraman ezkerrera"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Eraman eskuinera"</string>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index 703996a6f12d..13a2ea2db140 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"تنظیمات"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"ورود به حالت «صفحهٔ دونیمه»"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"منو"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"منو تصویر در تصویر"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> درحالت تصویر در تصویر است"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"اگر نمی‌خواهید <xliff:g id="NAME">%s</xliff:g> از این قابلیت استفاده کند، با ضربه زدن، تنظیمات را باز کنید و آن را خاموش کنید."</string>
<string name="pip_play" msgid="3496151081459417097">"پخش"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"تغییر اندازه"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"مخفی‌سازی"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"لغو مخفی‌سازی"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن است برنامه با «صفحهٔ دونیمه» کار نکند."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"برنامه از تقسیم صفحه پشتیبانی نمی‌کند."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"ممکن است برنامه با صفحهٔ دونیمه کار نکند"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"برنامه از صفحهٔ دونیمه پشتیبانی نمی‌کند"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"این برنامه فقط در ۱ پنجره می‌تواند باز شود."</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="703810061635792791">"تقسیم‌کننده صفحه"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"تقسیم‌کننده صفحهٔ دونیمه"</string>
+ <string name="divider_title" msgid="1963391955593749442">"تقسیم‌کننده صفحهٔ دونیمه"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"تمام‌صفحه چپ"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"٪۷۰ چپ"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"٪۵۰ چپ"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"مشکل برطرف نشد؟\nبرای برگرداندن ضربه بزنید"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"دوربین مشکلی ندارد؟ برای بستن ضربه بزنید."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"از چندین برنامه به‌طور هم‌زمان استفاده کنید"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"برای حالت صفحهٔ دونیمه، در برنامه‌ای دیگر بکشید"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"برای حالت صفحهٔ دونیمه، در برنامه‌ای دیگر بکشید"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"برای جابه‌جا کردن برنامه، بیرون از آن دوضربه بزنید"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"متوجه‌ام"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"برای اطلاعات بیشتر، گسترده کنید."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"نماگرفت"</string>
<string name="close_text" msgid="4986518933445178928">"بستن"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"بستن منو"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"باز کردن منو"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml
index 03f51d01a3a8..55394cbdc31a 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"انتقال"</string>
<string name="pip_expand" msgid="1051966011679297308">"گسترده کردن"</string>
<string name="pip_collapse" msgid="3903295106641385962">"جمع کردن"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" برای کنترل‌ها، دکمه "<annotation icon="home_icon">"صفحه اصلی"</annotation>" را دوبار فشار دهید"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"برای کنترل‌ها، دکمه "<annotation icon="home_icon">"صفحه اصلی"</annotation>" را دوبار فشار دهید"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"منوی تصویر در تصویر."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"انتقال به‌چپ"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"انتقال به‌راست"</string>
diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml
index 0cd1260d54dc..92fa76013134 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Asetukset"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Avaa jaettu näyttö"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Valikko"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Kuva kuvassa ‑valikko"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on kuva kuvassa ‑tilassa"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Jos et halua, että <xliff:g id="NAME">%s</xliff:g> voi käyttää tätä ominaisuutta, avaa asetukset napauttamalla ja poista se käytöstä."</string>
<string name="pip_play" msgid="3496151081459417097">"Toista"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Muuta kokoa"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Lisää turvasäilytykseen"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poista turvasäilytyksestä"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Sovellus ei ehkä toimi jaetulla näytöllä."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Sovellus ei tue jaetun näytön tilaa."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Näytön jakaja"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Näytönjakaja"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Näytönjakaja"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vasen koko näytölle"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vasen 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasen 50 %"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Eikö ongelma ratkennut?\nKumoa napauttamalla"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ei ongelmia kameran kanssa? Hylkää napauttamalla."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Näe ja tee enemmän"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Käytä jaettua näyttöä vetämällä tähän toinen sovellus"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Käytä jaettua näyttöä vetämällä tähän toinen sovellus"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Kaksoisnapauta sovelluksen ulkopuolella, jos haluat siirtää sitä"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Katso lisätietoja laajentamalla."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Kuvakaappaus"</string>
<string name="close_text" msgid="4986518933445178928">"Sulje"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Sulje valikko"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Avaa valikko"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml
index 24ab7d99e180..f580d01691f9 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Siirrä"</string>
<string name="pip_expand" msgid="1051966011679297308">"Laajenna"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Tiivistä"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Asetukset: paina "<annotation icon="home_icon">"ALOITUSNÄYTTÖPAINIKETTA"</annotation>" kahdesti"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Asetukset: paina "<annotation icon="home_icon">"ALOITUSNÄYTTÖPAINIKETTA"</annotation>" kahdesti"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kuva kuvassa ‑valikko."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Siirrä vasemmalle"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Siirrä oikealle"</string>
diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
index 016069f65dc0..7814b7d38fed 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Entrer dans l\'écran partagé"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu d\'incrustation d\'image"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode d\'incrustation d\'image"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, touchez l\'écran pour ouvrir les paramètres, puis désactivez-la."</string>
<string name="pip_play" msgid="3496151081459417097">"Lire"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ajouter à la réserve"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Retirer de la réserve"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'application n\'est pas compatible avec l\'écran partagé."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Cette application ne peut être ouverte que dans une 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="703810061635792791">"Séparateur d\'écran partagé"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Séparateur d\'écran partagé"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Séparateur d\'écran partagé"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Plein écran à la gauche"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % à la gauche"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % à la gauche"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle ignorée."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Pour obtenir un meilleur affichage, touchez pour redémarrer cette application."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Touchez pour redémarrer cette application afin d\'obtenir un meilleur affichage."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problèmes d\'appareil photo?\nTouchez pour réajuster"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problème non résolu?\nTouchez pour rétablir"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Aucun problème d\'appareil photo? Touchez pour ignorer."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Voir et en faire plus"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Faites glisser une autre application pour utiliser l\'écran partagé"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Faites glisser une autre application pour utiliser l\'écran partagé"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Touchez deux fois à côté d\'une application pour la repositionner"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Développer pour en savoir plus."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Capture d\'écran"</string>
<string name="close_text" msgid="4986518933445178928">"Fermer"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml
index 87651ec711d9..39a785d4fcc0 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Déplacer"</string>
<string name="pip_expand" msgid="1051966011679297308">"Développer"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Appuyez deux fois sur "<annotation icon="home_icon">" ACCUEIL "</annotation>" pour les commandes"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation>" pour les commandes"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu d\'incrustation d\'image."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 5cc8ff07014d..da5b5c9bfeba 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Accéder à l\'écran partagé"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu \"Picture-in-picture\""</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode Picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que l\'application <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, appuyez ici pour ouvrir les paramètres et la désactiver."</string>
<string name="pip_play" msgid="3496151081459417097">"Lecture"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionner"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Il est possible que l\'application ne fonctionne pas en mode Écran partagé."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Application incompatible avec l\'écran partagé."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Séparateur d\'écran partagé"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Séparateur d\'écran partagé"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Séparateur d\'écran partagé"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Écran de gauche en plein écran"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Écran de gauche à 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Écran de gauche à 50 %"</string>
@@ -77,17 +79,17 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle fermée."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Pour un meilleur affichage, appuyez pour redémarrer cette appli."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Appuyez pour redémarrer cette appli et avoir une meilleure vue."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problèmes d\'appareil photo ?\nAppuyez pour réajuster"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problème non résolu ?\nAppuyez pour rétablir"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Aucun problème d\'appareil photo ? Appuyez pour ignorer."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Voir et interagir plus"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Faites glisser une autre appli pour utiliser l\'écran partagé"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Faites glisser une autre appli pour utiliser l\'écran partagé"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Appuyez deux fois en dehors d\'une appli pour la repositionner"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Développez pour obtenir plus d\'informations"</string>
<string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Redémarrer pour améliorer l\'affichage ?"</string>
- <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Vous pouvez redémarrer l\'appli pour un meilleur rendu sur votre écran, mais il se peut que vous perdiez votre progression ou les modifications non enregistrées"</string>
+ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Vous pouvez redémarrer l\'appli pour en améliorer son aspect sur votre écran, mais vous risquez de perdre votre progression ou les modifications non enregistrées"</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"Annuler"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"Redémarrer"</string>
<string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Ne plus afficher"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Capture d\'écran"</string>
<string name="close_text" msgid="4986518933445178928">"Fermer"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml
index 37863fb82295..db4bc54cf665 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Déplacer"</string>
<string name="pip_expand" msgid="1051966011679297308">"Développer"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Réduire"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Menu de commandes : appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Commandes : appuyez deux fois sur "<annotation icon="home_icon">"ACCUEIL"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu \"Picture-in-picture\"."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Déplacer vers la gauche"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Déplacer vers la droite"</string>
diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml
index 3980017aa12b..c08cff86f63c 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Inserir pantalla dividida"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menú de pantalla superposta"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está na pantalla superposta"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Se non queres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca a configuración para abrir as opcións e desactivar a función."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproducir"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Cambiar tamaño"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Esconder"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Non esconder"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Pode que a aplicación non funcione coa pantalla dividida."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A aplicación non é compatible coa función de pantalla dividida."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Divisor de pantalla dividida"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de pantalla dividida"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Divisor de pantalla dividida"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Pantalla completa á esquerda"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70 % á esquerda"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % á esquerda"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Non se solucionaron os problemas?\nToca para reverter o seu tratamento"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Non hai problemas coa cámara? Tocar para ignorar."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Ver e facer máis"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Arrastra outra aplicación para usar a pantalla dividida"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arrastra outra aplicación para usar a pantalla dividida"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toca dúas veces fóra da aplicación para cambiala de posición"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendido"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Despregar para obter máis información."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string>
<string name="close_text" msgid="4986518933445178928">"Pechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Pechar o menú"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml
index 5d6de76c4deb..22e68d3707ac 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mover"</string>
<string name="pip_expand" msgid="1051966011679297308">"Despregar"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Contraer"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Preme "<annotation icon="home_icon">"INICIO"</annotation>" dúas veces para acceder aos controis"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Preme "<annotation icon="home_icon">"INICIO"</annotation>" dúas veces para acceder aos controis"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menú de pantalla superposta."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover cara á esquerda"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover cara á dereita"</string>
diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml
index 7a2ad0e93855..2a521991ef8d 100644
--- a/libs/WindowManager/Shell/res/values-gu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gu/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"સેટિંગ"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"વિભાજિત સ્ક્રીન મોડમાં દાખલ થાઓ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"મેનૂ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ચિત્રમાં ચિત્ર મેનૂ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ચિત્રમાં-ચિત્રની અંદર છે"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"જો તમે નથી ઇચ્છતા કે <xliff:g id="NAME">%s</xliff:g> આ સુવિધાનો ઉપયોગ કરે, તો સેટિંગ ખોલવા માટે ટૅપ કરો અને તેને બંધ કરો."</string>
<string name="pip_play" msgid="3496151081459417097">"ચલાવો"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"કદ બદલો"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"છુપાવો"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"બતાવો"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"વિભાજિત-સ્ક્રીન સાથે ઍપ કદાચ કામ ન કરે."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ઍપ્લિકેશન સ્ક્રીન-વિભાજનનું સમર્થન કરતી નથી."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"વિભાજિત સ્ક્રીન સાથે ઍપ કદાચ કામ ન કરે"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ઍપ વિભાજિત સ્ક્રીનને સપોર્ટ કરતી નથી"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"આ ઍપ માત્ર 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="703810061635792791">"સ્પ્લિટ-સ્ક્રીન વિભાજક"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"સ્ક્રીનને વિભાજિત કરતું વિભાજક"</string>
+ <string name="divider_title" msgid="1963391955593749442">"સ્ક્રીનને વિભાજિત કરતું વિભાજક"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ડાબી પૂર્ણ સ્ક્રીન"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ડાબે 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ડાબે 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"સુધારો નથી થયો?\nપહેલાંના પર પાછું ફેરવવા માટે ટૅપ કરો"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"કૅમેરામાં કોઈ સમસ્યા નથી? છોડી દેવા માટે ટૅપ કરો."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"જુઓ અને બીજું ઘણું કરો"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"સ્ક્રીન વિભાજન માટે કોઈ અન્ય ઍપમાં ખેંચો"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"વિભાજિત સ્ક્રીન માટે કોઈ અન્ય ઍપમાં ખેંચો"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"કોઈ ઍપની જગ્યા બદલવા માટે, તેની બહાર બે વાર ટૅપ કરો"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"સમજાઈ ગયું"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"વધુ માહિતી માટે મોટું કરો."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"સ્ક્રીનશૉટ"</string>
<string name="close_text" msgid="4986518933445178928">"બંધ કરો"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"મેનૂ બંધ કરો"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"મેનૂ ખોલો"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml
index 6c1b9db73582..01b9b4b987d0 100644
--- a/libs/WindowManager/Shell/res/values-gu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-gu/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ખસેડો"</string>
<string name="pip_expand" msgid="1051966011679297308">"મોટું કરો"</string>
<string name="pip_collapse" msgid="3903295106641385962">"નાનું કરો"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" નિયંત્રણો માટે "<annotation icon="home_icon">" હોમ "</annotation>" બટન પર બે વાર દબાવો"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"નિયંત્રણો માટે "<annotation icon="home_icon">"હોમ"</annotation>" બટન બે વાર દબાવો"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ચિત્રમાં ચિત્ર મેનૂ."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ડાબે ખસેડો"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"જમણે ખસેડો"</string>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index bd3fbb86891f..fb5040b36a89 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन मोड में जाएं"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"मेन्यू"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"पिक्चर में पिक्चर मेन्यू"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"पिक्चर में पिक्चर\" के अंदर है"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"अगर आप नहीं चाहते कि <xliff:g id="NAME">%s</xliff:g> इस सुविधा का उपयोग करे, तो सेटिंग खोलने के लिए टैप करें और उसे बंद करें ."</string>
<string name="pip_play" msgid="3496151081459417097">"चलाएं"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदलें"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"छिपाएं"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"दिखाएं"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ऐप्लिकेशन शायद स्प्लिट स्क्रीन मोड में काम न करे."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ऐप विभाजित स्‍क्रीन का समर्थन नहीं करता है."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"मुमकिन है कि ऐप्लिकेशन, स्प्लिट स्क्रीन मोड में काम न करे"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"यह ऐप्लिकेशन, स्प्लिट स्क्रीन मोड पर काम नहीं करता"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"इस ऐप्लिकेशन को सिर्फ़ एक विंडो में खोला जा सकता है."</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="703810061635792791">"विभाजित स्क्रीन विभाजक"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"स्प्लिट स्क्रीन डिवाइडर मोड"</string>
+ <string name="divider_title" msgid="1963391955593749442">"स्प्लिट स्क्रीन डिवाइडर मोड"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"बाईं स्क्रीन को फ़ुल स्क्रीन बनाएं"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"बाईं स्क्रीन को 70% बनाएं"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बाईं स्क्रीन को 50% बनाएं"</string>
@@ -82,12 +84,12 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"क्या समस्या ठीक नहीं हुई?\nपहले जैसा करने के लिए टैप करें"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"क्या कैमरे से जुड़ी कोई समस्या नहीं है? खारिज करने के लिए टैप करें."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"पूरी जानकारी लेकर, बेहतर तरीके से काम करें"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"स्प्लिट स्क्रीन के लिए, दूसरे ऐप्लिकेशन को खींचें और छोड़ें"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"स्प्लिट स्क्रीन का इस्तेमाल करने के लिए, किसी अन्य ऐप्लिकेशन को खींचें और छोड़ें"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"किसी ऐप्लिकेशन की जगह बदलने के लिए, उसके बाहर दो बार टैप करें"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ठीक है"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ज़्यादा जानकारी के लिए बड़ा करें."</string>
<string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"बेहतर व्यू पाने के लिए ऐप्लिकेशन को रीस्टार्ट करना है?"</string>
- <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"स्क्रीन पर ऐप्लिकेशन का बेहतर व्यू पाने के लिए उसे रीस्टार्ट करें. हालांकि, इससे अब तक किया गया काम और सेव न किए गए बदलाव मिट सकते हैं"</string>
+ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"स्क्रीन पर ऐप्लिकेशन का बेहतर व्यू पाने के लिए उसे रीस्टार्ट करें. हालांकि, आपने जो बदलाव सेव नहीं किए हैं या अब तक जो काम किए हैं उनका डेटा, ऐप्लिकेशन रीस्टार्ट करने पर मिट सकता है"</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"रद्द करें"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"रीस्टार्ट करें"</string>
<string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फिर से न दिखाएं"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"स्क्रीनशॉट"</string>
<string name="close_text" msgid="4986518933445178928">"बंद करें"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"मेन्यू बंद करें"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"मेन्यू खोलें"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml
index 6ae673622331..e2f272c36329 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ले जाएं"</string>
<string name="pip_expand" msgid="1051966011679297308">"बड़ा करें"</string>
<string name="pip_collapse" msgid="3903295106641385962">"छोटा करें"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" कंट्रोल मेन्यू पर जाने के लिए, "<annotation icon="home_icon">" होम बटन"</annotation>" दो बार दबाएं"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"कंट्रोल मेन्यू पर जाने के लिए "<annotation icon="home_icon">" होम"</annotation>" को दो बार दबाएं"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"पिक्चर में पिक्चर मेन्यू."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बाईं ओर ले जाएं"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दाईं ओर ले जाएं"</string>
diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml
index c69b97101626..2535657c7d86 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvorite podijeljeni zaslon"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Izbornik"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Izbornik slike u slici"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> jest na slici u slici"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da aplikacija <xliff:g id="NAME">%s</xliff:g> upotrebljava tu značajku, dodirnite da biste otvorili postavke i isključili je."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproduciraj"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Promjena veličine"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Sakrijte"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Poništite sakrivanje"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija možda neće funkcionirati s podijeljenim zaslonom."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podržava podijeljeni zaslon."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Razdjelnik podijeljenog zaslona"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Razdjelnik podijeljenog zaslona"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Razdjelnik podijeljenog zaslona"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lijevi zaslon u cijeli zaslon"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Lijevi zaslon na 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevi zaslon na 50%"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić odbačen."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Dodirnite za ponovno pokretanje te aplikacije i bolji prikaz."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Dodirnite da biste ponovo pokrenuli tu aplikaciju kako biste bolje vidjeli."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemi s fotoaparatom?\nDodirnite za popravak"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Problem nije riješen?\nDodirnite za vraćanje"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nemate problema s fotoaparatom? Dodirnite za odbacivanje."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Gledajte i učinite više"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Povucite drugu aplikaciju unutra da biste podijelili zaslon"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Povucite drugu aplikaciju unutra da biste podijelili zaslon"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvaput dodirnite izvan aplikacije da biste je premjestili"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Shvaćam"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Proširite da biste saznali više."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Snimka zaslona"</string>
<string name="close_text" msgid="4986518933445178928">"Zatvorite"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite izbornik"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje izbornika"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml
index a09e6e805f63..965b9b8c31c6 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Premjesti"</string>
<string name="pip_expand" msgid="1051966011679297308">"Proširi"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Sažmi"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Dvaput pritisnite "<annotation icon="home_icon">"POČETNI ZASLON"</annotation>" za kontrole"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Dvaput pritisnite "<annotation icon="home_icon">"POČETNI ZASLON"</annotation>" za kontrole"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izbornik slike u slici."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pomaknite ulijevo"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pomaknite udesno"</string>
diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml
index c4cdf6de709f..7566439ec7f8 100644
--- a/libs/WindowManager/Shell/res/values-hu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hu/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Beállítások"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Váltás osztott képernyőre"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Kép a képben menü"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"A(z) <xliff:g id="NAME">%s</xliff:g> kép a képben funkciót használ"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ha nem szeretné, hogy a(z) <xliff:g id="NAME">%s</xliff:g> használja ezt a funkciót, koppintson a beállítások megnyitásához, és kapcsolja ki."</string>
<string name="pip_play" msgid="3496151081459417097">"Lejátszás"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Átméretezés"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Félretevés"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Félretevés megszüntetése"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"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="7408396418008948957">"Az alkalmazás nem támogatja az osztott képernyős nézetet."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Elválasztó az osztott nézetben"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Elválasztó az osztott képernyős nézetben"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Elválasztó az osztott képernyős nézetben"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Bal oldali teljes képernyőre"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Bal oldali 70%-ra"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Bal oldali 50%-ra"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nem sikerült a hiba kijavítása?\nKoppintson a visszaállításhoz."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nincsenek problémái kamerával? Koppintson az elvetéshez."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Több mindent láthat és tehet"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Húzzon ide egy másik alkalmazást az osztott képernyő használatához"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Húzzon ide egy másik alkalmazást az osztott képernyő használatához"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Koppintson duplán az alkalmazáson kívül az áthelyezéséhez"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Értem"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Kibontással további információkhoz juthat."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Képernyőkép"</string>
<string name="close_text" msgid="4986518933445178928">"Bezárás"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menü bezárása"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Menü megnyitása"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml
index 5e065c2ad4e7..90cbfe643c82 100644
--- a/libs/WindowManager/Shell/res/values-hu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hu/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Áthelyezés"</string>
<string name="pip_expand" msgid="1051966011679297308">"Kibontás"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Összecsukás"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Vezérlők: "<annotation icon="home_icon">" KEZDŐKÉPERNYŐ "</annotation>" gomb kétszer megnyomva"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Vezérlők: A "<annotation icon="home_icon">"KEZDŐKÉPERNYŐ"</annotation>" gomb kétszeri megnyomása"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Kép a képben menü."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mozgatás balra"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mozgatás jobbra"</string>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml
index 05df3fa7c0d3..2b20870b1048 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Կարգավորումներ"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Մտնել տրոհված էկրանի ռեժիմ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Ընտրացանկ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"«Նկար նկարի մեջ» ռեժիմի ընտրացանկ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>-ը «Նկար նկարի մեջ» ռեժիմում է"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Եթե չեք ցանկանում, որ <xliff:g id="NAME">%s</xliff:g>-ն օգտագործի այս գործառույթը, հպեք՝ կարգավորումները բացելու և այն անջատելու համար։"</string>
<string name="pip_play" msgid="3496151081459417097">"Նվագարկել"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Փոխել չափը"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Թաքցնել"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ցուցադրել"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Հավելվածը չի կարող աշխատել տրոհված էկրանի ռեժիմում։"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Հավելվածը չի աջակցում էկրանի տրոհումը:"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Հավելվածը չի կարող աշխատել տրոհված էկրանի ռեժիմում"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Հավելվածը չի աջակցում էկրանի տրոհումը"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Այս հավելվածը հնարավոր է բացել միայն մեկ պատուհանում։"</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="703810061635792791">"Տրոհված էկրանի բաժանիչ"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Տրոհված էկրանի բաժանիչ"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Տրոհված էկրանի բաժանիչ"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ձախ էկրանը՝ լիաէկրան"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ձախ էկրանը՝ 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ձախ էկրանը՝ 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Չհաջողվե՞ց շտկել։\nՀպեք՝ փոփոխությունները չեղարկելու համար։"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Տեսախցիկի հետ կապված խնդիրներ չկա՞ն։ Փակելու համար հպեք։"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Միաժամանակ կատարեք մի քանի առաջադրանք"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Քաշեք մյուս հավելվածի մեջ՝ էկրանի տրոհումն օգտագործելու համար"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Քաշեք մյուս հավելվածի մեջ՝ էկրանի տրոհումն օգտագործելու համար"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Կրկնակի հպեք հավելվածի կողքին՝ այն տեղափոխելու համար"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Եղավ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Ծավալեք՝ ավելին իմանալու համար։"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Սքրինշոթ"</string>
<string name="close_text" msgid="4986518933445178928">"Փակել"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Փակել ընտրացանկը"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Բացել ընտրացանկը"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml
index 7963abf8972b..30b5911147b5 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Տեղափոխել"</string>
<string name="pip_expand" msgid="1051966011679297308">"Ծավալել"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Ծալել"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Կարգավորումների համար կրկնակի սեղմեք "<annotation icon="home_icon">"ԳԼԽԱՎՈՐ ԷԿՐԱՆ"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Կարգավորումների համար կրկնակի սեղմեք "<annotation icon="home_icon">"ԳԼԽԱՎՈՐ ԷԿՐԱՆ"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"«Նկար նկարի մեջ» ռեժիմի ընտրացանկ։"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Տեղափոխել ձախ"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Տեղափոխել աջ"</string>
diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml
index af57ccaeeeca..5747deb405ab 100644
--- a/libs/WindowManager/Shell/res/values-in/strings.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Setelan"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk ke mode layar terpisah"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu Picture-in-Picture"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> adalah picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Jika Anda tidak ingin <xliff:g id="NAME">%s</xliff:g> menggunakan fitur ini, ketuk untuk membuka setelan dan menonaktifkannya."</string>
<string name="pip_play" msgid="3496151081459417097">"Putar"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah ukuran"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Batalkan stash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikasi mungkin tidak berfungsi dengan layar terpisah."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App tidak mendukung layar terpisah."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Pembagi layar terpisah"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Pembagi layar terpisah"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Pembagi layar terpisah"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Layar penuh di kiri"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kiri 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string>
@@ -82,11 +84,11 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Tidak dapat diperbaiki?\nKetuk untuk mengembalikan"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Tidak ada masalah kamera? Ketuk untuk menutup."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Lihat dan lakukan lebih banyak hal"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Tarik aplikasi lain untuk menggunakan layar terpisah"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Tarik aplikasi lain untuk menggunakan layar terpisah"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Ketuk dua kali di luar aplikasi untuk mengubah posisinya"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Oke"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Luaskan untuk melihat informasi selengkapnya."</string>
- <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Mulai ulang untuk melihat tampilan yang lebih baik?"</string>
+ <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Mulai ulang untuk tampilan yang lebih baik?"</string>
<string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Anda dapat memulai ulang aplikasi agar terlihat lebih baik di layar, tetapi Anda mungkin kehilangan progres atau perubahan yang belum disimpan"</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"Batal"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"Mulai ulang"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Tutup"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-in/strings_tv.xml b/libs/WindowManager/Shell/res/values-in/strings_tv.xml
index 7d37154bb86c..0fda69f6c0e4 100644
--- a/libs/WindowManager/Shell/res/values-in/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Pindahkan"</string>
<string name="pip_expand" msgid="1051966011679297308">"Luaskan"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Ciutkan"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" HOME "</annotation>" untuk membuka kontrol"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Tekan dua kali "<annotation icon="home_icon">"HOME"</annotation>" untuk membuka kontrol"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture-in-Picture."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pindahkan ke kiri"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pindahkan ke kanan"</string>
diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml
index 512deb252274..145d26d35b75 100644
--- a/libs/WindowManager/Shell/res/values-is/strings.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Stillingar"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Opna skjáskiptingu"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Valmynd"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Valmynd fyrir mynd í mynd"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er með mynd í mynd"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ef þú vilt ekki að <xliff:g id="NAME">%s</xliff:g> noti þennan eiginleika skaltu ýta til að opna stillingarnar og slökkva á því."</string>
<string name="pip_play" msgid="3496151081459417097">"Spila"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Breyta stærð"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Geymsla"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Taka úr geymslu"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Hugsanlega virkar forritið ekki með skjáskiptingu."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Forritið styður ekki að skjánum sé skipt."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Skjáskipting"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Skilrúm skjáskiptingar"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Skilrúm skjáskiptingar"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Vinstri á öllum skjánum"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vinstri 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vinstri 50%"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Blaðra"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Stjórna"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Blöðru lokað."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Ýttu til að endurræsa forritið og fá betri sýn."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Ýta til að endurræsa forritið og fá betri sýn."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Myndavélavesen?\nÝttu til að breyta stærð"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ennþá vesen?\nÝttu til að afturkalla"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Ekkert myndavélavesen? Ýttu til að hunsa."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Sjáðu og gerðu meira"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Dragðu annað forrit inn til að nota skjáskiptingu"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Dragðu annað forrit inn til að nota skjáskiptingu"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Ýttu tvisvar utan við forrit til að færa það"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Ég skil"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Stækka til að sjá frekari upplýsingar."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Skjámynd"</string>
<string name="close_text" msgid="4986518933445178928">"Loka"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Loka valmynd"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Opna valmynd"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-is/strings_tv.xml b/libs/WindowManager/Shell/res/values-is/strings_tv.xml
index 1490cb98e034..e0d604f30d1a 100644
--- a/libs/WindowManager/Shell/res/values-is/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Færa"</string>
<string name="pip_expand" msgid="1051966011679297308">"Stækka"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Minnka"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Ýttu tvisvar á "<annotation icon="home_icon">" HEIM "</annotation>" til að opna stillingar"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Ýttu tvisvar á "<annotation icon="home_icon">"HEIM"</annotation>" til að opna stillingar"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Valmynd fyrir mynd í mynd."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Færa til vinstri"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Færa til hægri"</string>
diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml
index 482666632bc9..025646cbb9cd 100644
--- a/libs/WindowManager/Shell/res/values-it/strings.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Impostazioni"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Accedi a schermo diviso"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu Picture in picture"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> è in Picture in picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Se non desideri che l\'app <xliff:g id="NAME">%s</xliff:g> utilizzi questa funzione, tocca per aprire le impostazioni e disattivarla."</string>
<string name="pip_play" msgid="3496151081459417097">"Riproduci"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ridimensiona"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Accantona"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Annulla accantonamento"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"L\'app potrebbe non funzionare con lo schermo diviso."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"L\'app non supporta la modalità Schermo diviso."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Strumento per schermo diviso"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Strumento per schermo diviso"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Strumento per schermo diviso"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Schermata sinistra a schermo intero"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Schermata sinistra al 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Schermata sinistra al 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Il problema non si è risolto?\nTocca per ripristinare"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nessun problema con la fotocamera? Tocca per ignorare."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Visualizza più contenuti e fai di più"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Trascina in un\'altra app per usare lo schermo diviso"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Trascina in un\'altra app per usare lo schermo diviso"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tocca due volte fuori da un\'app per riposizionarla"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Espandi per avere ulteriori informazioni."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Chiudi"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Chiudi il menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Apri menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-it/strings_tv.xml b/libs/WindowManager/Shell/res/values-it/strings_tv.xml
index a48516f2588e..267f67463917 100644
--- a/libs/WindowManager/Shell/res/values-it/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Sposta"</string>
<string name="pip_expand" msgid="1051966011679297308">"Espandi"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Comprimi"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Premi due volte "<annotation icon="home_icon">" HOME "</annotation>" per aprire i controlli"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Premi due volte "<annotation icon="home_icon">"HOME"</annotation>" per accedere ai controlli"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Picture in picture."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sposta a sinistra"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sposta a destra"</string>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml
index 578d2478347b..bb3845b26a88 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"הגדרות"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"כניסה למסך המפוצל"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"תפריט"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"תפריט \'תמונה בתוך תמונה\'"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> במצב תמונה בתוך תמונה"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"אם אינך רוצה שהתכונה הזו תשמש את <xliff:g id="NAME">%s</xliff:g>, יש להקיש כדי לפתוח את ההגדרות ולהשבית את התכונה."</string>
<string name="pip_play" msgid="3496151081459417097">"הפעלה"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"שינוי גודל"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"הסתרה זמנית"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ביטול ההסתרה הזמנית"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ייתכן שהאפליקציה לא תפעל במסך מפוצל."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"האפליקציה אינה תומכת במסך מפוצל."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"יכול להיות שהאפליקציה לא תפעל עם מסך מפוצל"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"האפליקציה לא תומכת במסך מפוצל"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ניתן לפתוח את האפליקציה הזו רק בחלון אחד."</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="703810061635792791">"מחלק מסך מפוצל"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"מחלק מסך מפוצל"</string>
+ <string name="divider_title" msgid="1963391955593749442">"מחלק מסך מפוצל"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"מסך שמאלי מלא"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"שמאלה 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"שמאלה 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"הבעיה לא נפתרה?\nאפשר להקיש כדי לחזור לגרסה הקודמת"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"אין בעיות במצלמה? אפשר להקיש כדי לסגור."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"רוצה לראות ולעשות יותר?"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"צריך לגרור אפליקציה אחרת כדי להשתמש במסך מפוצל"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"צריך לגרור אפליקציה אחרת כדי להשתמש במסך המפוצל"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"צריך להקיש הקשה כפולה מחוץ לאפליקציה כדי למקם אותה מחדש"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"הבנתי"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"מרחיבים כדי לקבל מידע נוסף."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"צילום מסך"</string>
<string name="close_text" msgid="4986518933445178928">"סגירה"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"סגירת התפריט"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"פתיחת התפריט"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml
index 2af1896d3c67..6b30f5642ad3 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"העברה"</string>
<string name="pip_expand" msgid="1051966011679297308">"הרחבה"</string>
<string name="pip_collapse" msgid="3903295106641385962">"כיווץ"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" לחיצה כפולה על "<annotation icon="home_icon">" הלחצן הראשי "</annotation>" תציג את אמצעי הבקרה"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"לחיצה כפולה על "<annotation icon="home_icon">"בית"</annotation>" תציג את אמצעי הבקרה"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"תפריט \'תמונה בתוך תמונה\'."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"הזזה שמאלה"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"הזזה ימינה"</string>
diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml
index 0e4477a29520..6c1bafee9d82 100644
--- a/libs/WindowManager/Shell/res/values-ja/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ja/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"分割画面に切り替え"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"メニュー"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ピクチャー イン ピクチャーのメニュー"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>はピクチャー イン ピクチャーで表示中です"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>でこの機能を使用しない場合は、タップして設定を開いて OFF にしてください。"</string>
<string name="pip_play" msgid="3496151081459417097">"再生"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"サイズ変更"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"非表示"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"表示"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"アプリは分割画面では動作しないことがあります。"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"アプリで分割画面がサポートされていません。"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"アプリは分割画面では動作しないことがあります"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"アプリで分割画面がサポートされていません"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"このアプリはウィンドウが 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="703810061635792791">"分割画面の分割線"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"分割画面の分割線"</string>
+ <string name="divider_title" msgid="1963391955593749442">"分割画面の分割線"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左全画面"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左 50%"</string>
@@ -52,7 +54,7 @@
<string name="accessibility_split_top" msgid="2789329702027147146">"上に分割"</string>
<string name="accessibility_split_bottom" msgid="8694551025220868191">"下に分割"</string>
<string name="one_handed_tutorial_title" msgid="4583241688067426350">"片手モードの使用"</string>
- <string name="one_handed_tutorial_description" msgid="3486582858591353067">"終了するには、画面を下から上にスワイプするか、アプリの上側の任意の場所をタップします"</string>
+ <string name="one_handed_tutorial_description" msgid="3486582858591353067">"終了するには、画面を下から上にスワイプするか、アプリの任意の場所をタップします"</string>
<string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"片手モードを開始します"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"片手モードを終了します"</string>
<string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g> のバブルの設定"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"バブル"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ふきだしが非表示になっています。"</string>
- <string name="restart_button_description" msgid="6712141648865547958">"タップしてこのアプリを再起動すると、より見やすく表示されます。"</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"タップしてこのアプリを再起動すると、表示が適切になります。"</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"カメラに関する問題の場合は、\nタップすると修正できます"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"修正されなかった場合は、\nタップすると元に戻ります"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"カメラに関する問題でない場合は、タップすると閉じます。"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"表示を拡大して機能を強化"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"分割画面にするにはもう 1 つのアプリをドラッグしてください"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"分割画面にするにはもう 1 つのアプリをドラッグしてください"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"位置を変えるにはアプリの外側をダブルタップしてください"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"開くと詳細が表示されます。"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"スクリーンショット"</string>
<string name="close_text" msgid="4986518933445178928">"閉じる"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"メニューを閉じる"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"メニューを開く"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml
index bc7dcb7aa029..2a79e3c651a1 100644
--- a/libs/WindowManager/Shell/res/values-ja/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ja/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"移動"</string>
<string name="pip_expand" msgid="1051966011679297308">"開く"</string>
<string name="pip_collapse" msgid="3903295106641385962">"閉じる"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" コントロールにアクセス: "<annotation icon="home_icon">" ホーム "</annotation>" を 2 回押します"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"コントロールにアクセス: "<annotation icon="home_icon">" ホーム "</annotation>" を 2 回押します"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ピクチャー イン ピクチャーのメニューです。"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左に移動"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右に移動"</string>
diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml
index f50a9cdde355..e58e67ab36cb 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"პარამეტრები"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"გაყოფილ ეკრანში შესვლა"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"მენიუ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"„ეკრანი ეკრანში“ რეჟიმის მენიუ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> იყენებს რეჟიმს „ეკრანი ეკრანში“"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"თუ არ გსურთ, რომ <xliff:g id="NAME">%s</xliff:g> ამ ფუნქციას იყენებდეს, აქ შეხებით შეგიძლიათ გახსნათ პარამეტრები და გამორთოთ ის."</string>
<string name="pip_play" msgid="3496151081459417097">"დაკვრა"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ზომის შეცვლა"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"გადანახვა"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"გადანახვის გაუქმება"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"აპმა შეიძლება არ იმუშაოს გაყოფილი ეკრანის რეჟიმში."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ეკრანის გაყოფა არ არის მხარდაჭერილი აპის მიერ."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"აპმა შეიძლება არ იმუშაოს გაყოფილი ეკრანის რეჟიმში"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ეკრანის გაყოფა არ არის მხარდაჭერილი აპის მიერ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ამ აპის გახსნა შესაძლებელია მხოლოდ 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="703810061635792791">"გაყოფილი ეკრანის რეჟიმის გამყოფი"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"ეკრანის გაყოფის გამყოფი"</string>
+ <string name="divider_title" msgid="1963391955593749442">"ეკრანის გაყოფის გამყოფი"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"მარცხენა ნაწილის სრულ ეკრანზე გაშლა"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"მარცხენა ეკრანი — 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"მარცხენა ეკრანი — 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"არ გამოსწორდა?\nშეეხეთ წინა ვერსიის დასაბრუნებლად"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"კამერას პრობლემები არ აქვს? შეეხეთ უარყოფისთვის."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"მეტის ნახვა და გაკეთება"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"ეკრანის გასაყოფად ჩავლებით გადაიტანეთ სხვა აპში"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ეკრანის გასაყოფად ჩავლებით გადაიტანეთ სხვა აპში"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ორმაგად შეეხეთ აპის გარშემო სივრცეს, რათა ის სხვაგან გადაიტანოთ"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"გასაგებია"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"დამატებითი ინფორმაციისთვის გააფართოეთ."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ეკრანის ანაბეჭდი"</string>
<string name="close_text" msgid="4986518933445178928">"დახურვა"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"მენიუს დახურვა"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"მენიუს გახსნა"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml
index 898dac2aca88..58bae02120ff 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"გადაადგილება"</string>
<string name="pip_expand" msgid="1051966011679297308">"გაშლა"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ჩაკეცვა"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<annotation icon="home_icon">" მთავარ ღილაკს "</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"სახლის მართვის საშუალებებზე წვდომისთვის ორმაგად დააჭირეთ "<annotation icon="home_icon">" მთავარ ღილაკს "</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"მენიუ „ეკრანი ეკრანში“."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"მარცხნივ გადატანა"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"მარჯვნივ გადატანა"</string>
diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml
index 02dd33085133..7c9120e22b30 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Параметрлер"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Бөлінген экранға кіру"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Mәзір"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"\"Сурет ішіндегі сурет\" мәзірі"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"суреттегі сурет\" режимінде"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> деген пайдаланушының бұл мүмкіндікті пайдалануын қаламасаңыз, параметрлерді түртіп ашыңыз да, оларды өшіріңіз."</string>
<string name="pip_play" msgid="3496151081459417097">"Ойнату"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Өлшемін өзгерту"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Жасыру"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Көрсету"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Қолданба экранды бөлу режимінде жұмыс істемеуі мүмкін."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Қодланба бөлінген экранды қолдамайды."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Қолданба экранды бөлу режимінде жұмыс істемеуі мүмкін."</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Қолданбада экранды бөлу мүмкін емес."</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Бұл қолданбаны тек 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="703810061635792791">"Бөлінген экран бөлгіші"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Экранды бөлу режимінің бөлгіші"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Экранды бөлу режимінің бөлгіші"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Сол жағын толық экранға шығару"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% сол жақта"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% сол жақта"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Көпіршік"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Басқару"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқыма хабар жабылды."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Түртсеңіз, қолданба жабылып, ыңғайлы көрініспен қайта ашылады."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Ыңғайлы көріністі реттеу үшін қолданбаны түртіп, өшіріп қосыңыз."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Камерада қателер шықты ма?\nЖөндеу үшін түртіңіз."</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Жөнделмеді ме?\nҚайтару үшін түртіңіз."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Камерада қателер шықпады ма? Жабу үшін түртіңіз."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Қосымша ақпаратты қарап, әрекеттер жасау"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Экранды бөлу үшін басқа қолданбаға сүйреңіз."</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Экранды бөлу үшін басқа қолданбаға өтіңіз."</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Қолданбаның орнын өзгерту үшін одан тыс жерді екі рет түртіңіз."</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Түсінікті"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Толығырақ ақпарат алу үшін терезені жайыңыз."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string>
<string name="close_text" msgid="4986518933445178928">"Жабу"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Мәзірді жабу"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Мәзірді ашу"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml
index cdf564fb4ca0..df5f6171b11b 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Жылжыту"</string>
<string name="pip_expand" msgid="1051966011679297308">"Жаю"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Жию"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Басқару элементтері: "<annotation icon="home_icon">" НЕГІЗГІ ЭКРАН "</annotation>" түймесін екі рет басыңыз."</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Басқару: "<annotation icon="home_icon">" НЕГІЗГІ ЭКРАН "</annotation>" түймесін екі рет басыңыз."</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"Сурет ішіндегі сурет\" мәзірі."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солға жылжыту"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңға жылжыту"</string>
diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml
index 1be2465d7d9c..45302677a043 100644
--- a/libs/WindowManager/Shell/res/values-km/strings.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ការកំណត់"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"ចូលមុខងារ​បំបែកអេក្រង់"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"ម៉ឺនុយ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ម៉ឺនុយ​រូប​ក្នុងរូប"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ស្ថិតក្នុងមុខងាររូបក្នុងរូប"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"ប្រសិនបើ​អ្នក​មិន​ចង់​ឲ្យ <xliff:g id="NAME">%s</xliff:g> ប្រើ​មុខងារ​នេះ​ សូមចុច​​បើក​ការកំណត់ រួច​បិទ​វា។"</string>
<string name="pip_play" msgid="3496151081459417097">"លេង"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ប្ដូរ​ទំហំ"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"លាក់ជាបណ្ដោះអាសន្ន"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ឈប់លាក់ជាបណ្ដោះអាសន្ន"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"កម្មវិធី​អាចនឹងមិន​ដំណើរការ​ជាមួយ​មុខងារបំបែកអេក្រង់​ទេ។"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"កម្មវិធីមិនគាំទ្រអេក្រង់បំបែកជាពីរទេ"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"កម្មវិធី​អាចមិន​ដំណើរការ​ជាមួយ​មុខងារបំបែកអេក្រង់​ទេ"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"កម្មវិធីមិនអាចប្រើមុខងារ​បំបែកអេក្រង់បានទេ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"កម្មវិធីនេះអាចបើកនៅក្នុងវិនដូតែ 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="703810061635792791">"កម្មវិធីចែកអេក្រង់បំបែក"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"បន្ទាត់ខណ្ឌចែកក្នុងមុខងារ​បំបែកអេក្រង់"</string>
+ <string name="divider_title" msgid="1963391955593749442">"បន្ទាត់ខណ្ឌចែកក្នុងមុខងារ​បំបែកអេក្រង់"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"អេក្រង់ពេញខាងឆ្វេង"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ឆ្វេង 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ឆ្វេង 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"មិនបាន​ដោះស្រាយ​បញ្ហានេះទេឬ?\nចុចដើម្បី​ត្រឡប់"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"មិនមាន​បញ្ហាពាក់ព័ន្ធនឹង​កាមេរ៉ាទេឬ? ចុចដើម្បី​ច្រានចោល។"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"មើលឃើញ និងធ្វើបានកាន់តែច្រើន"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"អូស​កម្មវិធី​មួយ​ទៀត​ចូល ដើម្បី​ប្រើ​មុខងារ​បំបែកអេក្រង់"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"អូស​កម្មវិធី​មួយ​ទៀត​ចូល ដើម្បី​ប្រើ​មុខងារ​បំបែកអេក្រង់"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ចុចពីរដង​នៅ​ក្រៅ​កម្មវិធី ដើម្បី​ប្ដូរ​ទីតាំង​កម្មវិធី​នោះ"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"យល់ហើយ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ពង្រីកដើម្បីទទួលបានព័ត៌មានបន្ថែម។"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"រូបថតអេក្រង់"</string>
<string name="close_text" msgid="4986518933445178928">"បិទ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"បិទ​ម៉ឺនុយ"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"បើកម៉ឺនុយ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-km/strings_tv.xml b/libs/WindowManager/Shell/res/values-km/strings_tv.xml
index 1a7ae813c1d3..a3c7e22f268a 100644
--- a/libs/WindowManager/Shell/res/values-km/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ផ្លាស់ទី"</string>
<string name="pip_expand" msgid="1051966011679297308">"ពង្រីក"</string>
<string name="pip_collapse" msgid="3903295106641385962">"បង្រួម"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ចុចពីរដងលើ"<annotation icon="home_icon">"ប៊ូតុងដើម"</annotation>" ដើម្បីបើកផ្ទាំងគ្រប់គ្រង"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"ចុចពីរដងលើ"<annotation icon="home_icon">"ប៊ូតុងដើម"</annotation>" ដើម្បីបើកផ្ទាំងគ្រប់គ្រង"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ម៉ឺនុយ​រូប​ក្នុងរូប"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ផ្លាស់ទី​ទៅ​ឆ្វេង"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ផ្លាស់ទីទៅ​ស្តាំ"</string>
diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml
index 106053172a03..2dfbad49bb06 100644
--- a/libs/WindowManager/Shell/res/values-kn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kn/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"ಸ್ಪ್ಲಿಟ್‌-ಸ್ಕ್ರೀನ್‌ಗೆ ಪ್ರವೇಶಿಸಿ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"ಮೆನು"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ ಮೆನು"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರವಾಗಿದೆ"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ಈ ಫೀಚರ್ ಬಳಸುವುದನ್ನು ನೀವು ಬಯಸದಿದ್ದರೆ, ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ತೆರೆಯಲು ಮತ್ತು ಅದನ್ನು ಆಫ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
<string name="pip_play" msgid="3496151081459417097">"ಪ್ಲೇ"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ಮರುಗಾತ್ರಗೊಳಿಸಿ"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ಅನ್‌ಸ್ಟ್ಯಾಶ್ ಮಾಡಿ"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ವಿಭಜಿಸಿದ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿ ಆ್ಯಪ್ ಕೆಲಸ ಮಾಡದೇ ಇರಬಹುದು."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ಅಪ್ಲಿಕೇಶನ್ ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಬೆಂಬಲಿಸುವುದಿಲ್ಲ."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್‌ನಲ್ಲಿ ಆ್ಯಪ್ ಕೆಲಸ ಮಾಡದೇ ಇರಬಹುದು"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್‌ ಅನ್ನು ಆ್ಯಪ್ ಬೆಂಬಲಿಸುವುದಿಲ್ಲ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ಈ ಆ್ಯಪ್ ಅನ್ನು 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="703810061635792791">"ಸ್ಪ್ಲಿಟ್-ಪರದೆ ಡಿವೈಡರ್"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್ ಡಿವೈಡರ್"</string>
+ <string name="divider_title" msgid="1963391955593749442">"ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್ ಡಿವೈಡರ್"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ಎಡ ಪೂರ್ಣ ಪರದೆ"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% ಎಡಕ್ಕೆ"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% ಎಡಕ್ಕೆ"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ಅದನ್ನು ಸರಿಪಡಿಸಲಿಲ್ಲವೇ?\nಹಿಂತಿರುಗಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ಕ್ಯಾಮರಾ ಸಮಸ್ಯೆಗಳಿಲ್ಲವೇ? ವಜಾಗೊಳಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ನೋಡಿ ಮತ್ತು ಹೆಚ್ಚಿನದನ್ನು ಮಾಡಿ"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್‌ಗಾಗಿ ಮತ್ತೊಂದು ಆ್ಯಪ್‌ನಲ್ಲಿ ಎಳೆಯಿರಿ"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್‌ಗಾಗಿ ಮತ್ತೊಂದು ಆ್ಯಪ್‌ನಲ್ಲಿ ಡ್ರ್ಯಾಗ್ ಮಾಡಿ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ಆ್ಯಪ್ ಒಂದರ ಸ್ಥಾನವನ್ನು ಬದಲಾಯಿಸಲು ಅದರ ಹೊರಗೆ ಡಬಲ್-ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ಸರಿ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ಇನ್ನಷ್ಟು ಮಾಹಿತಿಗಾಗಿ ವಿಸ್ತೃತಗೊಳಿಸಿ."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ಸ್ಕ್ರೀನ್‌ಶಾಟ್"</string>
<string name="close_text" msgid="4986518933445178928">"ಮುಚ್ಚಿ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ಮೆನು ಮುಚ್ಚಿ"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"ಮೆನು ತೆರೆಯಿರಿ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml
index 45de068c80a0..3dfe573a6506 100644
--- a/libs/WindowManager/Shell/res/values-kn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-kn/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ಸರಿಸಿ"</string>
<string name="pip_expand" msgid="1051966011679297308">"ವಿಸ್ತೃತಗೊಳಿಸಿ"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ಕುಗ್ಗಿಸಿ"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ಕಂಟ್ರೋಲ್‌ಗಳಿಗಾಗಿ "<annotation icon="home_icon">" ಹೋಮ್ "</annotation>" ಅನ್ನು ಎರಡು ಬಾರಿ ಒತ್ತಿ"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"ಕಂಟ್ರೋಲ್‌ಗಳಿಗಾಗಿ "<annotation icon="home_icon">"ಹೋಮ್"</annotation>" ಅನ್ನು ಎರಡು ಬಾರಿ ಒತ್ತಿರಿ"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ಚಿತ್ರದಲ್ಲಿ ಚಿತ್ರ ಮೆನು."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ಎಡಕ್ಕೆ ಸರಿಸಿ"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ಬಲಕ್ಕೆ ಸರಿಸಿ"</string>
diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml
index d82577b37d80..39d717dd461a 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"설정"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"화면 분할 모드로 전환"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"메뉴"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"PIP 모드 메뉴"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>에서 PIP 사용 중"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>에서 이 기능이 사용되는 것을 원하지 않는 경우 탭하여 설정을 열고 기능을 사용 중지하세요."</string>
<string name="pip_play" msgid="3496151081459417097">"재생"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"크기 조절"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"숨기기"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"숨기기 취소"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"앱이 분할 화면에서 작동하지 않을 수 있습니다."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"앱이 화면 분할을 지원하지 않습니다."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"앱이 화면 분할 모드로는 작동하지 않을 수 있습니다"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"앱이 화면 분할을 지원하지 않습니다"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"이 앱은 창 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="703810061635792791">"화면 분할기"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"화면 분할기"</string>
+ <string name="divider_title" msgid="1963391955593749442">"화면 분할기"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"왼쪽 화면 전체화면"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"왼쪽 화면 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"왼쪽 화면 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"해결되지 않았나요?\n되돌리려면 탭하세요."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"카메라에 문제가 없나요? 닫으려면 탭하세요."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"더 많은 정보를 보고 더 많은 작업을 처리하세요"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"화면 분할을 사용하려면 다른 앱을 드래그해 가져옵니다."</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"화면 분할을 사용하려면 다른 앱을 드래그해 가져옵니다."</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"앱 위치를 조정하려면 앱 외부를 두 번 탭합니다."</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"확인"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"추가 정보는 펼쳐서 확인하세요."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"스크린샷"</string>
<string name="close_text" msgid="4986518933445178928">"닫기"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"메뉴 닫기"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"메뉴 열기"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml
index 9e8f1f1258a5..969a68d0346e 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"이동"</string>
<string name="pip_expand" msgid="1051966011679297308">"펼치기"</string>
<string name="pip_collapse" msgid="3903295106641385962">"접기"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" 제어 메뉴에 액세스하려면 "<annotation icon="home_icon">" 홈 "</annotation>"을 두 번 누르세요."</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"제어 메뉴에 액세스하려면 "<annotation icon="home_icon">"홈"</annotation>"을 두 번 누르세요."</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"PIP 모드 메뉴입니다."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"왼쪽으로 이동"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"오른쪽으로 이동"</string>
diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml
index 2fe1c403402d..f210ea29da00 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ky/strings.xml
@@ -22,8 +22,9 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Параметрлер"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Экранды бөлүү режимине өтүү"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Сүрөт ичиндеги сүрөт менюсу"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> – сүрөт ичиндеги сүрөт"</string>
- <string name="pip_notification_message" msgid="8854051911700302620">"Эгер <xliff:g id="NAME">%s</xliff:g> колдонмосу бул функцияны пайдаланбасын десеңиз, параметрлерди ачып туруп, аны өчүрүп коюңуз."</string>
+ <string name="pip_notification_message" msgid="8854051911700302620">"Эгер <xliff:g id="NAME">%s</xliff:g> колдонмосу бул функцияны пайдаланбасын десеңиз, жөндөөлөрдү ачып туруп, аны өчүрүп коюңуз."</string>
<string name="pip_play" msgid="3496151081459417097">"Ойнотуу"</string>
<string name="pip_pause" msgid="690688849510295232">"Тындыруу"</string>
<string name="pip_skip_to_next" msgid="8403429188794867653">"Кийинкисине өткөрүп жиберүү"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Өлчөмүн өзгөртүү"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Сейфке салуу"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Сейфтен чыгаруу"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Колдонмодо экран бөлүнбөшү мүмкүн."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Колдонмодо экран бөлүнбөйт."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Колдонмодо экран бөлүнбөшү мүмкүн"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Колдонмодо экран бөлүнбөйт"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Бул колдонмону 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="703810061635792791">"Экранды бөлгүч"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Экранды бөлгүч"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Экранды бөлгүч"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Сол жактагы экранды толук экран режимине өткөрүү"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Сол жактагы экранды 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Сол жактагы экранды 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Оңдолгон жокпу?\nАртка кайтаруу үчүн таптаңыз"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Камерада маселе жокпу? Этибарга албоо үчүн таптаңыз."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Көрүп, көбүрөөк нерселерди жасаңыз"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Экранды бөлүү үчүн башка колдонмону сүйрөңүз"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Экранды бөлүү үчүн башка колдонмону сүйрөңүз"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Колдонмону жылдыруу үчүн сырт жагын эки жолу таптаңыз"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Түшүндүм"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Толук маалымат алуу үчүн жайып көрүңүз."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string>
<string name="close_text" msgid="4986518933445178928">"Жабуу"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Менюну жабуу"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Менюну ачуу"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml
index 19fac5876bb0..68262e521f4a 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ky/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Жылдыруу"</string>
<string name="pip_expand" msgid="1051966011679297308">"Жайып көрсөтүү"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Жыйыштыруу"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Башкаруу элементтерин ачуу үчүн "<annotation icon="home_icon">" БАШКЫ БЕТ "</annotation>" баскычын эки жолу басыңыз"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Башкаруу элементтерин ачуу үчүн "<annotation icon="home_icon">" БАШКЫ БЕТ "</annotation>" баскычын эки жолу басыңыз"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Сүрөт ичиндеги сүрөт менюсу."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Солго жылдыруу"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Оңго жылдыруу"</string>
diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml
index f9501795be7f..a25699fa4d10 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ການຕັ້ງຄ່າ"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"ເຂົ້າການແບ່ງໜ້າຈໍ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"ເມນູ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ເມນູການສະແດງຜົນຊ້ອນກັນ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ແມ່ນເປັນການສະແດງຜົນຫຼາຍຢ່າງພ້ອມກັນ"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"ຫາກທ່ານບໍ່ຕ້ອງການ <xliff:g id="NAME">%s</xliff:g> ໃຫ້ໃຊ້ຄຸນສົມບັດນີ້, ໃຫ້ແຕະເພື່ອເປີດການຕັ້ງຄ່າ ແລ້ວປິດມັນໄວ້."</string>
<string name="pip_play" msgid="3496151081459417097">"ຫຼິ້ນ"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ປ່ຽນຂະໜາດ"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ເກັບໄວ້ບ່ອນເກັບສ່ວນຕົວ"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ເອົາອອກຈາກບ່ອນເກັບສ່ວນຕົວ"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ແອັບອາດໃຊ້ບໍ່ໄດ້ກັບການແບ່ງໜ້າຈໍ."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ແອັບບໍ່ຮອງຮັບໜ້າຈໍແບບແຍກກັນ."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"ແອັບອາດໃຊ້ບໍ່ໄດ້ກັບໂໝດແບ່ງໜ້າຈໍ"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ແອັບບໍ່ຮອງຮັບການແບ່ງໜ້າຈໍ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ແອັບນີ້ສາມາດເປີດໄດ້ໃນ 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="703810061635792791">"ຕົວຂັ້ນການແບ່ງໜ້າຈໍ"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"ເສັ້ນແບ່ງໜ້າຈໍ"</string>
+ <string name="divider_title" msgid="1963391955593749442">"ເສັ້ນແບ່ງໜ້າຈໍ"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ເຕັມໜ້າຈໍຊ້າຍ"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ຊ້າຍ 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ຊ້າຍ 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ບໍ່ໄດ້ແກ້ໄຂມັນບໍ?\nແຕະເພື່ອແປງກັບຄືນ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ບໍ່ມີບັນຫາກ້ອງຖ່າຍຮູບບໍ? ແຕະເພື່ອ​ປິດ​ໄວ້."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ເບິ່ງ ແລະ ເຮັດຫຼາຍຂຶ້ນ"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"ລາກແອັບອື່ນເຂົ້າມາເພື່ອແບ່ງໜ້າຈໍ"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ລາກໄປໄວ້ໃນແອັບອື່ນເພື່ອແບ່ງໜ້າຈໍ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ແຕະສອງເທື່ອໃສ່ນອກແອັບໃດໜຶ່ງເພື່ອຈັດຕຳແໜ່ງຂອງມັນຄືນໃໝ່"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ເຂົ້າໃຈແລ້ວ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ຂະຫຍາຍເພື່ອເບິ່ງຂໍ້ມູນເພີ່ມເຕີມ."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ຮູບໜ້າຈໍ"</string>
<string name="close_text" msgid="4986518933445178928">"ປິດ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ປິດເມນູ"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"ເປີດເມນູ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml
index 6cd0f37c516c..b84c83555ea0 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ຍ້າຍ"</string>
<string name="pip_expand" msgid="1051966011679297308">"ຂະຫຍາຍ"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ຫຍໍ້"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ກົດ "<annotation icon="home_icon">" HOME "</annotation>" ສອງເທື່ອສຳລັບການຄວບຄຸມ"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"ກົດ "<annotation icon="home_icon">"HOME"</annotation>" ສອງເທື່ອສຳລັບການຄວບຄຸມ"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ເມນູການສະແດງຜົນຊ້ອນກັນ."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ຍ້າຍໄປຊ້າຍ"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ຍ້າຍໄປຂວາ"</string>
diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml
index 1b0c64d9a719..d893fcf21d46 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Nustatymai"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Įjungti išskaidyto ekrano režimą"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Vaizdo vaizde meniu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> rodom. vaizdo vaizde"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Jei nenorite, kad „<xliff:g id="NAME">%s</xliff:g>“ naudotų šią funkciją, palietę atidarykite nustatymus ir išjunkite ją."</string>
<string name="pip_play" msgid="3496151081459417097">"Leisti"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Pakeisti dydį"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Paslėpti"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Nebeslėpti"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Programa gali neveikti naudojant išskaidyto ekrano režimą."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programoje nepalaikomas skaidytas ekranas."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Š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="703810061635792791">"Skaidyto ekrano daliklis"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Išskaidyto ekrano režimo daliklis"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Išskaidyto ekrano režimo daliklis"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Kairysis ekranas viso ekrano režimu"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kairysis ekranas 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kairysis ekranas 50 %"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nepavyko pataisyti?\nPalieskite, kad grąžintumėte"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nėra jokių problemų dėl kameros? Palieskite, kad atsisakytumėte."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Daugiau turinio ir funkcijų"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Vilkite kitoje programoje, kad galėtumėte naudoti išskaidyto ekrano režimą"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Vilkite kitoje programoje, kad galėtumėte naudoti išskaidyto ekrano režimą"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dukart palieskite už programos ribų, kad pakeistumėte jos poziciją"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Supratau"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Išskleiskite, jei reikia daugiau informacijos."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Ekrano kopija"</string>
<string name="close_text" msgid="4986518933445178928">"Uždaryti"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Uždaryti meniu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Atidaryti meniu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml
index 52017dca2b94..0537553cc36a 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Perkelti"</string>
<string name="pip_expand" msgid="1051966011679297308">"Išskleisti"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Sutraukti"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Jei reikia valdiklių, dukart paspauskite "<annotation icon="home_icon">"PAGRINDINIS"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Jei reikia valdiklių, dukart pasp. "<annotation icon="home_icon">"PAGRINDINIS"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Vaizdo vaizde meniu."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Perkelti kairėn"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Perkelti dešinėn"</string>
diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml
index 0cb63954f495..a1fbcddb1e3a 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Iestatījumi"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Piekļūt ekrāna sadalīšanas režīmam"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Izvēlne"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Izvēlne attēlam attēlā"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ir attēlā attēlā"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ja nevēlaties lietotnē <xliff:g id="NAME">%s</xliff:g> izmantot šo funkciju, pieskarieties, lai atvērtu iestatījumus un izslēgtu funkciju."</string>
<string name="pip_play" msgid="3496151081459417097">"Atskaņot"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Mainīt lielumu"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Paslēpt"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Rādīt"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Iespējams, lietotne nedarbosies ekrāna sadalīšanas režīmā."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Lietotnē netiek atbalstīta ekrāna sadalīšana."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Š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="703810061635792791">"Ekrāna sadalītājs"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Ekrāna sadalītājs"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Ekrāna sadalītājs"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Kreisā daļa pa visu ekrānu"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Pa kreisi 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pa kreisi 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Vai problēma netika novērsta?\nPieskarieties, lai atjaunotu."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Vai nav problēmu ar kameru? Pieskarieties, lai nerādītu."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Uzziniet un paveiciet vairāk"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Lai izmantotu sadalītu ekrānu, ievelciet vēl vienu lietotni"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Lai izmantotu sadalītu ekrānu, ievelciet vēl vienu lietotni"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Lai pārvietotu lietotni, veiciet dubultskārienu ārpus lietotnes"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Labi"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Izvērsiet, lai iegūtu plašāku informāciju."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Ekrānuzņēmums"</string>
<string name="close_text" msgid="4986518933445178928">"Aizvērt"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Aizvērt izvēlni"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Atvērt izvēlni"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml
index 11abac6f6197..13baa9bc46eb 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Pārvietot"</string>
<string name="pip_expand" msgid="1051966011679297308">"Izvērst"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Sakļaut"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Atvērt vadīklas: divreiz nospiediet pogu "<annotation icon="home_icon">"SĀKUMS"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Atvērt vadīklas: divreiz nospiediet pogu "<annotation icon="home_icon">"SĀKUMS"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Izvēlne attēlam attēlā."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Pārvietot pa kreisi"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Pārvietot pa labi"</string>
diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml
index a21e50281262..427433c7f2d4 100644
--- a/libs/WindowManager/Shell/res/values-mk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mk/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Поставки"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Влези во поделен екран"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Мени за „Слика во слика“"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> е во слика во слика"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ако не сакате <xliff:g id="NAME">%s</xliff:g> да ја користи функцијава, допрете за да ги отворите поставките и да ја исклучите."</string>
<string name="pip_play" msgid="3496151081459417097">"Пушти"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Промени големина"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Сокријте"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Прикажете"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликацијата може да не работи со поделен екран."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликацијата не поддржува поделен екран."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Апликацијата можеби нема да работи со поделен екран"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Апликацијата не поддржува поделен екран"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Апликацијава може да се отвори само во еден прозорец."</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="703810061635792791">"Разделник на поделен екран"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Разделник на поделен екран"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Разделник на поделен екран"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левиот на цел екран"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левиот 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левиот 50%"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Управувајте"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отфрлено."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"За подобар приказ, допрете за да ја рестартирате апликацијава."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Допрете за да ја рестартирате апликацијава за подобар приказ."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Проблеми со камерата?\nДопрете за да се совпадне повторно"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Не се поправи?\nДопрете за враќање"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Нема проблеми со камерата? Допрете за отфрлање."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Погледнете и направете повеќе"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Повлечете во друга апликација за поделен екран"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Повлечете друга апликација за поделен екран"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Допрете двапати надвор од некоја апликација за да ја преместите"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Сфатив"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Проширете за повеќе информации."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Слика од екранот"</string>
<string name="close_text" msgid="4986518933445178928">"Затворете"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Отвори го менито"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml
index 21293223b882..d7a9516bea7f 100644
--- a/libs/WindowManager/Shell/res/values-mk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-mk/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Премести"</string>
<string name="pip_expand" msgid="1051966011679297308">"Прошири"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Собери"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Притиснете двапати на "<annotation icon="home_icon">" HOME "</annotation>" за контроли"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Притиснете двапати на "<annotation icon="home_icon">"HOME"</annotation>" за контроли"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени за „Слика во слика“."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Премести налево"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Премести надесно"</string>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml
index 5ef137a0fcd8..5cca248e0dd4 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ക്രമീകരണം"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"സ്ക്രീൻ വിഭജന മോഡിൽ പ്രവേശിക്കുക"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"മെനു"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ചിത്രത്തിനുള്ളിൽ ചിത്രം മെനു"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ചിത്രത്തിനുള്ളിൽ ചിത്രം രീതിയിലാണ്"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ഈ ഫീച്ചർ ഉപയോഗിക്കേണ്ടെങ്കിൽ, ടാപ്പ് ചെയ്‌ത് ക്രമീകരണം തുറന്ന് അത് ഓഫാക്കുക."</string>
<string name="pip_play" msgid="3496151081459417097">"പ്ലേ ചെയ്യുക"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"വലുപ്പം മാറ്റുക"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"സ്റ്റാഷ് ചെയ്യൽ"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"അൺസ്റ്റാഷ് ചെയ്യൽ"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"സ്‌ക്രീൻ വിഭജന മോഡിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"സ്പ്ലിറ്റ്-സ്ക്രീനിനെ ആപ്പ് പിന്തുണയ്ക്കുന്നില്ല."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"സ്‌ക്രീൻ വിഭജന മോഡിൽ ആപ്പ് പ്രവർത്തിച്ചേക്കില്ല"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"സ്‌ക്രീൻ വിഭജന മോഡിനെ ആപ്പ് പിന്തുണയ്ക്കുന്നില്ല"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ഈ ആപ്പ് ഒരു വിൻഡോയിൽ മാത്രമേ തുറക്കാനാകൂ."</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="703810061635792791">"സ്പ്ലിറ്റ്-സ്ക്രീൻ ഡിവൈഡർ"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"സ്‌ക്രീൻ വിഭജന മോഡ് ഡിവൈഡർ"</string>
+ <string name="divider_title" msgid="1963391955593749442">"സ്‌ക്രീൻ വിഭജന മോഡ് ഡിവൈഡർ"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ഇടത് പൂർണ്ണ സ്ക്രീൻ"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ഇടത് 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ഇടത് 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"അത് പരിഹരിച്ചില്ലേ?\nപുനഃസ്ഥാപിക്കാൻ ടാപ്പ് ചെയ്യുക"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ക്യാമറാ പ്രശ്നങ്ങളൊന്നുമില്ലേ? നിരസിക്കാൻ ടാപ്പ് ചെയ്യുക."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"കൂടുതൽ കാണുക, ചെയ്യുക"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"സ്‌ക്രീൻ വിഭജന മോഡിന്, മറ്റൊരു ആപ്പ് വലിച്ചിടുക"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"സ്‌ക്രീൻ വിഭജന മോഡിന്, മറ്റൊരു ആപ്പ് വലിച്ചിടുക"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ആപ്പിന്റെ സ്ഥാനം മാറ്റാൻ അതിന് പുറത്ത് ഡബിൾ ടാപ്പ് ചെയ്യുക"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"മനസ്സിലായി"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"കൂടുതൽ വിവരങ്ങൾക്ക് വികസിപ്പിക്കുക."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"സ്ക്രീൻഷോട്ട്"</string>
<string name="close_text" msgid="4986518933445178928">"അടയ്ക്കുക"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"മെനു അടയ്ക്കുക"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"മെനു തുറക്കുക"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml
index 549e39b21101..56f2b196421b 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"നീക്കുക"</string>
<string name="pip_expand" msgid="1051966011679297308">"വികസിപ്പിക്കുക"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ചുരുക്കുക"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" നിയന്ത്രണങ്ങൾക്കായി "<annotation icon="home_icon">" ഹോം "</annotation>" രണ്ട് തവണ അമർത്തുക"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"നിയന്ത്രണങ്ങൾക്കായി "<annotation icon="home_icon">"ഹോം "</annotation>" രണ്ട് തവണ അമർത്തുക"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ചിത്രത്തിനുള്ളിൽ ചിത്രം മെനു."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ഇടത്തേക്ക് നീക്കുക"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"വലത്തേക്ക് നീക്കുക"</string>
diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml
index ecb65d11e56a..72e54fcf8711 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mn/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Тохиргоо"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Хуваасан дэлгэцийг оруулна уу"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Цэс"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Дэлгэц доторх дэлгэцийн цэс"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> дэлгэцэн доторх дэлгэцэд байна"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Та <xliff:g id="NAME">%s</xliff:g>-д энэ онцлогийг ашиглуулахыг хүсэхгүй байвал тохиргоог нээгээд, үүнийг унтраана уу."</string>
<string name="pip_play" msgid="3496151081459417097">"Тоглуулах"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Хэмжээг өөрчлөх"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Нуух"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Ил гаргах"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Апп хуваагдсан дэлгэц дээр ажиллахгүй байж болзошгүй."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Энэ апп нь дэлгэц хуваах тохиргоог дэмждэггүй."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Апп дэлгэцийг хуваах горимтой ажиллахгүй байж магадгүй"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Апп дэлгэцийг хуваах горимыг дэмждэггүй"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Энэ аппыг зөвхөн 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="703810061635792791">"\"Дэлгэц хуваах\" хуваагч"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Дэлгэцийг хуваах хуваагч"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Дэлгэцийг хуваах хуваагч"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Зүүн талын бүтэн дэлгэц"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Зүүн 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Зүүн 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Үүнийг засаагүй юу?\nБуцаахын тулд товшино уу"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Камерын асуудал байхгүй юу? Хаахын тулд товшино уу."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Харж илүү ихийг хий"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Дэлгэцийг хуваахын тулд өөр апп руу чирнэ үү"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Дэлгэц хуваах горимд ашиглахын тулд өөр аппыг чирнэ үү"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Аппыг дахин байрлуулахын тулд гадна талд нь хоёр товшино"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Ойлголоо"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Нэмэлт мэдээлэл авах бол дэлгэнэ үү."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Дэлгэцийн агшин"</string>
<string name="close_text" msgid="4986518933445178928">"Хаах"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Цэсийг хаах"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Цэс нээх"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml
index 9a85d96ca602..0e6dcca17e38 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-mn/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Зөөх"</string>
<string name="pip_expand" msgid="1051966011679297308">"Дэлгэх"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Хураах"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Хяналтад хандах бол "<annotation icon="home_icon">" HOME "</annotation>" дээр хоёр дарна уу"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Хяналтад хандах бол "<annotation icon="home_icon">"HOME"</annotation>" дээр хоёр дарна уу"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Дэлгэцэн доторх дэлгэцийн цэс."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Зүүн тийш зөөх"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Баруун тийш зөөх"</string>
diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml
index dba4d57e8dd8..a9e6319a2110 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग्ज"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन एंटर करा"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"मेनू"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"चित्रात-चित्र मेनू"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> चित्रामध्ये चित्र मध्ये आहे"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>ने हे वैशिष्ट्य वापरू नये असे तुम्हाला वाटत असल्यास, सेटिंग्ज उघडण्यासाठी टॅप करा आणि ते बंद करा."</string>
<string name="pip_play" msgid="3496151081459417097">"प्ले करा"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदला"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"स्टॅश करा"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्टॅश करा"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"अ‍ॅप कदाचित स्प्लिट स्क्रीनसह काम करू शकत नाही."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अ‍ॅप स्क्रीन-विभाजनास समर्थन देत नाही."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"अ‍ॅप कदाचित स्प्लिट स्क्रीनसह काम करणार नाही"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"अ‍ॅप हे स्प्लिट स्क्रीनला सपोर्ट करत नाही"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"हे अ‍ॅप फक्त एका विंडोमध्ये उघडले जाऊ शकते."</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="703810061635792791">"विभाजित-स्क्रीन विभाजक"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"स्प्लिट स्क्रीन विभाजक"</string>
+ <string name="divider_title" msgid="1963391955593749442">"स्प्लिट स्क्रीन विभाजक"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"डावी फुल स्क्रीन"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"डावी 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"डावी 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"निराकरण झाले नाही?\nरिव्हर्ट करण्यासाठी कृपया टॅप करा"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"कॅमेराशी संबंधित कोणत्याही समस्या नाहीत का? डिसमिस करण्‍यासाठी टॅप करा."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"पहा आणि आणखी बरेच काही करा"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"स्प्लिट-स्क्रीन वापरण्यासाठी दुसऱ्या ॲपमध्ये ड्रॅग करा"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"स्प्लिट स्क्रीन वापरण्यासाठी दुसरे ॲप ड्रॅग करा"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ॲपची स्थिती पुन्हा बदलण्यासाठी, त्याच्या बाहेर दोनदा टॅप करा"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"समजले"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"अधिक माहितीसाठी विस्तार करा."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"स्क्रीनशॉट"</string>
<string name="close_text" msgid="4986518933445178928">"बंद करा"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"मेनू बंद करा"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"मेनू उघडा"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml
index 660212a66935..89654d0a5750 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"हलवा"</string>
<string name="pip_expand" msgid="1051966011679297308">"विस्तार करा"</string>
<string name="pip_collapse" msgid="3903295106641385962">"कोलॅप्स करा"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" नियंत्रणांसाठी "<annotation icon="home_icon">" होम "</annotation>" दोनदा प्रेस करा"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"नियंत्रणांसाठी "<annotation icon="home_icon">"होम"</annotation>" दोनदा प्रेस करा"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"चित्रात-चित्र मेनू."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"डावीकडे हलवा"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"उजवीकडे हलवा"</string>
diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml
index d0e9f66f6c23..b47531711863 100644
--- a/libs/WindowManager/Shell/res/values-ms/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ms/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Tetapan"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk skrin pisah"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu Gambar dalam Gambar"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> terdapat dalam gambar dalam gambar"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Jika anda tidak mahu <xliff:g id="NAME">%s</xliff:g> menggunakan ciri ini, ketik untuk membuka tetapan dan matikan ciri."</string>
<string name="pip_play" msgid="3496151081459417097">"Main"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ubah saiz"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Sembunyikan"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Tunjukkan"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Apl mungkin tidak berfungsi dengan skrin pisah."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Apl tidak menyokong skrin pisah."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Pembahagi skrin pisah"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Pembahagi skrin pisah"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Pembahagi skrin pisah"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Skrin penuh kiri"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kiri 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Isu tidak dibetulkan?\nKetik untuk kembali"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Tiada isu kamera? Ketik untuk mengetepikan."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Lihat dan lakukan lebih"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Seret apl lain untuk skrin pisah"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Seret masuk apl lain untuk menggunakan skrin pisah"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Ketik dua kali di luar apl untuk menempatkan semula apl itu"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Kembangkan untuk mendapatkan maklumat lanjut."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Tangkapan skrin"</string>
<string name="close_text" msgid="4986518933445178928">"Tutup"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml
index 8fe992d9f3b9..afea48d7b510 100644
--- a/libs/WindowManager/Shell/res/values-ms/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ms/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Alih"</string>
<string name="pip_expand" msgid="1051966011679297308">"Kembangkan"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Kuncupkan"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Tekan dua kali "<annotation icon="home_icon">" LAMAN UTAMA "</annotation>" untuk mengakses kawalan"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Tekan dua kali "<annotation icon="home_icon">"LAMAN UTAMA"</annotation>" untuk mengakses kawalan"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu Gambar dalam Gambar."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Alih ke kiri"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Alih ke kanan"</string>
diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml
index 8618c27e8085..cb6a1df95b33 100644
--- a/libs/WindowManager/Shell/res/values-my/strings.xml
+++ b/libs/WindowManager/Shell/res/values-my/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ဆက်တင်များ"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းသို့ ဝင်ရန်"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"မီနူး"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"နှစ်ခုထပ်၍ ကြည့်ခြင်းမီနူး"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> သည် နှစ်ခုထပ်၍ကြည့်ခြင်း ဖွင့်ထားသည်"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> အား ဤဝန်ဆောင်မှုကို အသုံးမပြုစေလိုလျှင် ဆက်တင်ကိုဖွင့်ရန် တို့ပြီး ၎င်းဝန်ဆောင်မှုကို ပိတ်လိုက်ပါ။"</string>
<string name="pip_play" msgid="3496151081459417097">"ဖွင့်ရန်"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"အရွယ်အစားပြောင်းရန်"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"သိုဝှက်ရန်"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"မသိုဝှက်ရန်"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းဖြင့် အက်ပ်သည် အလုပ်မလုပ်ပါ။"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"အက်ပ်သည် မျက်နှာပြင်ခွဲပြရန် ပံ့ပိုးထားခြင်းမရှိပါ။"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းဖြင့် အက်ပ်သည် အလုပ်မလုပ်ပါ"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"အက်ပ်တွင် မျက်နှာပြင် ခွဲ၍ပြသခြင်းကို မပံ့ပိုးပါ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ဤအက်ပ်ကို ဝင်းဒိုး ၁ ခုတွင်သာ ဖွင့်နိုင်သည်။"</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="703810061635792791">"မျက်နှာပြင်ခွဲခြမ်း ပိုင်းခြားပေးသည့်စနစ်"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"မျက်နှာပြင် ခွဲ၍ပြသခြင်း ပိုင်းခြားစနစ်"</string>
+ <string name="divider_title" msgid="1963391955593749442">"မျက်နှာပြင် ခွဲ၍ပြသခြင်း ပိုင်းခြားစနစ်"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ဘယ်ဘက် မျက်နှာပြင်အပြည့်"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ဘယ်ဘက်မျက်နှာပြင် ၇၀%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ဘယ်ဘက် မျက်နှာပြင် ၅၀%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ကောင်းမသွားဘူးလား။\nပြန်ပြောင်းရန် တို့ပါ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ကင်မရာပြဿနာ မရှိဘူးလား။ ပယ်ရန် တို့ပါ။"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ကြည့်ပြီး ပိုမိုလုပ်ဆောင်ပါ"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"မျက်နှာပြင် ခွဲ၍ပြသနိုင်ရန် နောက်အက်ပ်တစ်ခုကို ဖိဆွဲပါ"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းအတွက် အက်ပ်နောက်တစ်ခုကို ဖိဆွဲပါ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"နေရာပြန်ချရန် အက်ပ်အပြင်ဘက်ကို နှစ်ချက်တို့ပါ"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"နားလည်ပြီ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"နောက်ထပ်အချက်အလက်များအတွက် ချဲ့နိုင်သည်။"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ဖန်သားပြင်ဓာတ်ပုံ"</string>
<string name="close_text" msgid="4986518933445178928">"ပိတ်ရန်"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"မီနူး ပိတ်ရန်"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"မီနူး ဖွင့်ရန်"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-my/strings_tv.xml b/libs/WindowManager/Shell/res/values-my/strings_tv.xml
index 105628d8149e..f3ed65da43da 100644
--- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ရွှေ့ရန်"</string>
<string name="pip_expand" msgid="1051966011679297308">"ချဲ့ရန်"</string>
<string name="pip_collapse" msgid="3903295106641385962">"လျှော့ပြရန်"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ထိန်းချုပ်မှုအတွက် "<annotation icon="home_icon">" ပင်မခလုတ် "</annotation>" နှစ်ချက်နှိပ်ပါ"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"ထိန်းချုပ်မှုအတွက် "<annotation icon="home_icon">" ပင်မခလုတ် "</annotation>" ကို နှစ်ချက်နှိပ်ပါ"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"နှစ်ခုထပ်၍ ကြည့်ခြင်းမီနူး။"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ဘယ်သို့ရွှေ့ရန်"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ညာသို့ရွှေ့ရန်"</string>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml
index 8c319a279eaa..6c80144fee18 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Innstillinger"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivér delt skjerm"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Bilde-i-bilde-meny"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er i bilde-i-bilde"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke vil at <xliff:g id="NAME">%s</xliff:g> skal bruke denne funksjonen, kan du trykke for å åpne innstillingene og slå den av."</string>
<string name="pip_play" msgid="3496151081459417097">"Spill av"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Endre størrelse"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Oppbevar"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Avslutt oppbevaring"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Det kan hende at appen ikke fungerer med delt skjerm."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen støtter ikke delt skjerm."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Skilleelement for delt skjerm"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Skilleelement for delt skjerm"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Skilleelement for delt skjerm"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Utvid den venstre delen av skjermen til hele skjermen"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Sett størrelsen på den venstre delen av skjermen til 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sett størrelsen på den venstre delen av skjermen til 50 %"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ble ikke problemet løst?\nTrykk for å gå tilbake"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Har du ingen kameraproblemer? Trykk for å lukke."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Se og gjør mer"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Dra inn en annen app for å bruke delt skjerm"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Dra inn en annen app for å bruke delt skjerm"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dobbelttrykk utenfor en app for å flytte den"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Greit"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Vis for å få mer informasjon."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Skjermdump"</string>
<string name="close_text" msgid="4986518933445178928">"Lukk"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Lukk menyen"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Åpne menyen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml
index ca63518df7a5..1402e3c9c9bc 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Flytt"</string>
<string name="pip_expand" msgid="1051966011679297308">"Vis"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Skjul"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontroller"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Dobbelttrykk på "<annotation icon="home_icon">"HJEM"</annotation>" for å åpne kontrollene"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bilde-i-bilde-meny."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytt til venstre"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytt til høyre"</string>
diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml
index f3f18765e455..f9f580530f8b 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"सेटिङहरू"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रिन मोड प्रयोग गर्नुहोस्"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"मेनु"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"\"picture-in-picture\" मेनु"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> Picture-in-picture मा छ"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"तपाईं <xliff:g id="NAME">%s</xliff:g> ले सुविधा प्रयोग नगरोस् भन्ने चाहनुहुन्छ भने ट्याप गरेर सेटिङहरू खोल्नुहोस् र यसलाई निष्क्रिय पार्नुहोस्।"</string>
<string name="pip_play" msgid="3496151081459417097">"प्ले गर्नुहोस्"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"आकार बदल्नुहोस्"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"स्ट्यास गर्नुहोस्"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"अनस्ट्यास गर्नुहोस्"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"एप विभाजित स्क्रिनमा काम नगर्न सक्छ।"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"अनुप्रयोगले विभाजित-स्क्रिनलाई समर्थन गर्दैन।"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"यो एपले स्प्लिट स्क्रिन मोडमा काम नगर्न सक्छ"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"यो एप स्प्लिट स्क्रिन मोडमा प्रयोग गर्न मिल्दैन"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"यो एप एउटा विन्डोमा मात्र खोल्न मिल्छ।"</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="703810061635792791">"विभाजित-स्क्रिन छुट्याउने"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"स्प्लिट स्क्रिन डिभाइडर"</string>
+ <string name="divider_title" msgid="1963391955593749442">"स्प्लिट स्क्रिन डिभाइडर"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"बायाँ भाग फुल स्क्रिन"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"बायाँ भाग ७०%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बायाँ भाग ५०%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"समस्या हल भएन?\nपहिलेको जस्तै बनाउन ट्याप गर्नुहोस्"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"क्यामेरासम्बन्धी कुनै पनि समस्या छैन? खारेज गर्न ट्याप गर्नुहोस्।"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"थप कुरा हेर्नुहोस् र गर्नुहोस्"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"स्प्लिट स्क्रिन मोड प्रयोग गर्न अर्को एप ड्रयाग एन्ड ड्रप गर्नुहोस्"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"स्प्लिट स्क्रिन मोड प्रयोग गर्न अर्को एप ड्रयाग एन्ड ड्रप गर्नुहोस्"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"तपाईं जुन एपको स्थिति मिलाउन चाहनुहुन्छ सोही एपको बाहिर डबल ट्याप गर्नुहोस्"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"बुझेँ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"थप जानकारी प्राप्त गर्न चाहनुहुन्छ भने एक्स्पान्ड गर्नुहोस्।"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"स्क्रिनसट"</string>
<string name="close_text" msgid="4986518933445178928">"बन्द गर्नुहोस्"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"मेनु बन्द गर्नुहोस्"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"मेनु खोल्नुहोस्"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml
index 7cbf9e294e7b..2b1f20fba4e2 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"सार्नुहोस्"</string>
<string name="pip_expand" msgid="1051966011679297308">"एक्स्पान्ड गर्नुहोस्"</string>
<string name="pip_collapse" msgid="3903295106641385962">"कोल्याप्स गर्नुहोस्"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" कन्ट्रोल मेनु खोल्न "<annotation icon="home_icon">" होम "</annotation>" बटन दुई पटक थिच्नुहोस्"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"कन्ट्रोल मेनु खोल्न "<annotation icon="home_icon">" होम "</annotation>" बटन दुई पटक थिच्नुहोस्"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"\"picture-in-picture\" मेनु।"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"बायाँतिर सार्नुहोस्"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"दायाँतिर सार्नुहोस्"</string>
diff --git a/libs/WindowManager/Shell/res/values-night/colors.xml b/libs/WindowManager/Shell/res/values-night/colors.xml
index 5c6bb57a7f1c..83c4d93982f4 100644
--- a/libs/WindowManager/Shell/res/values-night/colors.xml
+++ b/libs/WindowManager/Shell/res/values-night/colors.xml
@@ -15,7 +15,6 @@
-->
<resources>
- <color name="docked_divider_handle">#ffffff</color>
<!-- Bubbles -->
<color name="bubbles_icon_tint">@color/GM2_grey_200</color>
<!-- Splash screen-->
diff --git a/libs/WindowManager/Shell/res/values-night/styles.xml b/libs/WindowManager/Shell/res/values-night/styles.xml
new file mode 100644
index 000000000000..4871f7ada627
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values-night/styles.xml
@@ -0,0 +1,33 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat">
+ <item name="android:focusable">false</item>
+ <item name="android:focusableInTouchMode">false</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:lineSpacingExtra">-1sp</item>
+ <item name="android:textSize">12sp</item>
+ <item name="android:textAlignment">center</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textAppearance">
+ @*android:style/TextAppearance.DeviceDefault.Body2
+ </item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml
index 75cd69e1cff0..3064cccba7cc 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Instellingen"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Gesplitst scherm openen"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Scherm-in-scherm-menu"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in scherm-in-scherm"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Als je niet wilt dat <xliff:g id="NAME">%s</xliff:g> deze functie gebruikt, tik je om de instellingen te openen en zet je de functie uit."</string>
<string name="pip_play" msgid="3496151081459417097">"Afspelen"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Formaat aanpassen"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Verbergen"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Niet meer verbergen"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"De app werkt mogelijk niet met gesplitst scherm."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"App biedt geen ondersteuning voor gesplitst scherm."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Deze app kan slechts 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="703810061635792791">"Scheiding voor gesplitst scherm"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Scheiding voor gesplitst scherm"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Scheiding voor gesplitst scherm"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Linkerscherm op volledig scherm"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Linkerscherm 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Linkerscherm 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Is dit geen oplossing?\nTik om terug te zetten."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Geen cameraproblemen? Tik om te sluiten."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Zie en doe meer"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Sleep een andere app hier naartoe om het scherm te splitsen"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Sleep een andere app hier naartoe om het scherm te splitsen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dubbeltik naast een app om deze opnieuw te positioneren"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Uitvouwen voor meer informatie."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Sluiten"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menu sluiten"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Menu openen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml
index 2deaeddc4080..6766773fa866 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Verplaatsen"</string>
<string name="pip_expand" msgid="1051966011679297308">"Uitvouwen"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Samenvouwen"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Druk twee keer op "<annotation icon="home_icon">" HOME "</annotation>" voor bedieningselementen"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Druk 2 keer op "<annotation icon="home_icon">"HOME"</annotation>" voor bedieningsopties"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Scherm-in-scherm-menu."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Naar links verplaatsen"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Naar rechts verplaatsen"</string>
diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml
index 4ffbf34ef61c..e4c70537c452 100644
--- a/libs/WindowManager/Shell/res/values-or/strings.xml
+++ b/libs/WindowManager/Shell/res/values-or/strings.xml
@@ -22,21 +22,23 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ସେଟିଂସ୍"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ମୋଡ ବ୍ୟବହାର କରନ୍ତୁ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"ମେନୁ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ପିକଚର-ଇନ-ପିକଚର ମେନୁ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> \"ଛବି-ଭିତରେ-ଛବି\"ରେ ଅଛି"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"ଏହି ବୈଶିଷ୍ଟ୍ୟ <xliff:g id="NAME">%s</xliff:g> ବ୍ୟବହାର ନକରିବାକୁ ଯଦି ଆପଣ ଚାହାଁନ୍ତି, ସେଟିଙ୍ଗ ଖୋଲିବାକୁ ଟାପ୍‍ କରନ୍ତୁ ଏବଂ ଏହା ଅଫ୍‍ କରିଦିଅନ୍ତୁ।"</string>
<string name="pip_play" msgid="3496151081459417097">"ପ୍ଲେ କରନ୍ତୁ"</string>
- <string name="pip_pause" msgid="690688849510295232">"ବିରତ କରନ୍ତୁ"</string>
+ <string name="pip_pause" msgid="690688849510295232">"ପଜ୍‍ କରନ୍ତୁ"</string>
<string name="pip_skip_to_next" msgid="8403429188794867653">"ପରବର୍ତ୍ତୀକୁ ଯାଆନ୍ତୁ"</string>
<string name="pip_skip_to_prev" msgid="7172158111196394092">"ପୂର୍ବବର୍ତ୍ତୀକୁ ଛାଡ଼ନ୍ତୁ"</string>
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ରିସାଇଜ୍ କରନ୍ତୁ"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ଲୁଚାନ୍ତୁ"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ଦେଖାନ୍ତୁ"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ସ୍ପ୍ଲିଟ୍-ସ୍କ୍ରିନରେ ଆପ୍ କାମ କରିନପାରେ।"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ଆପ୍‍ ସ୍ପ୍ଲିଟ୍‍-ସ୍କ୍ରୀନକୁ ସପୋର୍ଟ କରେ ନାହିଁ।"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନରେ ଆପ କାମ କରିନପାରେ"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନକୁ ଆପ ସମର୍ଥନ କରେ ନାହିଁ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ଏହି ଆପକୁ କେବଳ 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="703810061635792791">"ସ୍ପ୍ଲିଟ୍‍-ସ୍କ୍ରୀନ ବିଭାଜକ"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଡିଭାଇଡର"</string>
+ <string name="divider_title" msgid="1963391955593749442">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଡିଭାଇଡର"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ବାମ ପଟକୁ ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍‍ କରନ୍ତୁ"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ବାମ ପଟକୁ 70% କରନ୍ତୁ"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ବାମ ପଟକୁ 50% କରନ୍ତୁ"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ଏହାର ସମାଧାନ ହୋଇନାହିଁ?\nଫେରିଯିବା ପାଇଁ ଟାପ କରନ୍ତୁ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"କ୍ୟାମେରାରେ କିଛି ସମସ୍ୟା ନାହିଁ? ଖାରଜ କରିବାକୁ ଟାପ କରନ୍ତୁ।"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ଦେଖନ୍ତୁ ଏବଂ ଆହୁରି ଅନେକ କିଛି କରନ୍ତୁ"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"ସ୍ପ୍ଲିଟ-ସ୍କ୍ରିନ ପାଇଁ ଅନ୍ୟ ଏକ ଆପକୁ ଡ୍ରାଗ କରନ୍ତୁ"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ପାଇଁ ଅନ୍ୟ ଏକ ଆପକୁ ଡ୍ରାଗ କରନ୍ତୁ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ଏକ ଆପକୁ ରିପୋଜିସନ କରିବା ପାଇଁ ଏହାର ବାହାରେ ଦୁଇଥର-ଟାପ କରନ୍ତୁ"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ବୁଝିଗଲି"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ଅଧିକ ସୂଚନା ପାଇଁ ବିସ୍ତାର କରନ୍ତୁ।"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ସ୍କ୍ରିନସଟ"</string>
<string name="close_text" msgid="4986518933445178928">"ବନ୍ଦ କରନ୍ତୁ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"ମେନୁ ଖୋଲନ୍ତୁ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-or/strings_tv.xml b/libs/WindowManager/Shell/res/values-or/strings_tv.xml
index 0c1d99e4ca71..1e81f4d80f32 100644
--- a/libs/WindowManager/Shell/res/values-or/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-or/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ମୁଭ କରନ୍ତୁ"</string>
<string name="pip_expand" msgid="1051966011679297308">"ବିସ୍ତାର କରନ୍ତୁ"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ସଙ୍କୁଚିତ କରନ୍ତୁ"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<annotation icon="home_icon">" ହୋମ ବଟନ "</annotation>"କୁ ଦୁଇଥର ଦବାନ୍ତୁ"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ ପାଇଁ "<annotation icon="home_icon">"ହୋମ ବଟନ"</annotation>"କୁ ଦୁଇଥର ଦବାନ୍ତୁ"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ପିକଚର-ଇନ-ପିକଚର ମେନୁ।"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ବାମକୁ ମୁଭ କରନ୍ତୁ"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml
index e8f5fd70d2a4..d9f7f340a889 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ਸੈਟਿੰਗਾਂ"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਿੱਚ ਦਾਖਲ ਹੋਵੋ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"ਮੀਨੂ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ ਮੀਨੂ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ਤਸਵੀਰ-ਅੰਦਰ-ਤਸਵੀਰ ਵਿੱਚ ਹੈ"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"ਜੇਕਰ ਤੁਸੀਂ ਨਹੀਂ ਚਾਹੁੰਦੇ ਕਿ <xliff:g id="NAME">%s</xliff:g> ਐਪ ਇਸ ਵਿਸ਼ੇਸ਼ਤਾ ਦੀ ਵਰਤੋਂ ਕਰੇ, ਤਾਂ ਸੈਟਿੰਗਾਂ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ ਅਤੇ ਇਸਨੂੰ ਬੰਦ ਕਰੋ।"</string>
<string name="pip_play" msgid="3496151081459417097">"ਚਲਾਓ"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ਆਕਾਰ ਬਦਲੋ"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"ਸਟੈਸ਼"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ਅਣਸਟੈਸ਼"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ।"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ਐਪ ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਨੂੰ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ।"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਐਪ ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ਐਪ ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ਇਹ ਐਪ ਸਿਰਫ਼ 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="703810061635792791">"ਸਪਲਿਟ-ਸਕ੍ਰੀਨ ਡਿਵਾਈਡਰ"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਿਭਾਜਕ"</string>
+ <string name="divider_title" msgid="1963391955593749442">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਿਭਾਜਕ"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ਖੱਬੇ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ਖੱਬੇ 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ਖੱਬੇ 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"ਕੀ ਇਹ ਠੀਕ ਨਹੀਂ ਹੋਈ?\nਵਾਪਸ ਉਹੀ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"ਕੀ ਕੈਮਰੇ ਸੰਬੰਧੀ ਕੋਈ ਸਮੱਸਿਆ ਨਹੀਂ ਹੈ? ਖਾਰਜ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"ਦੇਖੋ ਅਤੇ ਹੋਰ ਬਹੁਤ ਕੁਝ ਕਰੋ"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੇ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਵਿੱਚ ਘਸੀਟੋ"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੇ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਵਿੱਚ ਘਸੀਟੋ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ਕਿਸੇ ਐਪ ਦੀ ਜਗ੍ਹਾ ਬਦਲਣ ਲਈ ਉਸ ਦੇ ਬਾਹਰ ਡਬਲ ਟੈਪ ਕਰੋ"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ਸਮਝ ਲਿਆ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ਹੋਰ ਜਾਣਕਾਰੀ ਲਈ ਵਿਸਤਾਰ ਕਰੋ।"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ਸਕ੍ਰੀਨਸ਼ਾਟ"</string>
<string name="close_text" msgid="4986518933445178928">"ਬੰਦ ਕਰੋ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ਮੀਨੂ ਬੰਦ ਕਰੋ"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"ਮੀਨੂ ਖੋਲ੍ਹੋ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml
index a1edde738775..758aafad457a 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ਲਿਜਾਓ"</string>
<string name="pip_expand" msgid="1051966011679297308">"ਵਿਸਤਾਰ ਕਰੋ"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ਸਮੇਟੋ"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" ਕੰਟਰੋਲਾਂ ਲਈ "<annotation icon="home_icon">" ਹੋਮ ਬਟਨ "</annotation>" ਨੂੰ ਦੋ ਵਾਰ ਦਬਾਓ"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"ਕੰਟਰੋਲਾਂ ਲਈ "<annotation icon="home_icon">"ਹੋਮ"</annotation>" ਨੂੰ ਦੋ ਵਾਰ ਦਬਾਓ"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"ਤਸਵੀਰ-ਵਿੱਚ-ਤਸਵੀਰ ਮੀਨੂ।"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ਖੱਬੇ ਲਿਜਾਓ"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ਸੱਜੇ ਲਿਜਾਓ"</string>
diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml
index bcead2118327..0699f5df99ab 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Ustawienia"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Włącz podzielony ekran"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu funkcji Obraz w obrazie."</string>
<string name="pip_notification_title" msgid="1347104727641353453">"Aplikacja <xliff:g id="NAME">%s</xliff:g> działa w trybie obraz w obrazie"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Jeśli nie chcesz, by aplikacja <xliff:g id="NAME">%s</xliff:g> korzystała z tej funkcji, otwórz ustawienia i wyłącz ją."</string>
<string name="pip_play" msgid="3496151081459417097">"Odtwórz"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmień rozmiar"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Przenieś do schowka"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zabierz ze schowka"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacja może nie działać przy podzielonym ekranie."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacja nie obsługuje dzielonego ekranu."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Linia dzielenia ekranu"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Linia dzielenia ekranu"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Linia dzielenia ekranu"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lewa część ekranu na pełnym ekranie"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% lewej części ekranu"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% lewej części ekranu"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Naprawa się nie udała?\nKliknij, aby cofnąć"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Brak problemów z aparatem? Kliknij, aby zamknąć"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Zobacz i zrób więcej"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Aby podzielić ekran, przeciągnij drugą aplikację"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Aby podzielić ekran, przeciągnij drugą aplikację"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Kliknij dwukrotnie poza aplikacją, aby ją przenieść"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Rozwiń, aby wyświetlić więcej informacji."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Zrzut ekranu"</string>
<string name="close_text" msgid="4986518933445178928">"Zamknij"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zamknij menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Otwórz menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml
index 2bb90addc241..b598351b5127 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Przenieś"</string>
<string name="pip_expand" msgid="1051966011679297308">"Rozwiń"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Zwiń"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Naciśnij dwukrotnie "<annotation icon="home_icon">"EKRAN GŁÓWNY"</annotation>", aby wyświetlić ustawienia"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Naciśnij dwukrotnie "<annotation icon="home_icon">"EKRAN GŁÓWNY"</annotation>", aby wyświetlić ustawienia"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu funkcji Obraz w obrazie."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Przenieś w lewo"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Przenieś w prawo"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
index cc1fb4ea5851..eea9be26f432 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu do picture-in-picture"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ocultar"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Este 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="703810061635792791">"Divisor de tela"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de tela"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Divisor de tela"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lado esquerdo em tela cheia"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Esquerda a 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Arraste outro app para a tela dividida"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arraste outro app para dividir a tela"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de um app para reposicionar"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendi"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Abra para ver mais informações."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captura de tela"</string>
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml
index 14d1c34fd3e8..2528ea9b8ebb 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mover"</string>
<string name="pip_expand" msgid="1051966011679297308">"Abrir"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Pressione o botão "<annotation icon="home_icon">"HOME"</annotation>" duas vezes para acessar os controles"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
index b5a0a5a0d48d..ed0cdb61dacf 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Definições"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Aceder ao ecrã dividido"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu de ecrã no ecrã"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"A app <xliff:g id="NAME">%s</xliff:g> está no modo de ecrã no ecrã"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Se não pretende que a app <xliff:g id="NAME">%s</xliff:g> utilize esta funcionalidade, toque para abrir as definições e desative-a."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Armazenar"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Remover do armazenamento"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"A app pode não funcionar com o ecrã dividido."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"A app não é compatível com o ecrã dividido."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Divisor do ecrã dividido"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Divisor do ecrã dividido"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Divisor do ecrã dividido"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ecrã esquerdo inteiro"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"70% no ecrã esquerdo"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% no ecrã esquerdo"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Balão"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gerir"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão ignorado."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Toque para reiniciar esta app e ver melhor."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Toque para reiniciar esta app e ficar com uma melhor visão."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemas com a câmara?\nToque aqui para reajustar"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Não foi corrigido?\nToque para reverter"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nenhum problema com a câmara? Toque para ignorar."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Arraste outra app para usar o ecrã dividido"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arraste outra app para usar o ecrã dividido"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de uma app para a reposicionar"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Expandir para obter mais informações"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captura de ecrã"</string>
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml
index 1ada4508714a..a678f581c272 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mover"</string>
<string name="pip_expand" msgid="1051966011679297308">"Expandir"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Reduzir"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Prima duas vezes "<annotation icon="home_icon">" PÁGINA INICIAL "</annotation>" para controlos"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Prima duas vezes "<annotation icon="home_icon">"PÁGINA INICIAL"</annotation>" para os controlos"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu de ecrã no ecrã."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml
index cc1fb4ea5851..eea9be26f432 100644
--- a/libs/WindowManager/Shell/res/values-pt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu do picture-in-picture"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string>
<string name="pip_play" msgid="3496151081459417097">"Reproduzir"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionar"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ocultar"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Exibir"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"É possível que o app não funcione com a tela dividida."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"O app não é compatível com a divisão de tela."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Este 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="703810061635792791">"Divisor de tela"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Divisor de tela"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Divisor de tela"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Lado esquerdo em tela cheia"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Esquerda a 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Arraste outro app para a tela dividida"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arraste outro app para dividir a tela"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de um app para reposicionar"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Entendi"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Abra para ver mais informações."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captura de tela"</string>
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml
index 14d1c34fd3e8..2528ea9b8ebb 100644
--- a/libs/WindowManager/Shell/res/values-pt/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-pt/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mover"</string>
<string name="pip_expand" msgid="1051966011679297308">"Abrir"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Fechar"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Pressione o botão "<annotation icon="home_icon">"home"</annotation>" duas vezes para acessar os controles"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Pressione o botão "<annotation icon="home_icon">"HOME"</annotation>" duas vezes para acessar os controles"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu do picture-in-picture"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mover para a esquerda"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mover para a direita"</string>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml
index 8bcd9c372136..8a64b1686543 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Setări"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Accesează ecranul împărțit"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Meniu picture-in-picture"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> este în modul picture-in-picture"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Dacă nu vrei ca <xliff:g id="NAME">%s</xliff:g> să folosească această funcție, atinge pentru a deschide setările și dezactiveaz-o."</string>
<string name="pip_play" msgid="3496151081459417097">"Redă"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Redimensionează"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stochează"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Anulează stocarea"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"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="7408396418008948957">"Aplicația nu acceptă ecranul împărțit."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Aplicația poate fi deschisă î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="703810061635792791">"Separator pentru ecranul împărțit"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Separator pentru ecranul împărțit"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Separator pentru ecranul împărțit"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Partea stângă pe ecran complet"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Partea stângă: 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Partea stângă: 50%"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionează"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Atinge ca să repornești aplicația pentru o afișare mai bună."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Atinge ca să repornești aplicația pentru o vizualizare mai bună."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Ai probleme cu camera foto?\nAtinge pentru a reîncadra"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nu ai remediat problema?\nAtinge pentru a reveni"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nu ai probleme cu camera foto? Atinge pentru a închide."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Vezi și fă mai multe"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Trage în altă aplicație pentru a folosi ecranul împărțit"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Trage în altă aplicație pentru a folosi ecranul împărțit"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Atinge de două ori lângă o aplicație pentru a o repoziționa"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Extinde pentru mai multe informații"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Captură de ecran"</string>
<string name="close_text" msgid="4986518933445178928">"Închide"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Închide meniul"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Deschide meniul"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml
index b5245ffbf0bc..c3226ff28934 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Mută"</string>
<string name="pip_expand" msgid="1051966011679297308">"Extinde"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Restrânge"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Apasă de două ori "<annotation icon="home_icon">"butonul ecran de pornire"</annotation>" pentru comenzi"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Apasă de 2 ori "<annotation icon="home_icon">"ECRANUL DE PORNIRE"</annotation>" pentru comenzi"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meniu picture-in-picture."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Mută la stânga"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Mută la dreapta"</string>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml
index ff2c85031122..a7db44dda901 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Включить разделение экрана"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню \"Картинка в картинке\""</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> находится в режиме \"Картинка в картинке\""</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Чтобы отключить эту функцию для приложения \"<xliff:g id="NAME">%s</xliff:g>\", перейдите в настройки."</string>
<string name="pip_play" msgid="3496151081459417097">"Воспроизвести"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Изменить размер"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Скрыть"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показать"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"В режиме разделения экрана приложение может работать нестабильно."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Приложение не поддерживает разделение экрана."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Когда включено разделение экрана, приложение может работать нестабильно."</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Приложение не поддерживает разделение экрана."</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Это приложение можно открыть только в одном окне."</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="703810061635792791">"Разделитель экрана"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Разделитель экрана"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Разделитель экрана"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Левый во весь экран"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Левый на 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левый на 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Не помогло?\nНажмите, чтобы отменить изменения."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Нет проблем с камерой? Нажмите, чтобы закрыть."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Выполняйте несколько задач одновременно"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Перетащите сюда другое приложение, чтобы использовать разделение экрана."</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Перетащите сюда другое приложение, чтобы использовать разделение экрана."</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Чтобы переместить приложение, дважды нажмите рядом с ним."</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ОК"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Развернуть, чтобы узнать больше."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string>
<string name="close_text" msgid="4986518933445178928">"Закрыть"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Закрыть меню"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Открыть меню"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml
index e7f55ec1bc57..c8fb47913ec9 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Переместить"</string>
<string name="pip_expand" msgid="1051966011679297308">"Развернуть"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Свернуть"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Элементы управления: дважды нажмите "<annotation icon="home_icon">" кнопку главного экрана "</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Параметры: дважды нажмите "<annotation icon="home_icon">"кнопку главного экрана"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"Картинка в картинке\"."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Переместить влево"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Переместить вправо"</string>
diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml
index 577b2b85ee3b..4153ce2bcf1f 100644
--- a/libs/WindowManager/Shell/res/values-si/strings.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"සැකසීම්"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"බෙදුම් තිරයට ඇතුළු වන්න"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"මෙනුව"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"පින්තූරය තුළ පින්තූරය මෙනුව"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> පින්තූරය-තුළ-පින්තූරය තුළ වේ"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"ඔබට <xliff:g id="NAME">%s</xliff:g> මෙම විශේෂාංගය භාවිත කිරීමට අවශ්‍ය නැති නම්, සැකසීම් විවෘත කිරීමට තට්ටු කර එය ක්‍රියාවිරහිත කරන්න."</string>
<string name="pip_play" msgid="3496151081459417097">"ධාවනය කරන්න"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ප්‍රතිප්‍රමාණ කරන්න"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"සඟවා තබන්න"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"සඟවා තැබීම ඉවත් කරන්න"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"යෙදුම බෙදුම් තිරය සමග ක්‍රියා නොකළ හැකිය"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"යෙදුම බෙදුණු-තිරය සඳහා සහාය නොදක්වයි."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"යෙදුම බෙදීම් තිරය සමග ක්‍රියා නොකළ හැක"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"යෙදුම බෙදුම් තිරයට සහාය නොදක්වයි"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"මෙම යෙදුම විවෘත කළ හැක්කේ 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="703810061635792791">"බෙදුම්-තිර වෙන්කරණය"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"බෙදුම් තිර වෙන්කරණය"</string>
+ <string name="divider_title" msgid="1963391955593749442">"බෙදුම් තිර වෙන්කරණය"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"වම් පූර්ණ තිරය"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"වම් 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"වම් 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"එය විසඳුවේ නැතිද?\nප්‍රතිවර්තනය කිරීමට තට්ටු කරන්න"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"කැමරා ගැටලු නොමැතිද? ඉවත දැමීමට තට්ටු කරන්න"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"බලන්න සහ තවත් දේ කරන්න"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"බෙදුම් තිරය සඳහා වෙනත් යෙදුමකට අදින්න"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"බෙදුම් තිරය සඳහා වෙනත් යෙදුමකට අදින්න"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"යෙදුමක් නැවත ස්ථානගත කිරීමට පිටතින් දෙවරක් තට්ටු කරන්න"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"තේරුණා"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"වැඩිදුර තොරතුරු සඳහා දිග හරින්න"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"තිර රුව"</string>
<string name="close_text" msgid="4986518933445178928">"වසන්න"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"මෙනුව වසන්න"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"මෙනුව විවෘත කරන්න"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-si/strings_tv.xml b/libs/WindowManager/Shell/res/values-si/strings_tv.xml
index 5478ce5d3d40..aa949ec75b3d 100644
--- a/libs/WindowManager/Shell/res/values-si/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ගෙන යන්න"</string>
<string name="pip_expand" msgid="1051966011679297308">"දිග හරින්න"</string>
<string name="pip_collapse" msgid="3903295106641385962">"හකුළන්න"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" පාලන සඳහා "<annotation icon="home_icon">" මුල් පිටුව "</annotation>" දෙවරක් ඔබන්න"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"පාලන සඳහා "<annotation icon="home_icon">"නිවහන"</annotation>" දෙවරක් ඔබන්න"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"පින්තූරය තුළ පින්තූරය මෙනුව"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"වමට ගෙන යන්න"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"දකුණට ගෙන යන්න"</string>
diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml
index 9a1002c19350..4e38943662ea 100644
--- a/libs/WindowManager/Shell/res/values-sk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sk/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Nastavenia"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Prejsť na rozdelenú obrazovku"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Ponuka"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Ponuka obrazu v obraze"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v režime obraz v obraze"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ak nechcete, aby aplikácia <xliff:g id="NAME">%s</xliff:g> používala túto funkciu, klepnutím otvorte nastavenia a vypnite ju."</string>
<string name="pip_play" msgid="3496151081459417097">"Prehrať"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Zmeniť veľkosť"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Skryť"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Zrušiť skrytie"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikácia nemusí fungovať s rozdelenou obrazovkou."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikácia nepodporuje rozdelenú obrazovku."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Rozdeľovač obrazovky"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Rozdeľovač obrazovky"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Rozdeľovač obrazovky"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ľavá – na celú obrazovku"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ľavá – 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ľavá – 50 %"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nevyriešilo sa to?\nKlepnutím sa vráťte."</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nemáte problémy s kamerou? Klepnutím zatvoríte."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Zobrazte si a zvládnite toho viac"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Rozdelenú obrazovku aktivujete presunutím ďalšie aplikácie"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Rozdelenú obrazovku môžete použiť presunutím do inej aplikácie"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvojitým klepnutím mimo aplikácie zmeníte jej pozíciu"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Dobre"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Po rozbalení sa dozviete viac."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Snímka obrazovky"</string>
<string name="close_text" msgid="4986518933445178928">"Zavrieť"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zavrieť ponuku"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Otvoriť ponuku"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml
index 1df43afca2da..d5562d519d4b 100644
--- a/libs/WindowManager/Shell/res/values-sk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sk/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Presunúť"</string>
<string name="pip_expand" msgid="1051966011679297308">"Rozbaliť"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Zbaliť"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Ovládanie zobraz. dvoj. stlač. "<annotation icon="home_icon">" TLAČIDLA PLOCHY "</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Ovládanie zobrazíte dvojitým stlačením "<annotation icon="home_icon">"DOMOV"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Ponuka obrazu v obraze."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Posunúť doľava"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Posunúť doprava"</string>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml
index 18ffe7be40ae..b0e67a771653 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Nastavitve"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Vklopi razdeljen zaslon"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Meni za sliko v sliki"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v načinu slika v sliki"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Če ne želite, da aplikacija <xliff:g id="NAME">%s</xliff:g> uporablja to funkcijo, se dotaknite, da odprete nastavitve, in funkcijo izklopite."</string>
<string name="pip_play" msgid="3496151081459417097">"Predvajaj"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Spremeni velikost"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Zakrij"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Razkrij"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacija morda ne deluje v načinu razdeljenega zaslona."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacija ne podpira načina razdeljenega zaslona."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Razdelilnik zaslonov"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Razdelilnik zaslonov"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Razdelilnik zaslonov"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Levi v celozaslonski način"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Levi 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi 50 %"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"To ni odpravilo težave?\nDotaknite se za povrnitev"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nimate težav s fotoaparatom? Dotaknite se za opustitev."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Oglejte si in naredite več"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Za razdeljeni zaslon povlecite sem še eno aplikacijo."</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Za razdeljeni zaslon povlecite sem še eno aplikacijo."</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Dvakrat se dotaknite zunaj aplikacije, če jo želite prestaviti."</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"V redu"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Razširitev za več informacij"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Posnetek zaslona"</string>
<string name="close_text" msgid="4986518933445178928">"Zapri"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zapri meni"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Odpri meni"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml
index 88fc8325aa01..a37375e1ae9c 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Premakni"</string>
<string name="pip_expand" msgid="1051966011679297308">"Razširi"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Strni"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Za kontrolnike dvakrat pritisnite gumb za "<annotation icon="home_icon">" ZAČETNI ZASLON "</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Za kontrolnike dvakrat pritisnite gumb za "<annotation icon="home_icon">"ZAČETNI ZASLON"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Meni za sliko v sliki"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Premakni levo"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Premakni desno"</string>
diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml
index baead5992980..29bfb9268f47 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Cilësimet"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Hyr në ekranin e ndarë"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menyja"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menyja e \"Figurës brenda figurës\""</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> është në figurë brenda figurës"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Nëse nuk dëshiron që <xliff:g id="NAME">%s</xliff:g> ta përdorë këtë funksion, trokit për të hapur cilësimet dhe për ta çaktivizuar."</string>
<string name="pip_play" msgid="3496151081459417097">"Luaj"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ndrysho përmasat"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Fshih"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Mos e fshih"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Aplikacioni mund të mos funksionojë me ekranin e ndarë."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Aplikacioni nuk mbështet ekranin e ndarë."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Ndarësi i ekranit të ndarë"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Ndarësi i ekranit të ndarë"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Ndarësi i ekranit të ndarë"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ekrani i plotë majtas"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Majtas 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Majtas 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Nuk u rregullua?\nTrokit për ta rikthyer"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Nuk ka probleme me kamerën? Trokit për ta shpërfillur."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Shiko dhe bëj më shumë"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Zvarrite në një aplikacion tjetër për ekranin e ndarë"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Zvarrite në një aplikacion tjetër për ekranin e ndarë"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Trokit dy herë jashtë një aplikacioni për ta ripozicionuar"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"E kuptova"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Zgjeroje për më shumë informacion."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Pamja e ekranit"</string>
<string name="close_text" msgid="4986518933445178928">"Mbyll"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Mbyll menynë"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Hap menynë"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml
index 58687e5867fe..3fbaaac2d3a2 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Lëviz"</string>
<string name="pip_expand" msgid="1051966011679297308">"Zgjero"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Palos"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Trokit dy herë "<annotation icon="home_icon">" KREU "</annotation>" për kontrollet"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Trokit dy herë te "<annotation icon="home_icon">"KREU"</annotation>" për kontrollet"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyja e \"Figurës brenda figurës\"."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Lëviz majtas"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Lëviz djathtas"</string>
diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml
index 517620efd32f..307efc9d6a2e 100644
--- a/libs/WindowManager/Shell/res/values-sr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sr/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Подешавања"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Уђи на подељени екран"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Мени"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Мени слике у слици."</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> је слика у слици"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ако не желите да <xliff:g id="NAME">%s</xliff:g> користи ову функцију, додирните да бисте отворили подешавања и искључили је."</string>
<string name="pip_play" msgid="3496151081459417097">"Пусти"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Промените величину"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ставите у тајну меморију"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Уклоните из тајне меморије"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Апликација можда неће радити са подељеним екраном."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Апликација не подржава подељени екран."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Апликација можда неће радити са подељеним екраном."</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Апликација не подржава подељени екран."</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ова апликација може да се отвори само у једном прозору."</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="703810061635792791">"Разделник подељеног екрана"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Разделник подељеног екрана"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Разделник подељеног екрана"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Режим целог екрана за леви екран"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Леви екран 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Леви екран 50%"</string>
@@ -82,12 +84,12 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Проблем није решен?\nДодирните да бисте вратили"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Немате проблема са камером? Додирните да бисте одбацили."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Видите и урадите више"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Превуците другу апликацију да бисте користили подељени екран"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Превуците другу апликацију да бисте користили подељени екран"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Двапут додирните изван апликације да бисте променили њену позицију"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Важи"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Проширите за још информација."</string>
<string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"Желите ли да рестартујете ради бољег приказа?"</string>
- <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Можете да рестартујете апликацију да би изгледала боље на екрану, али можете да изгубите напредак или несачуване промене"</string>
+ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"Можете да рестартујете апликацију да би изгледала боље на екрану, с тим што можете да изгубите оно што сте урадили или несачуване промене, ако их има"</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"Откажи"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"Рестартуј"</string>
<string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"Не приказуј поново"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Снимак екрана"</string>
<string name="close_text" msgid="4986518933445178928">"Затворите"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Отворите мени"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml
index e850979174a3..34950027772b 100644
--- a/libs/WindowManager/Shell/res/values-sr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sr/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Премести"</string>
<string name="pip_expand" msgid="1051966011679297308">"Прошири"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Скупи"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Двапут притисните "<annotation icon="home_icon">" HOME "</annotation>" за контроле"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Двапут притисните "<annotation icon="home_icon">" HOME "</annotation>" за контроле"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Мени Слика у слици."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Померите налево"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Померите надесно"</string>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml
index 133a57b6eebc..33652cd74fda 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Inställningar"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Starta delad skärm"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Bild-i-bild-meny"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> visas i bild-i-bild"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Om du inte vill att den här funktionen används i <xliff:g id="NAME">%s</xliff:g> öppnar du inställningarna genom att trycka. Sedan inaktiverar du funktionen."</string>
<string name="pip_play" msgid="3496151081459417097">"Spela upp"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Ändra storlek"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Utför stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Återställ stash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Appen kanske inte fungerar med delad skärm."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Appen har inte stöd för delad skärm."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Avdelare för delad skärm"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Avdelare för delad skärm"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Avdelare för delad skärm"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Helskärm på vänster skärm"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Vänster 70 %"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vänster 50 %"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Löstes inte problemet?\nTryck för att återställa"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Inga problem med kameran? Tryck för att ignorera."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Se och gör mer"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Dra till en annan app för läget Delad skärm"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Dra till en annan app för att dela upp skärmen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Tryck snabbt två gånger utanför en app för att flytta den"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Utöka för mer information."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Skärmbild"</string>
<string name="close_text" msgid="4986518933445178928">"Stäng"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Stäng menyn"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Öppna menyn"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml
index d3a9c3de66db..7116ac162fbd 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Flytta"</string>
<string name="pip_expand" msgid="1051966011679297308">"Utöka"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Komprimera"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Tryck snabbt två gånger på "<annotation icon="home_icon">" HEM "</annotation>" för kontroller"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Tryck snabbt två gånger på "<annotation icon="home_icon">"HEM"</annotation>" för inställningar"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Bild-i-bild-meny."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Flytta åt vänster"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Flytta åt höger"</string>
diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml
index 03cc92d1b96a..fe2ad1f1846d 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Mipangilio"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Weka skrini iliyogawanywa"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menyu ya kipengele cha Kupachika Picha ndani ya Picha nyingine."</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> iko katika hali ya picha ndani ya picha nyingine"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Ikiwa hutaki <xliff:g id="NAME">%s</xliff:g> itumie kipengele hiki, gusa ili ufungue mipangilio na uizime."</string>
<string name="pip_play" msgid="3496151081459417097">"Cheza"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Badilisha ukubwa"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ficha"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Fichua"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Huenda programu isifanye kazi kwenye skrini inayogawanywa."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Programu haiwezi kutumia skrini iliyogawanywa."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Kitenganishi cha skrini inayogawanywa"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Kitenganishi cha kugawa skrini"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Kitenganishi cha kugawa skrini"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Skrini nzima ya kushoto"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kushoto 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kushoto 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Umeshindwa kurekebisha?\nGusa ili urejeshe nakala ya awali"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Je, hakuna hitilafu za kamera? Gusa ili uondoe."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Angalia na ufanye zaidi"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Buruta ndani programu nyingine ili utumie hali ya skrini iliyogawanywa"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Buruta katika programu nyingine ili utumie skrini iliyogawanywa"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Gusa mara mbili nje ya programu ili uihamishe"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Nimeelewa"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Panua ili upate maelezo zaidi."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Picha ya skrini"</string>
<string name="close_text" msgid="4986518933445178928">"Funga"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Funga Menyu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Fungua Menyu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml
index 7b9a310ff0b6..1e9406f01433 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Hamisha"</string>
<string name="pip_expand" msgid="1051966011679297308">"Panua"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Kunja"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Bonyeza mara mbili kitufe cha "<annotation icon="home_icon">" UKURASA WA KWANZA "</annotation>" kupata vidhibiti"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Bonyeza mara mbili kitufe cha "<annotation icon="home_icon">" UKURASA WA KWANZA "</annotation>" kupata vidhibiti"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menyu ya kipengele cha kupachika picha ndani ya picha nyingine."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sogeza kushoto"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sogeza kulia"</string>
diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml
index afb0adfac36b..fd5f0e646d9b 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"அமைப்புகள்"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"திரைப் பிரிப்பு பயன்முறைக்குச் செல்"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"மெனு"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"பிக்ச்சர்-இன்-பிக்ச்சர் மெனு"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> தற்போது பிக்ச்சர்-இன்-பிக்ச்சரில் உள்ளது"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> இந்த அம்சத்தைப் பயன்படுத்த வேண்டாம் என நினைத்தால் இங்கு தட்டி அமைப்புகளைத் திறந்து இதை முடக்கவும்."</string>
<string name="pip_play" msgid="3496151081459417097">"இயக்கு"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"அளவு மாற்று"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"திரைப் பிரிப்பு அம்சத்தில் ஆப்ஸ் செயல்படாமல் போகக்கூடும்."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"திரையைப் பிரிப்பதைப் ஆப்ஸ் ஆதரிக்கவில்லை."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"திரைப் பிரிப்புப் பயன்முறையில் ஆப்ஸ் செயல்படாமல் போகக்கூடும்"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"திரைப் பிரிப்புப் பயன்முறையை ஆப்ஸ் ஆதரிக்காது"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"இந்த ஆப்ஸை 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="703810061635792791">"திரையைப் பிரிக்கும் பிரிப்பான்"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"திரைப் பிரிப்பான்"</string>
+ <string name="divider_title" msgid="1963391955593749442">"திரைப் பிரிப்பான்"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"இடது புறம் முழுத் திரை"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"இடது புறம் 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"இடது புறம் 50%"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"பபிள்"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"நிர்வகி"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"குமிழ் நிராகரிக்கப்பட்டது."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"இங்கு தட்டி ஆப்ஸை மீண்டும் தொடங்கி, ஆப்ஸ் காட்சியை இன்னும் சிறப்பாக்கலாம்."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"இங்கு தட்டுவதன் மூலம் இந்த ஆப்ஸை மீண்டும் தொடங்கி, ஆப்ஸ் காட்டப்படும் விதத்தை இன்னும் சிறப்பாக்கலாம்."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"கேமரா தொடர்பான சிக்கல்களா?\nமீண்டும் பொருத்த தட்டவும்"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"சிக்கல்கள் சரிசெய்யப்படவில்லையா?\nமாற்றியமைக்க தட்டவும்"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"கேமரா தொடர்பான சிக்கல்கள் எதுவும் இல்லையா? நிராகரிக்க தட்டவும்."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"பலவற்றைப் பார்த்தல் மற்றும் செய்தல்"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"திரைப் பிரிப்புக்கு மற்றொரு ஆப்ஸை இழுக்கலாம்"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"திரைப் பிரிப்புக்கு மற்றொரு ஆப்ஸை இழுக்கலாம்"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"ஆப்ஸை இடம் மாற்ற அதன் வெளியில் இருமுறை தட்டலாம்"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"சரி"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"கூடுதல் தகவல்களுக்கு விரிவாக்கலாம்."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ஸ்கிரீன்ஷாட்"</string>
<string name="close_text" msgid="4986518933445178928">"மூடும்"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"மெனுவை மூடும்"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"மெனுவைத் திற"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml
index e201401e2e35..ef1bcf913eaf 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"நகர்த்து"</string>
<string name="pip_expand" msgid="1051966011679297308">"விரி"</string>
<string name="pip_collapse" msgid="3903295106641385962">"சுருக்கு"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" கட்டுப்பாடுகள்: "<annotation icon="home_icon">" முகப்பு "</annotation>" பட்டனை இருமுறை அழுத்துக"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"கட்டுப்பாடுகளுக்கு "<annotation icon="home_icon">"முகப்பு"</annotation>" பட்டனை இருமுறை அழுத்துக"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"பிக்ச்சர்-இன்-பிக்ச்சர் மெனு."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"இடப்புறம் நகர்த்து"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"வலப்புறம் நகர்த்து"</string>
diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml
index a09cacb036db..6f95aa9c8305 100644
--- a/libs/WindowManager/Shell/res/values-te/strings.xml
+++ b/libs/WindowManager/Shell/res/values-te/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"సెట్టింగ్‌లు"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"స్ప్లిట్ స్క్రీన్‌ను ఎంటర్ చేయండి"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"మెనూ"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"పిక్చర్-ఇన్-పిక్చర్ మెనూ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> చిత్రంలో చిత్రం రూపంలో ఉంది"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ఈ లక్షణాన్ని ఉపయోగించకూడదు అని మీరు అనుకుంటే, సెట్టింగ్‌లను తెరవడానికి ట్యాప్ చేసి, దీన్ని ఆఫ్ చేయండి."</string>
<string name="pip_play" msgid="3496151081459417097">"ప్లే చేయి"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"సైజ్‌ మార్చు"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"స్టాచ్"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"ఆన్‌స్టాచ్"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"స్క్రీన్ విభజనతో యాప్‌ పని చేయకపోవచ్చు."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"యాప్‌లో స్క్రీన్ విభజనకు మద్దతు లేదు."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"స్ప్లిట్ స్క్రీన్‌తో యాప్ పని చేయకపోవచ్చు"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"యాప్‌లో స్ప్లిట్ స్క్రీన్‌కు సపోర్ట్ లేదు"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"ఈ యాప్‌ను 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="703810061635792791">"విభజన స్క్రీన్ విభాగిని"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"స్ప్లిట్ స్క్రీన్ డివైడర్"</string>
+ <string name="divider_title" msgid="1963391955593749442">"స్ప్లిట్ స్క్రీన్ డివైడర్"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"ఎడమవైపు ఫుల్-స్క్రీన్‌"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ఎడమవైపు 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ఎడమవైపు 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"దాని సమస్యను పరిష్కరించలేదా?\nపూర్వస్థితికి మార్చడానికి ట్యాప్ చేయండి"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"కెమెరా సమస్యలు లేవా? తీసివేయడానికి ట్యాప్ చేయండి."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"చూసి, మరిన్ని చేయండి"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"స్ప్లిట్-స్క్రీన్ కోసం మరొక యాప్‌లోకి లాగండి"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్‌లోకి లాగండి"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"యాప్ స్థానాన్ని మార్చడానికి దాని వెలుపల డబుల్-ట్యాప్ చేయండి"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"అర్థమైంది"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"మరింత సమాచారం కోసం విస్తరించండి."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"స్క్రీన్‌షాట్"</string>
<string name="close_text" msgid="4986518933445178928">"మూసివేయండి"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"మెనూను మూసివేయండి"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"మెనూను తెరవండి"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-te/strings_tv.xml b/libs/WindowManager/Shell/res/values-te/strings_tv.xml
index 6284d90cb11f..d9237dff6dd8 100644
--- a/libs/WindowManager/Shell/res/values-te/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-te/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"తరలించండి"</string>
<string name="pip_expand" msgid="1051966011679297308">"విస్తరించండి"</string>
<string name="pip_collapse" msgid="3903295106641385962">"కుదించండి"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" కంట్రోల్స్ కోసం "<annotation icon="home_icon">" HOME "</annotation>" బటన్ రెండుసార్లు నొక్కండి"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"కంట్రోల్స్ కోసం "<annotation icon="home_icon">"HOME"</annotation>" బటన్ రెండుసార్లు నొక్కండి"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"పిక్చర్-ఇన్-పిక్చర్ మెనూ."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ఎడమ వైపుగా జరపండి"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"కుడి వైపుగా జరపండి"</string>
diff --git a/libs/WindowManager/Shell/res/values-television/dimen.xml b/libs/WindowManager/Shell/res/values-television/dimen.xml
index 14e89f8b08df..376cc4faca6f 100644
--- a/libs/WindowManager/Shell/res/values-television/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-television/dimen.xml
@@ -21,4 +21,7 @@
<!-- Padding between PIP and keep clear areas that caused it to move. -->
<dimen name="pip_keep_clear_area_padding">16dp</dimen>
+
+ <!-- The corner radius for PiP window. -->
+ <dimen name="pip_corner_radius">0dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml
index 1569e49a039b..6733940fd445 100644
--- a/libs/WindowManager/Shell/res/values-th/strings.xml
+++ b/libs/WindowManager/Shell/res/values-th/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"การตั้งค่า"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"เข้าสู่โหมดแบ่งหน้าจอ"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"เมนู"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"เมนูการแสดงภาพซ้อนภาพ"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ใช้การแสดงภาพซ้อนภาพ"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"หากคุณไม่ต้องการให้ <xliff:g id="NAME">%s</xliff:g> ใช้ฟีเจอร์นี้ ให้แตะเพื่อเปิดการตั้งค่าแล้วปิดฟีเจอร์"</string>
<string name="pip_play" msgid="3496151081459417097">"เล่น"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"ปรับขนาด"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"เก็บเข้าที่เก็บส่วนตัว"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"เอาออกจากที่เก็บส่วนตัว"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"แอปอาจใช้ไม่ได้กับโหมดแบ่งหน้าจอ"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"แอปไม่สนับสนุนการแยกหน้าจอ"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"แอปอาจใช้ไม่ได้กับโหมดแยกหน้าจอ"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"แอปไม่รองรับการแยกหน้าจอ"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"แอปนี้เปิดได้ใน 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="703810061635792791">"เส้นแบ่งหน้าจอ"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"เส้นแยกหน้าจอ"</string>
+ <string name="divider_title" msgid="1963391955593749442">"เส้นแยกหน้าจอ"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"เต็มหน้าจอทางซ้าย"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"ซ้าย 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ซ้าย 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"หากไม่ได้แก้ไข\nแตะเพื่อเปลี่ยนกลับ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"หากไม่พบปัญหากับกล้อง แตะเพื่อปิด"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"รับชมและทำสิ่งต่างๆ ได้มากขึ้น"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"ลากไปไว้ในแอปอื่นเพื่อแยกหน้าจอ"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"ลากไปไว้ในแอปอื่นเพื่อแยกหน้าจอ"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"แตะสองครั้งด้านนอกแอปเพื่อเปลี่ยนตำแหน่ง"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"รับทราบ"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"ขยายเพื่อดูข้อมูลเพิ่มเติม"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"ภาพหน้าจอ"</string>
<string name="close_text" msgid="4986518933445178928">"ปิด"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ปิดเมนู"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"เปิดเมนู"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-th/strings_tv.xml b/libs/WindowManager/Shell/res/values-th/strings_tv.xml
index 27cf56c6e154..47a6bd1be812 100644
--- a/libs/WindowManager/Shell/res/values-th/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-th/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"ย้าย"</string>
<string name="pip_expand" msgid="1051966011679297308">"ขยาย"</string>
<string name="pip_collapse" msgid="3903295106641385962">"ยุบ"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" กดปุ่ม "<annotation icon="home_icon">" หน้าแรก "</annotation>" สองครั้งเพื่อเปิดการควบคุม"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"กดปุ่ม "<annotation icon="home_icon">" หน้าแรก "</annotation>" สองครั้งเพื่อเปิดการควบคุม"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"เมนูการแสดงภาพซ้อนภาพ"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"ย้ายไปทางซ้าย"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"ย้ายไปทางขวา"</string>
diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml
index 99a8e11cb31f..8cf4eb484378 100644
--- a/libs/WindowManager/Shell/res/values-tl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tl/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Mga Setting"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Pumasok sa split screen"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Menu ng Picture-in-Picture"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"Nasa picture-in-picture ang <xliff:g id="NAME">%s</xliff:g>"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Kung ayaw mong magamit ni <xliff:g id="NAME">%s</xliff:g> ang feature na ito, i-tap upang buksan ang mga setting at i-off ito."</string>
<string name="pip_play" msgid="3496151081459417097">"I-play"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"I-resize"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"I-stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"I-unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Posibleng hindi gumana ang app sa split screen."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Hindi sinusuportahan ng app ang split-screen."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Divider ng split-screen"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Divider ng split screen"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Divider ng split screen"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"I-full screen ang nasa kaliwa"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Gawing 70% ang nasa kaliwa"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Gawing 50% ang nasa kaliwa"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Hindi ito naayos?\nI-tap para i-revert"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Walang isyu sa camera? I-tap para i-dismiss."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Tumingin at gumawa ng higit pa"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Mag-drag ng ibang app para sa split screen"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Mag-drag ng isa pang app para sa split screen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Mag-double tap sa labas ng app para baguhin ang posisyon nito"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"I-expand para sa higit pang impormasyon."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string>
<string name="close_text" msgid="4986518933445178928">"Isara"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Isara ang Menu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Buksan ang Menu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml
index 4cc050bebe5b..2d890d4126b6 100644
--- a/libs/WindowManager/Shell/res/values-tl/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-tl/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Ilipat"</string>
<string name="pip_expand" msgid="1051966011679297308">"I-expand"</string>
<string name="pip_collapse" msgid="3903295106641385962">"I-collapse"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" I-double press ang "<annotation icon="home_icon">" HOME "</annotation>" para sa mga kontrol"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"I-double press ang "<annotation icon="home_icon">"HOME"</annotation>" para sa mga kontrol"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Menu ng Picture-in-Picture."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Ilipat pakaliwa"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Ilipat pakanan"</string>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml
index 8ac29391ea1a..1454435b3de7 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana geç"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Pencere içinde pencere menüsü"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>, pencere içinde pencere özelliğini kullanıyor"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> uygulamasının bu özelliği kullanmasını istemiyorsanız dokunarak ayarları açın ve söz konusu özelliği kapatın."</string>
<string name="pip_play" msgid="3496151081459417097">"Oynat"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Yeniden boyutlandır"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Depola"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Depolama"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Uygulama bölünmüş ekranda çalışmayabilir."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uygulama bölünmüş ekranı desteklemiyor."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Bölünmüş ekran ayırıcı"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Bölünmüş ekran ayırıcı"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Bölünmüş ekran ayırıcı"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Solda tam ekran"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Solda %70"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Solda %50"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Bu işlem sorunu düzeltmedi mi?\nİşlemi geri almak için dokunun"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kameranızda sorun yok mu? Kapatmak için dokunun."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Daha fazlasını görün ve yapın"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Bölünmüş ekran için başka bir uygulamayı sürükleyin"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Bölünmüş ekran için başka bir uygulamayı sürükleyin"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Yeniden konumlandırmak için uygulamanın dışına iki kez dokunun"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Anladım"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Daha fazla bilgi için genişletin."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Ekran görüntüsü"</string>
<string name="close_text" msgid="4986518933445178928">"Kapat"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menüyü kapat"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Menüyü Aç"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml
index 69bb608061e4..9e3e59b74c19 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Taşı"</string>
<string name="pip_expand" msgid="1051966011679297308">"Genişlet"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Daralt"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Kontroller için "<annotation icon="home_icon">" ANA SAYFA "</annotation>" düğmesine iki kez basın"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Kontroller için "<annotation icon="home_icon">"ANA EKRAN"</annotation>" düğmesine iki kez basın"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Pencere içinde pencere menüsü."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Sola taşı"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Sağa taşı"</string>
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index 02e726fbc3bf..fd825639f1e8 100644
--- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -15,34 +15,38 @@
limitations under the License.
-->
<resources>
- <!-- The dimensions to user for picture-in-picture action buttons. -->
- <dimen name="pip_menu_button_size">48dp</dimen>
- <dimen name="pip_menu_button_radius">20dp</dimen>
- <dimen name="pip_menu_icon_size">20dp</dimen>
- <dimen name="pip_menu_button_margin">4dp</dimen>
- <dimen name="pip_menu_button_wrapper_margin">26dp</dimen>
- <dimen name="pip_menu_border_width">4dp</dimen>
- <integer name="pip_menu_fade_animation_duration">500</integer>
+ <!-- The dimensions to use for tv window menu action buttons. -->
+ <dimen name="tv_window_menu_button_size">48dp</dimen>
+ <dimen name="tv_window_menu_button_radius">20dp</dimen>
+ <dimen name="tv_window_menu_icon_size">20dp</dimen>
+ <dimen name="tv_window_menu_button_margin">4dp</dimen>
+ <integer name="tv_window_menu_fade_animation_duration">500</integer>
<!-- The pip menu front border corner radius is 2dp smaller than
the background corner radius to hide the background from
showing through. -->
<dimen name="pip_menu_border_corner_radius">4dp</dimen>
<dimen name="pip_menu_background_corner_radius">6dp</dimen>
+ <dimen name="pip_menu_border_width">4dp</dimen>
<dimen name="pip_menu_outer_space">24dp</dimen>
+ <dimen name="pip_menu_button_start_end_offset">30dp</dimen>
<!-- outer space minus border width -->
<dimen name="pip_menu_outer_space_frame">20dp</dimen>
- <dimen name="pip_menu_arrow_size">24dp</dimen>
- <dimen name="pip_menu_arrow_elevation">5dp</dimen>
+ <dimen name="pip_menu_arrow_size">12dp</dimen>
+ <dimen name="pip_menu_arrow_elevation">1dp</dimen>
- <dimen name="pip_menu_elevation">1dp</dimen>
+ <dimen name="pip_menu_elevation_no_menu">1dp</dimen>
+ <dimen name="pip_menu_elevation_move_menu">7dp</dimen>
+ <dimen name="pip_menu_elevation_all_actions_menu">4dp</dimen>
<dimen name="pip_menu_edu_text_view_height">24dp</dimen>
<dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
<dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
- <integer name="pip_edu_text_show_duration_ms">10500</integer>
- <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
- <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
+ <integer name="pip_edu_text_scroll_times">2</integer>
+ <integer name="pip_edu_text_non_scroll_show_duration">10500</integer>
+ <integer name="pip_edu_text_start_scroll_delay">2000</integer>
+ <integer name="pip_edu_text_window_exit_animation_duration">1000</integer>
+ <integer name="pip_edu_text_view_exit_animation_duration">300</integer>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml
index c578283d21b8..78df129d8f50 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Налаштування"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Розділити екран"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Меню"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Меню \"Картинка в картинці\""</string>
<string name="pip_notification_title" msgid="1347104727641353453">"У додатку <xliff:g id="NAME">%s</xliff:g> є функція \"Картинка в картинці\""</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Щоб додаток <xliff:g id="NAME">%s</xliff:g> не використовував цю функцію, вимкніть її в налаштуваннях."</string>
<string name="pip_play" msgid="3496151081459417097">"Відтворити"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Змінити розмір"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Сховати"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Показати"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Додаток може не працювати в режимі розділеного екрана."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Додаток не підтримує розділення екрана."</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"Додаток може не працювати в режимі розділення екрана"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"Додаток не підтримує розділення екрана"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Цей додаток можна відкрити лише в одному вікні."</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="703810061635792791">"Розділювач екрана"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Розділювач екрана"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Розділювач екрана"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Ліве вікно на весь екран"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Ліве вікно на 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ліве вікно на 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Проблему не вирішено?\nНатисніть, щоб скасувати зміни"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Немає проблем із камерою? Торкніться, щоб закрити."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Більше простору та можливостей"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Щоб перейти в режим розділення екрана, перетягніть сюди інший додаток"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Щоб перейти в режим розділення екрана, перетягніть сюди інший додаток"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Щоб перемістити додаток, двічі торкніться області поза ним"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"ОK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Розгорніть, щоб дізнатися більше."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Знімок екрана"</string>
<string name="close_text" msgid="4986518933445178928">"Закрити"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Закрити меню"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Відкрити меню"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml
index 81a8285c58cf..5edb26977fe7 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Перемістити"</string>
<string name="pip_expand" msgid="1051966011679297308">"Розгорнути"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Згорнути"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Відкрити елементи керування: двічі натисніть "<annotation icon="home_icon">"HOME"</annotation></string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Відкрити елементи керування: двічі натисніть "<annotation icon="home_icon">"HOME"</annotation></string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Меню \"картинка в картинці\""</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Перемістити ліворуч"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Перемістити праворуч"</string>
diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml
index c40f34f4174e..ca1642488768 100644
--- a/libs/WindowManager/Shell/res/values-ur/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ur/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"ترتیبات"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"اسپلٹ اسکرین تک رسائی"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"مینیو"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"تصویر میں تصویر کا مینیو"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> تصویر میں تصویر میں ہے"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"اگر آپ نہیں چاہتے ہیں کہ <xliff:g id="NAME">%s</xliff:g> اس خصوصیت کا استعمال کرے تو ترتیبات کھولنے کے لیے تھپتھپا کر اسے آف کرے۔"</string>
<string name="pip_play" msgid="3496151081459417097">"چلائیں"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"سائز تبدیل کریں"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Stash"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Unstash"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"ممکن ہے کہ ایپ اسپلٹ اسکرین کے ساتھ کام نہ کرے۔"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ایپ سپلٹ اسکرین کو سپورٹ نہیں کرتی۔"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"ممکن ہے کہ ایپ اسپلٹ اسکرین کے ساتھ کام نہ کرے"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"ایپ اسپلٹ اسکرین کو سپورٹ نہیں کرتی ہے"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"یہ ایپ صرف 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="703810061635792791">"سپلٹ اسکرین تقسیم کار"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"اسپلٹ اسکرین ڈیوائیڈر"</string>
+ <string name="divider_title" msgid="1963391955593749442">"اسپلٹ اسکرین ڈیوائیڈر"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"بائیں فل اسکرین"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"بائیں %70"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"بائیں %50"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"یہ حل نہیں ہوا؟\nلوٹانے کیلئے تھپتھپائیں"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"کوئی کیمرے کا مسئلہ نہیں ہے؟ برخاست کرنے کیلئے تھپتھپائیں۔"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"دیکھیں اور بہت کچھ کریں"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"اسپلٹ اسکرین کے ليے دوسری ایپ میں گھسیٹیں"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"اسپلٹ اسکرین کے ليے دوسری ایپ میں گھسیٹیں"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"کسی ایپ کی پوزیشن تبدیل کرنے کے لیے اس ایپ کے باہر دو بار تھپتھپائیں"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"سمجھ آ گئی"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"مزید معلومات کے لیے پھیلائیں۔"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"اسکرین شاٹ"</string>
<string name="close_text" msgid="4986518933445178928">"بند کریں"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"مینیو بند کریں"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"مینو کھولیں"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml
index 6847f72d9062..d0f011c0b4cf 100644
--- a/libs/WindowManager/Shell/res/values-ur/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-ur/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"منتقل کریں"</string>
<string name="pip_expand" msgid="1051966011679297308">"پھیلائیں"</string>
<string name="pip_collapse" msgid="3903295106641385962">"سکیڑیں"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" کنٹرولز کے لیے "<annotation icon="home_icon">"ہوم "</annotation>" بٹن کو دو بار دبائیں"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"کنٹرولز کے لیے "<annotation icon="home_icon">"ہوم "</annotation>" کو دو بار دبائیں"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"تصویر میں تصویر کا مینیو۔"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"دائیں منتقل کریں"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"بائیں منتقل کریں"</string>
diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml
index 7f97ff2fbc93..c0dc03366cd6 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Sozlamalar"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Ajratilgan ekranga kirish"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Tasvir ustida tasvir menyusi"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> tasvir ustida tasvir rejimida"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ilovasi uchun bu funksiyani sozlamalar orqali faolsizlantirish mumkin."</string>
<string name="pip_play" msgid="3496151081459417097">"Ijro"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Oʻlchamini oʻzgartirish"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Berkitish"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Chiqarish"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Bu ilova ekranni ikkiga ajratish rejimini dastaklamaydi."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Bu ilova ekranni bo‘lish xususiyatini qo‘llab-quvvatlamaydi."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Ekranni ikkiga bo‘lish chizig‘i"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Ekranni ikkiga ajratish chizigʻi"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Ekranni ikkiga ajratish chizigʻi"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Chapda to‘liq ekran"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Chapda 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Chapda 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Tuzatilmadimi?\nQaytarish uchun bosing"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Kamera muammosizmi? Yopish uchun bosing."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Yana boshqa amallar"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Ekranni ikkiga ajratish uchun boshqa ilovani bu yerga torting"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Ekranni ikkiga ajratish uchun boshqa ilovani bu yerga torting"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Qayta joylash uchun ilova tashqarisiga ikki marta bosing"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Batafsil axborot olish uchun kengaytiring."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Skrinshot"</string>
<string name="close_text" msgid="4986518933445178928">"Yopish"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menyuni yopish"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Menyuni ochish"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml
index da953356628c..83fd8b4e5425 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Boshqa joyga olish"</string>
<string name="pip_expand" msgid="1051966011679297308">"Yoyish"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Yopish"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Boshqaruv uchun "<annotation icon="home_icon">"ASOSIY"</annotation>" tugmani ikki marta bosing"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Boshqaruv uchun "<annotation icon="home_icon">"ASOSIY"</annotation>" tugmani ikki marta bosing"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Tasvir ustida tasvir menyusi."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Chapga olish"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Oʻngga olish"</string>
diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml
index 4956c230d763..7d974005dd31 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Cài đặt"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Truy cập chế độ chia đôi màn hình"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Trình đơn hình trong hình."</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> đang ở chế độ ảnh trong ảnh"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Nếu bạn không muốn <xliff:g id="NAME">%s</xliff:g> sử dụng tính năng này, hãy nhấn để mở cài đặt và tắt tính năng này."</string>
<string name="pip_play" msgid="3496151081459417097">"Phát"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Đổi kích thước"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Ẩn"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Hiện"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Ứng dụng có thể không hoạt động với tính năng chia đôi màn hình."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Ứng dụng không hỗ trợ chia đôi màn hình."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"Ứng dụng này chỉ có thể mở 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="703810061635792791">"Bộ chia chia đôi màn hình"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Trình chia đôi màn hình"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Trình chia đôi màn hình"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Toàn màn hình bên trái"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Trái 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Trái 50%"</string>
@@ -77,12 +79,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bong bóng"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Quản lý"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Đã đóng bong bóng."</string>
- <string name="restart_button_description" msgid="6712141648865547958">"Nhấn để khởi động lại ứng dụng để có trải nghiệm xem tốt hơn."</string>
+ <string name="restart_button_description" msgid="6712141648865547958">"Nhấn để khởi động lại ứng dụng này để xem tốt hơn."</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Có vấn đề với máy ảnh?\nHãy nhấn để sửa lỗi"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Bạn chưa khắc phục vấn đề?\nHãy nhấn để hủy bỏ"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Không có vấn đề với máy ảnh? Hãy nhấn để đóng."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Xem và làm được nhiều việc hơn"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Kéo vào một ứng dụng khác để chia đôi màn hình"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Kéo một ứng dụng khác vào để chia đôi màn hình"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Nhấn đúp bên ngoài ứng dụng để đặt lại vị trí"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"OK"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Mở rộng để xem thêm thông tin."</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Ảnh chụp màn hình"</string>
<string name="close_text" msgid="4986518933445178928">"Đóng"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Đóng trình đơn"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Mở Trình đơn"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml
index 1f9260fdcff0..986690f0444c 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Di chuyển"</string>
<string name="pip_expand" msgid="1051966011679297308">"Mở rộng"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Thu gọn"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Nhấn đúp vào nút "<annotation icon="home_icon">" MÀN HÌNH CHÍNH "</annotation>" để mở trình đơn điều khiển"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Nhấn đúp vào nút "<annotation icon="home_icon">"MÀN HÌNH CHÍNH"</annotation>" để mở trình đơn điều khiển"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Trình đơn hình trong hình."</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Di chuyển sang trái"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Di chuyển sang phải"</string>
diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
index 3acbeebe8d79..d1f50dba1ce1 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"设置"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"进入分屏模式"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"菜单"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"画中画菜单"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>目前位于“画中画”中"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"如果您不想让“<xliff:g id="NAME">%s</xliff:g>”使用此功能,请点按以打开设置,然后关闭此功能。"</string>
<string name="pip_play" msgid="3496151081459417097">"播放"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"调整大小"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"隐藏"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消隐藏"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"应用可能无法在分屏模式下正常运行。"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"应用不支持分屏。"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"应用可能无法在分屏模式下正常运行"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"应用不支持分屏"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"此应用只能在 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="703810061635792791">"分屏分隔线"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"分屏分隔线"</string>
+ <string name="divider_title" msgid="1963391955593749442">"分屏分隔线"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左侧全屏"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左侧 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左侧 50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"没有解决此问题?\n点按即可恢复"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相机没有问题?点按即可忽略。"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"查看和处理更多任务"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"拖入另一个应用,即可使用分屏模式"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"拖入另一个应用,即可使用分屏模式"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"在某个应用外连续点按两次,即可调整它的位置"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展开即可了解详情。"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"屏幕截图"</string>
<string name="close_text" msgid="4986518933445178928">"关闭"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"关闭菜单"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"打开菜单"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml
index 399d639fe70f..4da96e89f136 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"移动"</string>
<string name="pip_expand" msgid="1051966011679297308">"展开"</string>
<string name="pip_collapse" msgid="3903295106641385962">"收起"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" 按两次"<annotation icon="home_icon">"主屏幕"</annotation>"按钮可查看相关控件"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"按两次"<annotation icon="home_icon">"主屏幕"</annotation>"按钮可查看相关控件"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"画中画菜单。"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"左移"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"右移"</string>
diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
index e67dde03a283..6f399e51be4d 100644
--- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
@@ -22,8 +22,9 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割螢幕"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"選單"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"畫中畫選單"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在畫中畫模式"</string>
- <string name="pip_notification_message" msgid="8854051911700302620">"如果你不想「<xliff:g id="NAME">%s</xliff:g>」使用此功能,請輕按以開啟設定,然後停用此功能。"</string>
+ <string name="pip_notification_message" msgid="8854051911700302620">"如果您不想「<xliff:g id="NAME">%s</xliff:g>」使用此功能,請輕按以開啟設定,然後停用此功能。"</string>
<string name="pip_play" msgid="3496151081459417097">"播放"</string>
<string name="pip_pause" msgid="690688849510295232">"暫停"</string>
<string name="pip_skip_to_next" msgid="8403429188794867653">"跳到下一個"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"調整大小"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"保護"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消保護"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"應用程式不支援分割畫面。"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"應用程式可能無法在分割螢幕中運作"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"應用程式不支援分割螢幕"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"此應用程式只可在 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="703810061635792791">"分割畫面分隔線"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"分割螢幕分隔線"</string>
+ <string name="divider_title" msgid="1963391955593749442">"分割螢幕分隔線"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"左邊全螢幕"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"左邊 70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左邊 50%"</string>
@@ -82,12 +84,12 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"未能修正問題?\n輕按即可還原"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相機冇問題?㩒一下就可以即可閂咗佢。"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"瀏覽更多內容及執行更多操作"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"拖入另一個應用程式即可分割螢幕"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"拖入另一個應用程式即可分割螢幕"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"在應用程式外輕按兩下即可調整位置"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"知道了"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展開即可查看詳情。"</string>
- <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"要重新啟動以改善檢視畫面嗎?"</string>
- <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"你可重新啟動應用程式,讓系統更新檢視畫面;但系統可能不會儲存目前進度及你作出的任何變更"</string>
+ <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"要重新啟動改善檢視畫面嗎?"</string>
+ <string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"您可重新啟動應用程式,讓系統更新檢視畫面;但系統可能不會儲存目前進度及您作出的任何變更"</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"取消"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"重新啟動"</string>
<string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"不要再顯示"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"螢幕截圖"</string>
<string name="close_text" msgid="4986518933445178928">"關閉"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"打開選單"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml
index acbc26d033cd..ce850ef3c675 100644
--- a/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"移動"</string>
<string name="pip_expand" msgid="1051966011679297308">"展開"</string>
<string name="pip_collapse" msgid="3903295106641385962">"收合"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">" 主畫面按鈕"</annotation>"即可顯示控制項"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"按兩下"<annotation icon="home_icon">" 主畫面按鈕"</annotation>"即可存取控制項"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"畫中畫選單。"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移"</string>
diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
index a9926b3f895f..4ca49e167118 100644
--- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割畫面"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"選單"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"子母畫面選單"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"「<xliff:g id="NAME">%s</xliff:g>」目前在子母畫面中"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"如果你不想讓「<xliff:g id="NAME">%s</xliff:g>」使用這項功能,請輕觸開啟設定頁面,然後停用此功能。"</string>
<string name="pip_play" msgid="3496151081459417097">"播放"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"調整大小"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"暫時隱藏"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"取消暫時隱藏"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"應用程式可能無法在分割畫面中運作。"</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"這個應用程式不支援分割畫面。"</string>
+ <string name="dock_forced_resizable" msgid="7429086980048964687">"應用程式可能無法在分割畫面中運作"</string>
+ <string name="dock_non_resizeble_failed_to_dock_text" msgid="2733543750291266047">"這個應用程式不支援分割畫面"</string>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"這個應用程式只能在 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="703810061635792791">"分割畫面分隔線"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"分割畫面分隔線"</string>
+ <string name="divider_title" msgid="1963391955593749442">"分割畫面分隔線"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"以全螢幕顯示左側畫面"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"以 70% 的螢幕空間顯示左側畫面"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"以 50% 的螢幕空間顯示左側畫面"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"未修正問題嗎?\n輕觸即可還原"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"相機沒問題嗎?輕觸即可關閉。"</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"瀏覽更多內容及執行更多操作"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"拖進另一個應用程式即可使用分割畫面模式"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"拖進另一個應用程式即可使用分割畫面模式"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"在應用程式外輕觸兩下即可調整位置"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"我知道了"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"展開即可查看詳細資訊。"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"螢幕截圖"</string>
<string name="close_text" msgid="4986518933445178928">"關閉"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"開啟選單"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml
index f8c683ec3a60..df870851e72b 100644
--- a/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"移動"</string>
<string name="pip_expand" msgid="1051966011679297308">"展開"</string>
<string name="pip_collapse" msgid="3903295106641385962">"收合"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" 按兩下"<annotation icon="home_icon">"主畫面按鈕"</annotation>"即可顯示控制選項"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"按兩下"<annotation icon="home_icon">"主畫面按鈕"</annotation>"即可存取控制選項"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"子母畫面選單。"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"向左移動"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"向右移動"</string>
diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml
index 4169122c7f7c..478b5a62c8a5 100644
--- a/libs/WindowManager/Shell/res/values-zu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zu/strings.xml
@@ -22,6 +22,7 @@
<string name="pip_phone_settings" msgid="5468987116750491918">"Amasethingi"</string>
<string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string>
<string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string>
+ <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Imenyu Yesithombe-Esithombeni"</string>
<string name="pip_notification_title" msgid="1347104727641353453">"U-<xliff:g id="NAME">%s</xliff:g> ungaphakathi kwesithombe esiphakathi kwesithombe"</string>
<string name="pip_notification_message" msgid="8854051911700302620">"Uma ungafuni i-<xliff:g id="NAME">%s</xliff:g> ukuthi isebenzise lesi sici, thepha ukuze uvule izilungiselelo uphinde uyivale."</string>
<string name="pip_play" msgid="3496151081459417097">"Dlala"</string>
@@ -31,12 +32,13 @@
<string name="accessibility_action_pip_resize" msgid="4623966104749543182">"Shintsha usayizi"</string>
<string name="accessibility_action_pip_stash" msgid="4060775037619702641">"Yenza isiteshi"</string>
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"Susa isiteshi"</string>
- <string name="dock_forced_resizable" msgid="1749750436092293116">"Izinhlelo zokusebenza kungenzeka zingasebenzi ngesikrini esihlukanisiwe."</string>
- <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"Uhlelo lokusebenza alusekeli isikrini esihlukanisiwe."</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>
<string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"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="703810061635792791">"Isihlukanisi sokuhlukanisa isikrini"</string>
+ <string name="accessibility_divider" msgid="6407584574218956849">"Isihlukanisi sokuhlukanisa isikrini"</string>
+ <string name="divider_title" msgid="1963391955593749442">"Isihlukanisi sokuhlukanisa isikrini"</string>
<string name="accessibility_action_divider_left_full" msgid="1792313656305328536">"Isikrini esigcwele esingakwesokunxele"</string>
<string name="accessibility_action_divider_left_70" msgid="8859845045360659250">"Kwesokunxele ngo-70%"</string>
<string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kwesokunxele ngo-50%"</string>
@@ -82,7 +84,7 @@
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Akuyilungisanga?\nThepha ukuze ubuyele"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Azikho izinkinga zekhamera? Thepha ukuze ucashise."</string>
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Bona futhi wenze okuningi"</string>
- <string name="letterbox_education_split_screen_text" msgid="6206339484068670830">"Hudula kwenye i-app mayelana nokuhlukanisa isikrini"</string>
+ <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Hudula kwenye i-app mayelana nokuhlukanisa isikrini"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Thepha kabili ngaphandle kwe-app ukuze uyimise kabusha"</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"Ngiyezwa"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"Nweba ukuze uthole ulwazi olwengeziwe"</string>
@@ -107,4 +109,5 @@
<string name="screenshot_text" msgid="1477704010087786671">"Isithombe-skrini"</string>
<string name="close_text" msgid="4986518933445178928">"Vala"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Vala Imenyu"</string>
+ <string name="expand_menu_text" msgid="3847736164494181168">"Vula Imenyu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml
index 20243a9dfc9c..34cc8f1f1647 100644
--- a/libs/WindowManager/Shell/res/values-zu/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values-zu/strings_tv.xml
@@ -24,7 +24,7 @@
<string name="pip_move" msgid="158770205886688553">"Hambisa"</string>
<string name="pip_expand" msgid="1051966011679297308">"Nweba"</string>
<string name="pip_collapse" msgid="3903295106641385962">"Goqa"</string>
- <string name="pip_edu_text" msgid="3672999496647508701">" Chofoza kabili "<annotation icon="home_icon">" IKHAYA"</annotation>" mayelana nezilawuli"</string>
+ <string name="pip_edu_text" msgid="7930546669915337998">"Chofoza kabili "<annotation icon="home_icon">" IKHAYA"</annotation>" mayelana nezilawuli"</string>
<string name="a11y_pip_menu_entered" msgid="5106343214776801614">"Imenyu yesithombe-esithombeni"</string>
<string name="a11y_action_pip_move_left" msgid="6612980937817141583">"Yisa kwesokunxele"</string>
<string name="a11y_action_pip_move_right" msgid="1119409122645529936">"Yisa kwesokudla"</string>
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 6fb70006e67f..171a6b2fe5fb 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -17,8 +17,8 @@
*/
-->
<resources>
- <color name="docked_divider_handle">#000000</color>
- <color name="split_divider_background">@color/taskbar_background</color>
+ <color name="docked_divider_handle">#ffffff</color>
+ <color name="split_divider_background">@color/taskbar_background_dark</color>
<drawable name="forced_resizable_background">#59000000</drawable>
<color name="minimize_dock_shadow_start">#60000000</color>
<color name="minimize_dock_shadow_end">#00000000</color>
@@ -39,7 +39,6 @@
<color name="compat_controls_text">@android:color/system_neutral1_50</color>
<!-- Letterbox Education -->
- <color name="letterbox_education_accent_primary">@android:color/system_accent1_100</color>
<color name="letterbox_education_text_secondary">@android:color/system_neutral2_200</color>
<!-- Letterbox Dialog -->
@@ -54,4 +53,19 @@
<color name="splash_screen_bg_light">#FFFFFF</color>
<color name="splash_screen_bg_dark">#000000</color>
<color name="splash_window_background_default">@color/splash_screen_bg_light</color>
+
+ <!-- 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_app_name_light">#EFF1F2</color>
+ <color name="desktop_mode_caption_app_name_dark">#1C1C17</color>
+ <color name="desktop_mode_caption_menu_text_color">#191C1D</color>
+ <color name="desktop_mode_caption_menu_buttons_color_inactive">#191C1D</color>
+ <color name="desktop_mode_caption_menu_buttons_color_active">#00677E</color>
+ <color name="desktop_mode_resize_veil_light">#EFF1F2</color>
+ <color name="desktop_mode_resize_veil_dark">#1C1C17</color>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml
index fa90fe36b545..5f7fb12c3002 100644
--- a/libs/WindowManager/Shell/res/values/colors_tv.xml
+++ b/libs/WindowManager/Shell/res/values/colors_tv.xml
@@ -15,15 +15,22 @@
~ limitations under the License.
-->
<resources>
- <color name="tv_pip_menu_icon_focused">#0E0E0F</color>
- <color name="tv_pip_menu_icon_unfocused">#F8F9FA</color>
- <color name="tv_pip_menu_icon_disabled">#80868B</color>
- <color name="tv_pip_menu_close_icon_bg_focused">#D93025</color>
- <color name="tv_pip_menu_close_icon_bg_unfocused">#D69F261F</color>
- <color name="tv_pip_menu_icon_bg_focused">#E8EAED</color>
- <color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color>
+ <color name="tv_window_menu_icon_focused">#0E0E0F</color>
+ <color name="tv_window_menu_icon_unfocused">#F8F9FA</color>
+
+ <color name="tv_window_menu_icon_disabled">#80868B</color>
+ <color name="tv_window_menu_close_icon_bg_focused">#D93025</color>
+ <color name="tv_window_menu_close_icon_bg_unfocused">#D69F261F</color>
+ <color name="tv_window_menu_icon_bg_focused">#E8EAED</color>
+ <color name="tv_window_menu_icon_bg_unfocused">#990E0E0F</color>
+
<color name="tv_pip_menu_focus_border">#E8EAED</color>
+ <color name="tv_pip_menu_dim_layer">#990E0E0F</color>
<color name="tv_pip_menu_background">#1E232C</color>
+ <!-- Normally, the arrow color would be the same as the focus border color. But due to
+ optical illusion that looks too dark on the screen. That's why we define a separate
+ (lighter) arrow color. -->
+ <color name="tv_pip_menu_arrow_color">#F1F3F4</color>
<color name="tv_pip_edu_text">#99D2E3FC</color>
<color name="tv_pip_edu_text_home_icon">#D2E3FC</color>
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 76eb0945d990..a3916b71592b 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -125,4 +125,7 @@
<!-- Whether the additional education about reachability is enabled -->
<bool name="config_letterboxIsReachabilityEducationEnabled">false</bool>
+
+ <!-- Whether DragAndDrop capability is enabled -->
+ <bool name="config_enableShellDragDrop">true</bool>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 9fd16461fa01..2be34c90a661 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -226,6 +226,10 @@
<dimen name="bubble_user_education_padding_end">58dp</dimen>
<!-- Padding between the bubble and the user education text. -->
<dimen name="bubble_user_education_stack_padding">16dp</dimen>
+ <!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. -->
+ <dimen name="bubblebar_size">72dp</dimen>
+ <!-- The size of the drag handle / menu shown along with a bubble bar expanded view. -->
+ <dimen name="bubblebar_expanded_view_menu_size">16dp</dimen>
<!-- Bottom and end margin for compat buttons. -->
<dimen name="compat_button_margin">24dp</dimen>
@@ -367,19 +371,34 @@
<!-- Height of button (32dp) + 2 * margin (5dp each). -->
<dimen name="freeform_decor_caption_height">42dp</dimen>
- <!-- Width of buttons (32dp each) + padding (128dp total). -->
- <dimen name="freeform_decor_caption_menu_width">256dp</dimen>
+ <!-- The width of the handle menu in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_width">216dp</dimen>
- <dimen name="freeform_decor_caption_menu_height">250dp</dimen>
+ <!-- The height of the handle menu's "App Info" pill in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_app_info_pill_height">52dp</dimen>
- <dimen name="freeform_resize_handle">30dp</dimen>
+ <!-- The height of the handle menu's "Windowing" pill in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_windowing_pill_height">52dp</dimen>
- <dimen name="freeform_resize_corner">44dp</dimen>
+ <!-- The height of the handle menu's "More Actions" pill in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_more_actions_pill_height">156dp</dimen>
- <!-- The radius of the caption menu shadow. -->
- <dimen name="caption_menu_shadow_radius">4dp</dimen>
+ <!-- The top margin of the handle menu in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_margin_top">4dp</dimen>
+
+ <!-- The start margin of the handle menu in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_margin_start">6dp</dimen>
+
+ <!-- The margin between pills of the handle menu in desktop mode. -->
+ <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen>
<!-- The radius of the caption menu corners. -->
- <dimen name="caption_menu_corner_radius">20dp</dimen>
+ <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen>
+ <!-- The radius of the caption menu shadow. -->
+ <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen>
+
+ <dimen name="freeform_resize_handle">30dp</dimen>
+
+ <dimen name="freeform_resize_corner">44dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index a201536da25a..b192fdf245e2 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -30,6 +30,9 @@
<!-- Title of menu shown over picture-in-picture. Used for accessibility. -->
<string name="pip_menu_title">Menu</string>
+ <!-- accessibility Title of menu shown over picture-in-picture [CHAR LIMIT=NONE] -->
+ <string name="pip_menu_accessibility_title">Picture-in-Picture Menu</string>
+
<!-- PiP BTW notification title. [CHAR LIMIT=50] -->
<string name="pip_notification_title"><xliff:g id="name" example="Google Maps">%s</xliff:g> is in picture-in-picture</string>
@@ -64,10 +67,10 @@
<string name="pip_phone_dismiss_hint">Drag down to dismiss</string>
<!-- Multi-Window strings -->
- <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed in split-screen and that things might crash/not work properly [CHAR LIMIT=NONE] -->
- <string name="dock_forced_resizable">App may not work with split-screen.</string>
+ <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed in split screen and that things might crash/not work properly [CHAR LIMIT=NONE] -->
+ <string name="dock_forced_resizable">App may not work with split screen</string>
<!-- Warning message when we try to dock a non-resizeable task and launch it in fullscreen instead [CHAR LIMIT=NONE] -->
- <string name="dock_non_resizeble_failed_to_dock_text">App does not support split-screen.</string>
+ <string name="dock_non_resizeble_failed_to_dock_text">App does not support split screen</string>
<!-- Warning message when we try to dock an app not supporting multiple instances split into multiple sides [CHAR LIMIT=NONE] -->
<string name="dock_multi_instances_not_supported_text">This app can only be opened in 1 window.</string>
<!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed on a secondary display and that things might crash/not work properly [CHAR LIMIT=NONE] -->
@@ -75,8 +78,10 @@
<!-- Warning message when we try to launch a non-resizeable activity on a secondary display and launch it on the primary instead. -->
<string name="activity_launch_on_secondary_display_failed_text">App does not support launch on secondary displays.</string>
- <!-- Accessibility label for the divider that separates the windows in split-screen mode [CHAR LIMIT=NONE] -->
- <string name="accessibility_divider">Split-screen divider</string>
+ <!-- Accessibility label and window tile for the divider that separates the windows in split screen mode [CHAR LIMIT=NONE] -->
+ <string name="accessibility_divider">Split screen divider</string>
+ <!-- Accessibility window title for the split screen divider window [CHAR LIMIT=NONE] -->
+ <string name="divider_title">Split screen divider</string>
<!-- Accessibility action for moving docked stack divider to make the left screen full screen [CHAR LIMIT=NONE] -->
<string name="accessibility_action_divider_left_full">Left full screen</string>
@@ -186,7 +191,7 @@
<string name="letterbox_education_dialog_title">See and do more</string>
<!-- Description of the split screen action. [CHAR LIMIT=NONE] -->
- <string name="letterbox_education_split_screen_text">Drag in another app for split-screen</string>
+ <string name="letterbox_education_split_screen_text">Drag in another app for split screen</string>
<!-- Description of the reposition app action. [CHAR LIMIT=NONE] -->
<string name="letterbox_education_reposition_text">Double-tap outside an app to reposition it</string>
@@ -257,4 +262,6 @@
<string name="close_text">Close</string>
<!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
<string name="collapse_menu_text">Close Menu</string>
+ <!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] -->
+ <string name="expand_menu_text">Open Menu</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml
index 2b7a13eac6ca..8f806cf56c9b 100644
--- a/libs/WindowManager/Shell/res/values/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values/strings_tv.xml
@@ -42,8 +42,8 @@
<!-- Educative text instructing the user to double press the HOME button to access the pip
controls menu [CHAR LIMIT=50] -->
- <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for
- controls </string>
+ <string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for
+ controls</string>
<!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] -->
<string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 6e1185559a50..d902fd49ba60 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -30,25 +30,31 @@
<item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
</style>
- <style name="CaptionButtonStyle">
- <item name="android:layout_width">32dp</item>
- <item name="android:layout_height">32dp</item>
- <item name="android:layout_margin">5dp</item>
- <item name="android:padding">4dp</item>
+ <style name="DesktopModeHandleMenuActionButton">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">52dp</item>
+ <item name="android:gravity">start|center_vertical</item>
+ <item name="android:padding">16dp</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textFontWeight">500</item>
+ <item name="android:textColor">@color/desktop_mode_caption_menu_text_color</item>
+ <item name="android:drawablePadding">16dp</item>
+ <item name="android:background">?android:selectableItemBackground</item>
</style>
- <style name="CaptionWindowingButtonStyle">
- <item name="android:layout_width">40dp</item>
- <item name="android:layout_height">40dp</item>
- <item name="android:padding">4dp</item>
+ <style name="DesktopModeHandleMenuWindowingButton">
+ <item name="android:layout_width">48dp</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:padding">14dp</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:background">?android:selectableItemBackgroundBorderless</item>
</style>
- <style name="CaptionMenuButtonStyle" parent="@style/Widget.AppCompat.Button.Borderless">
- <item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">52dp</item>
- <item name="android:layout_marginStart">10dp</item>
+ <style name="CaptionButtonStyle">
+ <item name="android:layout_width">32dp</item>
+ <item name="android:layout_height">32dp</item>
+ <item name="android:layout_margin">5dp</item>
<item name="android:padding">4dp</item>
- <item name="android:gravity">start|center_vertical</item>
</style>
<style name="DockedDividerBackground">
@@ -87,74 +93,44 @@
<style name="RestartDialogTitleText">
<item name="android:textSize">24sp</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
- <item name="android:lineSpacingExtra">2sp</item>
- <item name="android:textAppearance">
- @*android:style/TextAppearance.DeviceDefault.Headline
- </item>
- <item name="android:fontFamily">
- @*android:string/config_bodyFontFamilyMedium
- </item>
+ <item name="android:lineSpacingExtra">8sp</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
</style>
- <style name="RestartDialogBodyText">
+ <style name="RestartDialogBodyStyle">
<item name="android:textSize">14sp</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+
+ <style name="RestartDialogBodyText" parent="RestartDialogBodyStyle">
<item name="android:letterSpacing">0.02</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
- <item name="android:lineSpacingExtra">2sp</item>
- <item name="android:textAppearance">
- @*android:style/TextAppearance.DeviceDefault.Body2
- </item>
- <item name="android:fontFamily">
- @*android:string/config_bodyFontFamily
- </item>
+ <item name="android:lineSpacingExtra">6sp</item>
</style>
- <style name="RestartDialogCheckboxText">
- <item name="android:textSize">16sp</item>
+ <style name="RestartDialogCheckboxText" parent="RestartDialogBodyStyle">
<item name="android:textColor">?android:attr/textColorPrimary</item>
- <item name="android:lineSpacingExtra">4sp</item>
- <item name="android:textAppearance">
- @*android:style/TextAppearance.DeviceDefault.Headline
- </item>
- <item name="android:fontFamily">
- @*android:string/config_bodyFontFamilyMedium
- </item>
+ <item name="android:lineSpacingExtra">6sp</item>
</style>
- <style name="RestartDialogDismissButton">
+ <style name="RestartDialogDismissButton" parent="RestartDialogBodyStyle">
<item name="android:lineSpacingExtra">2sp</item>
- <item name="android:textSize">14sp</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
- <item name="android:textAppearance">
- @*android:style/TextAppearance.DeviceDefault.Body2
- </item>
- <item name="android:fontFamily">
- @*android:string/config_bodyFontFamily
- </item>
</style>
- <style name="RestartDialogConfirmButton">
+ <style name="RestartDialogConfirmButton" parent="RestartDialogBodyStyle">
<item name="android:lineSpacingExtra">2sp</item>
- <item name="android:textSize">14sp</item>
<item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
- <item name="android:textAppearance">
- @*android:style/TextAppearance.DeviceDefault.Body2
- </item>
- <item name="android:fontFamily">
- @*android:string/config_bodyFontFamily
- </item>
</style>
- <style name="ReachabilityEduHandLayout">
+ <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat.Light">
<item name="android:focusable">false</item>
<item name="android:focusableInTouchMode">false</item>
<item name="android:background">@android:color/transparent</item>
- <item name="android:contentDescription">@string/restart_button_description</item>
- <item name="android:visibility">invisible</item>
<item name="android:lineSpacingExtra">-1sp</item>
<item name="android:textSize">12sp</item>
<item name="android:textAlignment">center</item>
- <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
<item name="android:textAppearance">
@*android:style/TextAppearance.DeviceDefault.Body2
</item>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
index b085b73d78ce..34bf9e0dd98f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
@@ -76,6 +76,7 @@ public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer {
+ " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId));
}
+ leash.setUnreleasedWarningCallSite("RootDisplayAreaOrganizer.onDisplayAreaAppeared");
mDisplayAreasInfo.put(displayId, displayAreaInfo);
mLeashes.put(displayId, leash);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
index ca977ed2cb94..410ae78dba1b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
@@ -122,6 +122,8 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer {
+ " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId));
}
+ leash.setUnreleasedWarningCallSite(
+ "RootTaskDisplayAreaOrganizer.onDisplayAreaAppeared");
mDisplayAreasInfo.put(displayId, displayAreaInfo);
mLeashes.put(displayId, leash);
@@ -179,6 +181,17 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer {
}
/**
+ * Returns the list of display ids that are tracked by a {@link DisplayAreaInfo}
+ */
+ public int[] getDisplayIds() {
+ int[] displayIds = new int[mDisplayAreasInfo.size()];
+ for (int i = 0; i < mDisplayAreasInfo.size(); i++) {
+ displayIds[i] = mDisplayAreasInfo.keyAt(i);
+ }
+ return displayIds;
+ }
+
+ /**
* Returns the {@link DisplayAreaInfo} of the {@link DisplayAreaInfo#displayId}.
*/
@Nullable
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 97a9fede22d5..9aac694e41bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -42,6 +42,7 @@ import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceControl;
import android.window.ITaskOrganizerController;
+import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskAppearedInfo;
@@ -420,16 +421,16 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
/**
* Removes listener.
*/
- public void removeLocusIdListener(FocusListener listener) {
+ public void removeFocusListener(FocusListener listener) {
synchronized (mLock) {
mFocusListeners.remove(listener);
}
}
@Override
- public void addStartingWindow(StartingWindowInfo info, IBinder appToken) {
+ public void addStartingWindow(StartingWindowInfo info) {
if (mStartingWindow != null) {
- mStartingWindow.addStartingWindow(info, appToken);
+ mStartingWindow.addStartingWindow(info);
}
}
@@ -463,6 +464,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+ if (leash != null) {
+ leash.setUnreleasedWarningCallSite("ShellTaskOrganizer.onTaskAppeared");
+ }
synchronized (mLock) {
onTaskAppeared(new TaskAppearedInfo(taskInfo, leash));
}
@@ -489,7 +493,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
* Take a screenshot of a task.
*/
public void screenshotTask(RunningTaskInfo taskInfo, Rect crop,
- Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) {
+ Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) {
final TaskAppearedInfo info = mTasks.get(taskInfo.taskId);
if (info == null) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
deleted file mode 100644
index 07d501201105..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell;
-
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.os.IBinder;
-import android.util.Slog;
-import android.view.SurfaceControl;
-import android.view.WindowManager;
-import android.window.TransitionInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.transition.Transitions;
-
-import java.util.ArrayList;
-
-/**
- * Handles Shell Transitions that involve TaskView tasks.
- */
-public class TaskViewTransitions implements Transitions.TransitionHandler {
- private static final String TAG = "TaskViewTransitions";
-
- private final ArrayList<TaskView> mTaskViews = new ArrayList<>();
- private final ArrayList<PendingTransition> mPending = new ArrayList<>();
- private final Transitions mTransitions;
- private final boolean[] mRegistered = new boolean[]{ false };
-
- /**
- * TaskView makes heavy use of startTransition. Only one shell-initiated transition can be
- * in-flight (collecting) at a time (because otherwise, the operations could get merged into
- * a single transition). So, keep a queue here until we add a queue in server-side.
- */
- private static class PendingTransition {
- final @WindowManager.TransitionType int mType;
- final WindowContainerTransaction mWct;
- final @NonNull TaskView mTaskView;
- IBinder mClaimed;
-
- PendingTransition(@WindowManager.TransitionType int type,
- @Nullable WindowContainerTransaction wct, @NonNull TaskView taskView) {
- mType = type;
- mWct = wct;
- mTaskView = taskView;
- }
- }
-
- public TaskViewTransitions(Transitions transitions) {
- mTransitions = transitions;
- // Defer registration until the first TaskView because we want this to be the "first" in
- // priority when handling requests.
- // TODO(210041388): register here once we have an explicit ordering mechanism.
- }
-
- void addTaskView(TaskView tv) {
- synchronized (mRegistered) {
- if (!mRegistered[0]) {
- mRegistered[0] = true;
- mTransitions.addHandler(this);
- }
- }
- mTaskViews.add(tv);
- }
-
- void removeTaskView(TaskView tv) {
- mTaskViews.remove(tv);
- // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell
- }
-
- boolean isEnabled() {
- return mTransitions.isRegistered();
- }
-
- /**
- * Looks through the pending transitions for one matching `taskView`.
- * @param taskView the pending transition should be for this.
- * @param closing When true, this only returns a pending transition of the close/hide type.
- * Otherwise it selects open/show.
- * @param latest When true, this will only check the most-recent pending transition for the
- * specified taskView. If it doesn't match `closing`, this will return null even
- * if there is a match earlier. The idea behind this is to check the state of
- * the taskviews "as if all transitions already happened".
- */
- private PendingTransition findPending(TaskView taskView, boolean closing, boolean latest) {
- for (int i = mPending.size() - 1; i >= 0; --i) {
- if (mPending.get(i).mTaskView != taskView) continue;
- if (Transitions.isClosingType(mPending.get(i).mType) == closing) {
- return mPending.get(i);
- }
- if (latest) {
- return null;
- }
- }
- return null;
- }
-
- private PendingTransition findPending(IBinder claimed) {
- for (int i = 0; i < mPending.size(); ++i) {
- if (mPending.get(i).mClaimed != claimed) continue;
- return mPending.get(i);
- }
- return null;
- }
-
- /** @return whether there are pending transitions on TaskViews. */
- public boolean hasPending() {
- return !mPending.isEmpty();
- }
-
- @Override
- public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
- @Nullable TransitionRequestInfo request) {
- final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
- if (triggerTask == null) {
- return null;
- }
- final TaskView taskView = findTaskView(triggerTask);
- if (taskView == null) return null;
- // Opening types should all be initiated by shell
- if (!Transitions.isClosingType(request.getType())) return null;
- PendingTransition pending = findPending(taskView, true /* closing */, false /* latest */);
- if (pending == null) {
- pending = new PendingTransition(request.getType(), null, taskView);
- }
- if (pending.mClaimed != null) {
- throw new IllegalStateException("Task is closing in 2 collecting transitions?"
- + " This state doesn't make sense");
- }
- pending.mClaimed = transition;
- return new WindowContainerTransaction();
- }
-
- private TaskView findTaskView(ActivityManager.RunningTaskInfo taskInfo) {
- for (int i = 0; i < mTaskViews.size(); ++i) {
- if (mTaskViews.get(i).getTaskInfo() == null) continue;
- if (taskInfo.token.equals(mTaskViews.get(i).getTaskInfo().token)) {
- return mTaskViews.get(i);
- }
- }
- return null;
- }
-
- void startTaskView(WindowContainerTransaction wct, TaskView taskView) {
- mPending.add(new PendingTransition(TRANSIT_OPEN, wct, taskView));
- startNextTransition();
- }
-
- void setTaskViewVisible(TaskView taskView, boolean visible) {
- PendingTransition pending = findPending(taskView, !visible, true /* latest */);
- if (pending != null) {
- // Already opening or creating a task, so no need to do anything here.
- return;
- }
- if (taskView.getTaskInfo() == null) {
- // Nothing to update, task is not yet available
- return;
- }
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */);
- pending = new PendingTransition(
- visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView);
- mPending.add(pending);
- startNextTransition();
- // visibility is reported in transition.
- }
-
- private void startNextTransition() {
- if (mPending.isEmpty()) return;
- final PendingTransition pending = mPending.get(0);
- if (pending.mClaimed != null) {
- // Wait for this to start animating.
- return;
- }
- pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this);
- }
-
- @Override
- public boolean startAnimation(@NonNull IBinder transition,
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- PendingTransition pending = findPending(transition);
- if (pending == null) return false;
- mPending.remove(pending);
- TaskView taskView = pending.mTaskView;
- final ArrayList<TransitionInfo.Change> tasks = new ArrayList<>();
- for (int i = 0; i < info.getChanges().size(); ++i) {
- final TransitionInfo.Change chg = info.getChanges().get(i);
- if (chg.getTaskInfo() == null) continue;
- tasks.add(chg);
- }
- if (tasks.isEmpty()) {
- Slog.e(TAG, "Got a TaskView transition with no task.");
- return false;
- }
- WindowContainerTransaction wct = null;
- for (int i = 0; i < tasks.size(); ++i) {
- TransitionInfo.Change chg = tasks.get(i);
- if (Transitions.isClosingType(chg.getMode())) {
- final boolean isHide = chg.getMode() == TRANSIT_TO_BACK;
- TaskView tv = findTaskView(chg.getTaskInfo());
- if (tv == null) {
- throw new IllegalStateException("TaskView transition is closing a "
- + "non-taskview task ");
- }
- if (isHide) {
- tv.prepareHideAnimation(finishTransaction);
- } else {
- tv.prepareCloseAnimation();
- }
- } else if (Transitions.isOpeningType(chg.getMode())) {
- final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN;
- if (wct == null) wct = new WindowContainerTransaction();
- TaskView tv = taskView;
- if (!taskIsNew) {
- tv = findTaskView(chg.getTaskInfo());
- if (tv == null) {
- throw new IllegalStateException("TaskView transition is showing a "
- + "non-taskview task ");
- }
- }
- tv.prepareOpenAnimation(taskIsNew, startTransaction, finishTransaction,
- chg.getTaskInfo(), chg.getLeash(), wct);
- } else {
- throw new IllegalStateException("Claimed transition isn't an opening or closing"
- + " type: " + chg.getMode());
- }
- }
- // No animation, just show it immediately.
- startTransaction.apply();
- finishTransaction.apply();
- finishCallback.onTransitionFinished(wct, null /* wctCB */);
- startNextTransition();
- return true;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 00b9fcede4ca..39f861de1ba0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -22,7 +22,6 @@ import static android.graphics.Matrix.MTRANS_Y;
import android.annotation.CallSuper;
import android.graphics.Point;
import android.graphics.Rect;
-import android.view.Choreographer;
import android.view.SurfaceControl;
import android.view.animation.Animation;
import android.view.animation.Transformation;
@@ -30,7 +29,7 @@ import android.window.TransitionInfo;
import androidx.annotation.NonNull;
-import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
/**
* Wrapper to handle the ActivityEmbedding animation update in one
@@ -61,7 +60,7 @@ class ActivityEmbeddingAnimationAdapter {
private final Rect mContentBounds = new Rect();
/** Offset relative to the window parent surface for {@link #mContentBounds}. */
@NonNull
- private final Point mContentRelOffset = new Point();
+ final Point mContentRelOffset = new Point();
@NonNull
final Transformation mTransformation = new Transformation();
@@ -71,12 +70,11 @@ class ActivityEmbeddingAnimationAdapter {
final float[] mVecs = new float[4];
@NonNull
final Rect mRect = new Rect();
- private boolean mIsFirstFrame = true;
private int mOverrideLayer = LAYER_NO_OVERRIDE;
ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
- @NonNull TransitionInfo.Change change) {
- this(animation, change, change.getLeash(), change.getEndAbsBounds());
+ @NonNull TransitionInfo.Change change, @NonNull TransitionInfo.Root root) {
+ this(animation, change, change.getLeash(), change.getEndAbsBounds(), root);
}
/**
@@ -87,25 +85,34 @@ class ActivityEmbeddingAnimationAdapter {
*/
ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
@NonNull TransitionInfo.Change change, @NonNull SurfaceControl leash,
- @NonNull Rect wholeAnimationBounds) {
+ @NonNull Rect wholeAnimationBounds, @NonNull TransitionInfo.Root root) {
mAnimation = animation;
mChange = change;
mLeash = leash;
mWholeAnimationBounds.set(wholeAnimationBounds);
- if (Transitions.isClosingType(change.getMode())) {
+
+ final Rect startBounds = change.getStartAbsBounds();
+ final Rect endBounds = change.getEndAbsBounds();
+ if (change.getParent() != null) {
+ mContentRelOffset.set(change.getEndRelOffset());
+ } else {
+ // Change leash has been reparented to the root if its parent is not in the transition.
+ // Because it is reparented to the root, the actual offset should be its relative
+ // position to the root instead. See Transitions#setupAnimHierarchy.
+ final Point rootOffset = root.getOffset();
+ mContentRelOffset.set(endBounds.left - rootOffset.x, endBounds.top - rootOffset.y);
+ }
+
+ if (TransitionUtil.isClosingType(change.getMode())) {
// When it is closing, we want to show the content at the start position in case the
// window is resizing as well. For example, when the activities is changing from split
// to stack, the bottom TaskFragment will be resized to fullscreen when hiding.
- final Rect startBounds = change.getStartAbsBounds();
- final Rect endBounds = change.getEndAbsBounds();
mContentBounds.set(startBounds);
- mContentRelOffset.set(change.getEndRelOffset());
mContentRelOffset.offset(
- startBounds.left - endBounds.left,
- startBounds.top - endBounds.top);
+ startBounds.left - endBounds.left,
+ startBounds.top - endBounds.top);
} else {
mContentBounds.set(change.getEndAbsBounds());
- mContentRelOffset.set(change.getEndRelOffset());
}
}
@@ -117,20 +124,21 @@ class ActivityEmbeddingAnimationAdapter {
mOverrideLayer = layer;
}
- /** Called on frame update. */
- final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
- if (mIsFirstFrame) {
- t.show(mLeash);
- if (mOverrideLayer != LAYER_NO_OVERRIDE) {
- t.setLayer(mLeash, mOverrideLayer);
- }
- mIsFirstFrame = false;
+ /** Called to prepare for the starting state. */
+ final void prepareForFirstFrame(@NonNull SurfaceControl.Transaction startTransaction) {
+ startTransaction.show(mLeash);
+ if (mOverrideLayer != LAYER_NO_OVERRIDE) {
+ startTransaction.setLayer(mLeash, mOverrideLayer);
}
+ mAnimation.getTransformationAt(0, mTransformation);
+ onAnimationUpdateInner(startTransaction);
+ }
+ /** Called on frame update. */
+ final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
// Extract the transformation to the current time.
mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
mTransformation);
- t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
onAnimationUpdateInner(t);
}
@@ -184,8 +192,8 @@ class ActivityEmbeddingAnimationAdapter {
static class SnapshotAdapter extends ActivityEmbeddingAnimationAdapter {
SnapshotAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
- @NonNull SurfaceControl snapshotLeash) {
- super(animation, change, snapshotLeash, change.getEndAbsBounds());
+ @NonNull SurfaceControl snapshotLeash, @NonNull TransitionInfo.Root root) {
+ super(animation, change, snapshotLeash, change.getEndAbsBounds(), root);
}
@Override
@@ -211,14 +219,14 @@ class ActivityEmbeddingAnimationAdapter {
*/
static class BoundsChangeAdapter extends ActivityEmbeddingAnimationAdapter {
- BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change) {
- super(animation, change);
+ BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+ @NonNull TransitionInfo.Root root) {
+ super(animation, change, root);
}
@Override
void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
- final Point offset = mChange.getEndRelOffset();
- mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+ mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y);
t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
t.setAlpha(mLeash, mTransformation.getAlpha());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 164d2f149931..4d87c9583f64 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
+import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
@@ -32,6 +33,7 @@ import android.graphics.Rect;
import android.os.IBinder;
import android.util.ArraySet;
import android.util.Log;
+import android.view.Choreographer;
import android.view.SurfaceControl;
import android.view.animation.Animation;
import android.window.TransitionInfo;
@@ -41,8 +43,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationAdapter.SnapshotAdapter;
import com.android.wm.shell.common.ScreenshotUtils;
-import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.List;
@@ -58,6 +61,9 @@ class ActivityEmbeddingAnimationRunner {
@VisibleForTesting
final ActivityEmbeddingAnimationSpec mAnimationSpec;
+ @Nullable
+ private Animator mActiveAnimator;
+
ActivityEmbeddingAnimationRunner(@NonNull Context context,
@NonNull ActivityEmbeddingController controller) {
mController = controller;
@@ -72,8 +78,10 @@ class ActivityEmbeddingAnimationRunner {
// applied to make sure the surface is ready.
final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks =
new ArrayList<>();
- final Animator animator = createAnimator(info, startTransaction, finishTransaction,
+ final Animator animator = createAnimator(info, startTransaction,
+ finishTransaction,
() -> mController.onAnimationFinished(transition), postStartTransactionCallbacks);
+ mActiveAnimator = animator;
// Start the animation.
if (!postStartTransactionCallbacks.isEmpty()) {
@@ -95,6 +103,17 @@ class ActivityEmbeddingAnimationRunner {
}
}
+ void cancelAnimationFromMerge() {
+ if (mActiveAnimator == null) {
+ Log.e(TAG,
+ "No active ActivityEmbedding animator running but mergeAnimation is "
+ + "trying to cancel one."
+ );
+ return;
+ }
+ mActiveAnimator.end();
+ }
+
/**
* Sets transition animation scale settings value.
* @param scale The setting value of transition animation scale.
@@ -130,11 +149,13 @@ class ActivityEmbeddingAnimationRunner {
animator.addUpdateListener((anim) -> {
// Update all adapters in the same transaction.
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
}
t.apply();
});
+ prepareForFirstFrame(startTransaction, adapters);
}
animator.setDuration(duration);
animator.addListener(new Animator.AnimatorListener() {
@@ -148,6 +169,7 @@ class ActivityEmbeddingAnimationRunner {
adapter.onAnimationEnd(t);
}
t.apply();
+ mActiveAnimator = null;
animationFinishCallback.run();
}
@@ -181,24 +203,24 @@ class ActivityEmbeddingAnimationRunner {
if (isChangeTransition) {
return createChangeAnimationAdapters(info, startTransaction);
}
- if (Transitions.isClosingType(info.getType())) {
- return createCloseAnimationAdapters(info);
+ if (TransitionUtil.isClosingType(info.getType())) {
+ return createCloseAnimationAdapters(info, startTransaction);
}
- return createOpenAnimationAdapters(info);
+ return createOpenAnimationAdapters(info, startTransaction);
}
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
- @NonNull TransitionInfo info) {
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
return createOpenCloseAnimationAdapters(info, true /* isOpening */,
- mAnimationSpec::loadOpenAnimation);
+ mAnimationSpec::loadOpenAnimation, startTransaction);
}
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
- @NonNull TransitionInfo info) {
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
return createOpenCloseAnimationAdapters(info, false /* isOpening */,
- mAnimationSpec::loadCloseAnimation);
+ mAnimationSpec::loadCloseAnimation, startTransaction);
}
/**
@@ -208,7 +230,8 @@ class ActivityEmbeddingAnimationRunner {
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
@NonNull TransitionInfo info, boolean isOpening,
- @NonNull AnimationProvider animationProvider) {
+ @NonNull AnimationProvider animationProvider,
+ @NonNull SurfaceControl.Transaction startTransaction) {
// We need to know if the change window is only a partial of the whole animation screen.
// If so, we will need to adjust it to make the whole animation screen looks like one.
final List<TransitionInfo.Change> openingChanges = new ArrayList<>();
@@ -216,11 +239,13 @@ class ActivityEmbeddingAnimationRunner {
final Rect openingWholeScreenBounds = new Rect();
final Rect closingWholeScreenBounds = new Rect();
for (TransitionInfo.Change change : info.getChanges()) {
- if (Transitions.isOpeningType(change.getMode())) {
+ if (TransitionUtil.isOpeningType(change.getMode())) {
openingChanges.add(change);
openingWholeScreenBounds.union(change.getEndAbsBounds());
} else {
closingChanges.add(change);
+ // Also union with the start bounds because the closing transition may be shrunk.
+ closingWholeScreenBounds.union(change.getStartAbsBounds());
closingWholeScreenBounds.union(change.getEndAbsBounds());
}
}
@@ -238,6 +263,18 @@ class ActivityEmbeddingAnimationRunner {
adapters.add(adapter);
}
for (TransitionInfo.Change change : closingChanges) {
+ if (shouldUseSnapshotAnimationForClosingChange(change)) {
+ SurfaceControl screenshot = getOrCreateScreenshot(change, change, startTransaction);
+ if (screenshot != null) {
+ final SnapshotAdapter snapshotAdapter = new SnapshotAdapter(
+ createShowSnapshotForClosingAnimation(), change, screenshot,
+ TransitionUtil.getRootFor(change, info));
+ if (!isOpening) {
+ snapshotAdapter.overrideLayer(offsetLayer++);
+ }
+ adapters.add(snapshotAdapter);
+ }
+ }
final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
info, change, animationProvider, closingWholeScreenBounds);
if (!isOpening) {
@@ -248,6 +285,31 @@ class ActivityEmbeddingAnimationRunner {
return adapters;
}
+ /**
+ * Returns whether we should use snapshot animation for the closing change.
+ * It's usually because the end bounds of the closing change are shrunk, which leaves a black
+ * area in the transition.
+ */
+ static boolean shouldUseSnapshotAnimationForClosingChange(
+ @NonNull TransitionInfo.Change closingChange) {
+ // Only check closing type because we only take screenshot for closing bounds-changing
+ // changes.
+ if (!TransitionUtil.isClosingType(closingChange.getMode())) {
+ return false;
+ }
+ // Don't need to take screenshot if there's no bounds change.
+ return !closingChange.getStartAbsBounds().equals(closingChange.getEndAbsBounds());
+ }
+
+ /** Sets the first frame to the {@code startTransaction} to avoid any flicker on start. */
+ private void prepareForFirstFrame(@NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
+ startTransaction.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.prepareForFirstFrame(startTransaction);
+ }
+ }
+
/** Adds edge extension to the surfaces that have such an animation property. */
private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -259,7 +321,7 @@ class ActivityEmbeddingAnimationRunner {
continue;
}
final TransitionInfo.Change change = adapter.mChange;
- if (Transitions.isOpeningType(adapter.mChange.getMode())) {
+ if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) {
// Need to screenshot after startTransaction is applied otherwise activity
// may not be visible or ready yet.
postStartTransactionCallbacks.add(
@@ -294,7 +356,7 @@ class ActivityEmbeddingAnimationRunner {
@NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) {
final Animation animation = animationProvider.get(info, change, wholeAnimationBounds);
return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(),
- wholeAnimationBounds);
+ wholeAnimationBounds, TransitionUtil.getRootFor(change, info));
}
@NonNull
@@ -331,7 +393,7 @@ class ActivityEmbeddingAnimationRunner {
// When the parent window is also included in the transition as an opening window,
// we would like to animate the parent window instead.
final TransitionInfo.Change parentChange = info.getChange(parentToken);
- if (parentChange != null && Transitions.isOpeningType(parentChange.getMode())) {
+ if (parentChange != null && TransitionUtil.isOpeningType(parentChange.getMode())) {
// We won't create a separate animation for the parent, but to animate the
// parent for the child resizing.
handledChanges.add(parentChange);
@@ -343,6 +405,12 @@ class ActivityEmbeddingAnimationRunner {
// size.
parentBounds.union(boundsAnimationChange.getStartAbsBounds());
parentBounds.union(boundsAnimationChange.getEndAbsBounds());
+ if (boundsAnimationChange != change) {
+ // Union the change starting bounds in case the activity is resized and reparented
+ // to a TaskFragment. In that case, the TaskFragment may not cover the activity's
+ // starting bounds.
+ parentBounds.union(change.getStartAbsBounds());
+ }
// There are two animations in the array. The first one is for the start leash
// (snapshot), and the second one is for the end leash (TaskFragment).
@@ -357,17 +425,18 @@ class ActivityEmbeddingAnimationRunner {
// boundsAnimationChange.
final SurfaceControl screenshotLeash = getOrCreateScreenshot(change,
boundsAnimationChange, startTransaction);
+ final TransitionInfo.Root root = TransitionUtil.getRootFor(change, info);
if (screenshotLeash != null) {
// Adapter for the starting screenshot leash.
// The screenshot leash will be removed in SnapshotAdapter#onAnimationEnd
adapters.add(new ActivityEmbeddingAnimationAdapter.SnapshotAdapter(
- animations[0], change, screenshotLeash));
+ animations[0], change, screenshotLeash, root));
} else {
Log.e(TAG, "Failed to take screenshot for change=" + change);
}
// Adapter for the ending bounds changed leash.
adapters.add(new ActivityEmbeddingAnimationAdapter.BoundsChangeAdapter(
- animations[1], boundsAnimationChange));
+ animations[1], boundsAnimationChange, root));
}
if (parentBounds.isEmpty()) {
@@ -392,14 +461,15 @@ class ActivityEmbeddingAnimationRunner {
// No-op if it will be covered by the changing parent window, or it is a changing
// window without bounds change.
animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
- } else if (Transitions.isClosingType(change.getMode())) {
+ } else if (TransitionUtil.isClosingType(change.getMode())) {
animation = mAnimationSpec.createChangeBoundsCloseAnimation(change, parentBounds);
shouldShouldBackgroundColor = false;
} else {
animation = mAnimationSpec.createChangeBoundsOpenAnimation(change, parentBounds);
shouldShouldBackgroundColor = false;
}
- adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
+ adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change,
+ TransitionUtil.getRootFor(change, info)));
}
if (shouldShouldBackgroundColor && changeAnimation != null) {
@@ -457,7 +527,7 @@ class ActivityEmbeddingAnimationRunner {
// When the parent window is also included in the transition as an opening window,
// we would like to animate the parent window instead.
final TransitionInfo.Change parentChange = info.getChange(parentToken);
- if (parentChange != null && Transitions.isOpeningType(parentChange.getMode())) {
+ if (parentChange != null && TransitionUtil.isOpeningType(parentChange.getMode())) {
changingChanges.add(parentChange);
}
}
@@ -479,8 +549,8 @@ class ActivityEmbeddingAnimationRunner {
// No-op if it will be covered by the changing parent window.
continue;
}
- hasOpeningWindow |= Transitions.isOpeningType(change.getMode());
- hasClosingWindow |= Transitions.isClosingType(change.getMode());
+ hasOpeningWindow |= TransitionUtil.isOpeningType(change.getMode());
+ hasClosingWindow |= TransitionUtil.isClosingType(change.getMode());
}
return hasOpeningWindow && hasClosingWindow;
}
@@ -490,8 +560,19 @@ class ActivityEmbeddingAnimationRunner {
@NonNull SurfaceControl.Transaction startTransaction) {
for (TransitionInfo.Change change : info.getChanges()) {
final SurfaceControl leash = change.getLeash();
- startTransaction.setPosition(leash,
- change.getEndRelOffset().x, change.getEndRelOffset().y);
+ if (change.getParent() != null) {
+ startTransaction.setPosition(leash,
+ change.getEndRelOffset().x, change.getEndRelOffset().y);
+ } else {
+ // Change leash has been reparented to the root if its parent is not in the
+ // transition.
+ // Because it is reparented to the root, the actual offset should be its relative
+ // position to the root instead. See Transitions#setupAnimHierarchy.
+ final TransitionInfo.Root root = TransitionUtil.getRootFor(change, info);
+ startTransaction.setPosition(leash,
+ change.getEndAbsBounds().left - root.getOffset().x,
+ change.getEndAbsBounds().top - root.getOffset().y);
+ }
startTransaction.setWindowCrop(leash,
change.getEndAbsBounds().width(), change.getEndAbsBounds().height());
if (change.getMode() == TRANSIT_CLOSE) {
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 d10a6744b5f1..1793a3d0feb4 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
@@ -36,7 +36,7 @@ import android.window.TransitionInfo;
import androidx.annotation.NonNull;
import com.android.internal.policy.TransitionAnimation;
-import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
/** Animation spec for ActivityEmbedding transition. */
// TODO(b/206557124): provide an easier way to customize animation
@@ -73,10 +73,19 @@ class ActivityEmbeddingAnimationSpec {
@NonNull
static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
// Noop but just keep the window showing/hiding.
- final float alpha = Transitions.isClosingType(change.getMode()) ? 0f : 1f;
+ final float alpha = TransitionUtil.isClosingType(change.getMode()) ? 0f : 1f;
return new AlphaAnimation(alpha, alpha);
}
+ /**
+ * Animation that intended to show snapshot for closing animation because the closing end bounds
+ * are changed.
+ */
+ @NonNull
+ static Animation createShowSnapshotForClosingAnimation() {
+ return new AlphaAnimation(1f, 1f);
+ }
+
/** Animation for window that is opening in a change transition. */
@NonNull
Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change,
@@ -198,7 +207,7 @@ class ActivityEmbeddingAnimationSpec {
@NonNull
Animation loadOpenAnimation(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
- final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
final Animation animation;
if (shouldShowBackdrop(info, change)) {
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
@@ -222,7 +231,7 @@ class ActivityEmbeddingAnimationSpec {
@NonNull
Animation loadCloseAnimation(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
- final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
final Animation animation;
if (shouldShowBackdrop(info, change)) {
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
@@ -246,7 +255,7 @@ class ActivityEmbeddingAnimationSpec {
private boolean shouldShowBackdrop(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change change) {
final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE,
- mTransitionAnimation);
+ mTransitionAnimation, false);
return a != null && a.getShowBackdrop();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index 521a65cc4df6..57d374b2b8f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -16,12 +16,16 @@
package com.android.wm.shell.activityembedding;
+import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION;
import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
+import static com.android.wm.shell.transition.DefaultTransitionHandler.isSupportedOverrideAnimation;
+
import static java.util.Objects.requireNonNull;
import android.content.Context;
+import android.graphics.Rect;
import android.os.IBinder;
import android.util.ArrayMap;
import android.view.SurfaceControl;
@@ -35,6 +39,9 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
+
+import java.util.List;
/**
* Responsible for handling ActivityEmbedding related transitions.
@@ -86,12 +93,13 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
boolean containsEmbeddingSplit = false;
- for (TransitionInfo.Change change : info.getChanges()) {
+ boolean containsNonEmbeddedChange = false;
+ final List<TransitionInfo.Change> changes = info.getChanges();
+ for (int i = changes.size() - 1; i >= 0; i--) {
+ final TransitionInfo.Change change = changes.get(i);
if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) {
- // Only animate the transition if all changes are in a Task with ActivityEmbedding.
- return false;
- }
- if (!containsEmbeddingSplit && !change.hasFlags(FLAG_FILLS_TASK)) {
+ containsNonEmbeddedChange = true;
+ } else if (!change.hasFlags(FLAG_FILLS_TASK)) {
// Whether the Task contains any ActivityEmbedding split before or after the
// transition.
containsEmbeddingSplit = true;
@@ -103,6 +111,17 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
// such as the device is in a folded state.
return false;
}
+ if (containsNonEmbeddedChange && !handleNonEmbeddedChanges(changes)) {
+ return false;
+ }
+ final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+ if (options != null
+ // Scene-transition will be handled by app side.
+ && (options.getType() == ANIM_SCENE_TRANSITION
+ // Use default transition handler to animate override animation.
+ || isSupportedOverrideAnimation(options))) {
+ return false;
+ }
// Start ActivityEmbedding animation.
mTransitionCallbacks.put(transition, finishCallback);
@@ -110,6 +129,44 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
return true;
}
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ mAnimationRunner.cancelAnimationFromMerge();
+ }
+
+ private boolean handleNonEmbeddedChanges(List<TransitionInfo.Change> changes) {
+ final Rect nonClosingEmbeddedArea = new Rect();
+ for (int i = changes.size() - 1; i >= 0; i--) {
+ final TransitionInfo.Change change = changes.get(i);
+ if (!TransitionUtil.isClosingType(change.getMode())) {
+ if (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) {
+ nonClosingEmbeddedArea.union(change.getEndAbsBounds());
+ continue;
+ }
+ // Not able to handle non-embedded container if it is not closing.
+ return false;
+ }
+ }
+ for (int i = changes.size() - 1; i >= 0; i--) {
+ final TransitionInfo.Change change = changes.get(i);
+ if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)
+ && !nonClosingEmbeddedArea.contains(change.getEndAbsBounds())) {
+ // Unknown to animate containers outside the area of embedded activities.
+ return false;
+ }
+ }
+ // Drop the non-embedded closing change because it is occluded by embedded activities.
+ for (int i = changes.size() - 1; i >= 0; i--) {
+ final TransitionInfo.Change change = changes.get(i);
+ if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) {
+ changes.remove(i);
+ }
+ }
+ return true;
+ }
+
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
index a0dde6ad168d..2ec9e8b12fc6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.animation;
+import android.graphics.Path;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
@@ -53,6 +54,11 @@ public class Interpolators {
public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
/**
+ * The default emphasized interpolator. Used for hero / emphasized movement of content.
+ */
+ public static final Interpolator EMPHASIZED = createEmphasizedInterpolator();
+
+ /**
* The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
* is disappearing e.g. when moving off screen.
*/
@@ -81,4 +87,14 @@ public class Interpolators {
public static final PathInterpolator DIM_INTERPOLATOR =
new PathInterpolator(.23f, .87f, .52f, -0.11f);
+
+ // Create the default emphasized interpolator
+ private static PathInterpolator createEmphasizedInterpolator() {
+ Path path = new Path();
+ // Doing the same as fast_out_extra_slow_in
+ path.moveTo(0f, 0f);
+ path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f);
+ path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f);
+ return new PathInterpolator(path);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
index 8cbe44b15e42..c98090638010 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
@@ -33,12 +33,19 @@ public interface BackAnimation {
*
* @param touchX the X touch position of the {@link MotionEvent}.
* @param touchY the Y touch position of the {@link MotionEvent}.
+ * @param velocityX the X velocity computed from the {@link MotionEvent}.
+ * @param velocityY the Y velocity computed from the {@link MotionEvent}.
* @param keyAction the original {@link KeyEvent#getAction()} when the event was dispatched to
* the process. This is forwarded separately because the input pipeline may mutate
* the {#event} action state later.
* @param swipeEdge the edge from which the swipe begins.
*/
- void onBackMotion(float touchX, float touchY, int keyAction,
+ void onBackMotion(
+ float touchX,
+ float touchY,
+ float velocityX,
+ float velocityY,
+ int keyAction,
@BackEvent.SwipeEdge int swipeEdge);
/**
@@ -47,9 +54,46 @@ public interface BackAnimation {
void setTriggerBack(boolean triggerBack);
/**
- * Sets the threshold values that defining edge swipe behavior.
- * @param triggerThreshold the min threshold to trigger back.
- * @param progressThreshold the max threshold to keep progressing back animation.
+ * Sets the threshold values that define edge swipe behavior.<br>
+ * <br>
+ * <h1>How does {@code nonLinearFactor} work?</h1>
+ * <pre>
+ * screen screen screen
+ * width width width
+ * |——————| |————————————| |————————————————————|
+ * A B A B C A
+ * 1 +——————+—————+ 1 +————————————+ 1 +————————————+———————+
+ * | / | | —/| | | —————/|
+ * | / | | —/ | | ——/ |
+ * | / | | —/ | | ——/ | |
+ * | / | | —/ | | ——/ | |
+ * | / | | —/ | | ——/ | |
+ * |/ | |—/ | |—/ | |
+ * 0 +————————————+ 0 +————————————+ 0 +————————————+———————+
+ * B B B
+ * </pre>
+ * Three devices with different widths (smaller, equal, and wider) relative to the progress
+ * threshold are shown in the graphs.<br>
+ * - A is the width of the screen<br>
+ * - B is the progress threshold (horizontal swipe distance where progress is linear)<br>
+ * - C equals B + (A - B) * nonLinearFactor<br>
+ * <br>
+ * If A is less than or equal to B, {@code progress} for the swipe distance between:<br>
+ * - [0, A] will scale linearly between [0, 1].<br>
+ * If A is greater than B, {@code progress} for swipe distance between:<br>
+ * - [0, B] will scale linearly between [0, B / C]<br>
+ * - (B, A] will scale non-linearly and reach 1.
+ *
+ * @param linearDistance up to this distance progress continues linearly. B in the graph above.
+ * @param maxDistance distance at which the progress will be 1f. A in the graph above.
+ * @param nonLinearFactor This value is used to calculate the target if the screen is wider
+ * than the progress threshold.
+ */
+ void setSwipeThresholds(float linearDistance, float maxDistance, float nonLinearFactor);
+
+ /**
+ * Sets the system bar listener to control the system bar color.
+ * @param customizer the controller to control system bar color.
*/
- void setSwipeThresholds(float triggerThreshold, float progressThreshold);
+ void setStatusBarCustomizer(StatusBarCustomizer customizer);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
new file mode 100644
index 000000000000..9bf3b80d262e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.back;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+
+import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
+
+import android.annotation.NonNull;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.view.AppearanceRegion;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+
+/**
+ * Controls background surface for the back animations
+ */
+public class BackAnimationBackground {
+ private static final int BACKGROUND_LAYER = -1;
+
+ private static final int NO_APPEARANCE = 0;
+
+ private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+ private SurfaceControl mBackgroundSurface;
+
+ private StatusBarCustomizer mCustomizer;
+ private boolean mIsRequestingStatusBarAppearance;
+ private boolean mBackgroundIsDark;
+ private Rect mStartBounds;
+
+ public BackAnimationBackground(RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+ }
+
+ /**
+ * Ensures the back animation background color layer is present.
+ * @param startRect The start bounds of the closing target.
+ * @param color The background color.
+ * @param transaction The animation transaction.
+ */
+ void ensureBackground(Rect startRect, int color,
+ @NonNull SurfaceControl.Transaction transaction) {
+ if (mBackgroundSurface != null) {
+ return;
+ }
+
+ mBackgroundIsDark = ColorUtils.calculateLuminance(color) < 0.5f;
+
+ final float[] colorComponents = new float[] { Color.red(color) / 255.f,
+ Color.green(color) / 255.f, Color.blue(color) / 255.f };
+
+ final SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
+ .setName("back-animation-background")
+ .setCallsite("BackAnimationBackground")
+ .setColorLayer();
+
+ mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder);
+ mBackgroundSurface = colorLayerBuilder.build();
+ transaction.setColor(mBackgroundSurface, colorComponents)
+ .setLayer(mBackgroundSurface, BACKGROUND_LAYER)
+ .show(mBackgroundSurface);
+ mStartBounds = startRect;
+ mIsRequestingStatusBarAppearance = false;
+ }
+
+ void removeBackground(@NonNull SurfaceControl.Transaction transaction) {
+ if (mBackgroundSurface == null) {
+ return;
+ }
+
+ if (mBackgroundSurface.isValid()) {
+ transaction.remove(mBackgroundSurface);
+ }
+ mBackgroundSurface = null;
+ mIsRequestingStatusBarAppearance = false;
+ }
+
+ void setStatusBarCustomizer(StatusBarCustomizer customizer) {
+ mCustomizer = customizer;
+ }
+
+ void onBackProgressed(float progress) {
+ if (mCustomizer == null || mStartBounds.isEmpty()) {
+ return;
+ }
+
+ final boolean shouldCustomizeSystemBar = progress > UPDATE_SYSUI_FLAGS_THRESHOLD;
+ if (shouldCustomizeSystemBar == mIsRequestingStatusBarAppearance) {
+ return;
+ }
+
+ mIsRequestingStatusBarAppearance = shouldCustomizeSystemBar;
+ if (mIsRequestingStatusBarAppearance) {
+ final AppearanceRegion region = new AppearanceRegion(!mBackgroundIsDark
+ ? APPEARANCE_LIGHT_STATUS_BARS : NO_APPEARANCE,
+ mStartBounds);
+ mCustomizer.customizeStatusBarAppearance(region);
+ } else {
+ mCustomizer.customizeStatusBarAppearance(null);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
index f9b08000290f..e06d3ef4e1ab 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,12 @@
* limitations under the License.
*/
-@file:JvmName("CommonAssertions")
-package com.android.wm.shell.flicker.pip
+package com.android.wm.shell.back;
-internal const val PIP_WINDOW_COMPONENT = "PipMenuActivity"
+/**
+ * The common constant values used in back animators.
+ */
+class BackAnimationConstants {
+ static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.20f;
+ static final float PROGRESS_COMMIT_THRESHOLD = 0.1f;
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index aaeef196b618..bb543f24a8ea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -16,49 +16,52 @@
package com.android.wm.shell.back;
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
-import android.app.WindowConfiguration;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
-import android.hardware.HardwareBuffer;
import android.hardware.input.InputManager;
import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
-import android.os.IBinder;
+import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings.Global;
+import android.util.DisplayMetrics;
import android.util.Log;
-import android.view.IWindowFocusObserver;
+import android.util.MathUtils;
+import android.util.SparseArray;
+import android.view.IRemoteAnimationRunner;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.window.BackAnimationAdaptor;
+import android.window.BackAnimationAdapter;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
+import android.window.IBackAnimationFinishedCallback;
import android.window.IBackAnimationRunner;
-import android.window.IBackNaviAnimationController;
import android.window.IOnBackInvokedCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.view.AppearanceRegion;
+import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
@@ -73,133 +76,103 @@ import java.util.concurrent.atomic.AtomicBoolean;
* Controls the window animation run when a user initiates a back gesture.
*/
public class BackAnimationController implements RemoteCallable<BackAnimationController> {
- private static final String TAG = "BackAnimationController";
+ private static final String TAG = "ShellBackPreview";
private static final int SETTING_VALUE_OFF = 0;
private static final int SETTING_VALUE_ON = 1;
public static final boolean IS_ENABLED =
SystemProperties.getInt("persist.wm.debug.predictive_back",
SETTING_VALUE_ON) == SETTING_VALUE_ON;
- /** Flag for U animation features */
+ /** Flag for U animation features */
public static boolean IS_U_ANIMATION_ENABLED =
SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
SETTING_VALUE_ON) == SETTING_VALUE_ON;
+
+ public static final float FLING_MAX_LENGTH_SECONDS = 0.1f; // 100ms
+ public static final float FLING_SPEED_UP_FACTOR = 0.6f;
+
+ /**
+ * The maximum additional progress in case of fling gesture.
+ * The end animation starts after the user lifts the finger from the screen, we continue to
+ * fire {@link BackEvent}s until the velocity reaches 0.
+ */
+ private static final float MAX_FLING_PROGRESS = 0.3f; /* 30% of the screen */
+
/** Predictive back animation developer option */
private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
- // TODO (b/241808055) Find a appropriate time to remove during refactor
- private static final boolean USE_TRANSITION =
- SystemProperties.getInt("persist.wm.debug.predictive_back_ani_trans", 1) != 0;
/**
- * Max duration to wait for a transition to finish before accepting another gesture start
- * request.
+ * Max duration to wait for an animation to finish before triggering the real back.
*/
- private static final long MAX_TRANSITION_DURATION = 2000;
+ private static final long MAX_ANIMATION_DURATION = 2000;
/** True when a back gesture is ongoing */
private boolean mBackGestureStarted = false;
- /** Tracks if an uninterruptible transition is in progress */
- private boolean mTransitionInProgress = false;
+ /** Tracks if an uninterruptible animation is in progress */
+ private boolean mPostCommitAnimationInProgress = false;
/** Tracks if we should start the back gesture on the next motion move event */
private boolean mShouldStartOnNextMoveEvent = false;
/** @see #setTriggerBack(boolean) */
private boolean mTriggerBack;
+ private FlingAnimationUtils mFlingAnimationUtils;
@Nullable
private BackNavigationInfo mBackNavigationInfo;
- private final SurfaceControl.Transaction mTransaction;
private final IActivityTaskManager mActivityTaskManager;
private final Context mContext;
private final ContentResolver mContentResolver;
private final ShellController mShellController;
private final ShellExecutor mShellExecutor;
private final Handler mBgHandler;
- @Nullable
- private IOnBackInvokedCallback mBackToLauncherCallback;
- private float mTriggerThreshold;
- private final Runnable mResetTransitionRunnable = () -> {
- finishAnimation();
- mTransitionInProgress = false;
+ private final Runnable mAnimationTimeoutRunnable = () -> {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
+ MAX_ANIMATION_DURATION);
+ onBackAnimationFinished();
};
- private RemoteAnimationTarget mAnimationTarget;
- IBackAnimationRunner mIBackAnimationRunner;
- private IBackNaviAnimationController mBackAnimationController;
- private BackAnimationAdaptor mBackAnimationAdaptor;
+ private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
+ @VisibleForTesting
+ BackAnimationAdapter mBackAnimationAdapter;
private final TouchTracker mTouchTracker = new TouchTracker();
- private final CachingBackDispatcher mCachingBackDispatcher = new CachingBackDispatcher();
+
+ private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>();
+ @Nullable
+ private IOnBackInvokedCallback mActiveCallback;
+
+ private CrossActivityAnimation mDefaultActivityAnimation;
+ private CustomizeActivityAnimation mCustomizeActivityAnimation;
@VisibleForTesting
- final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() {
- @Override
- public void focusGained(IBinder inputToken) { }
- @Override
- public void focusLost(IBinder inputToken) {
- mShellExecutor.execute(() -> {
- if (!mBackGestureStarted || mTransitionInProgress) {
- // If an uninterruptible transition is already in progress, we should ignore
- // this due to the transition may cause focus lost. (alpha = 0)
- return;
+ final RemoteCallback mNavigationObserver = new RemoteCallback(
+ new RemoteCallback.OnResultListener() {
+ @Override
+ public void onResult(@Nullable Bundle result) {
+ mShellExecutor.execute(() -> {
+ if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
+ // If an uninterruptible animation is already in progress, we should
+ // ignore this due to it may cause focus lost. (alpha = 0)
+ return;
+ }
+ ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone.");
+ setTriggerBack(false);
+ onGestureFinished(false);
+ });
}
- setTriggerBack(false);
- onGestureFinished(false);
});
- }
- };
-
- /**
- * Cache the temporary callback and trigger result if gesture was finish before received
- * BackAnimationRunner#onAnimationStart/cancel, so there can continue play the animation.
- */
- private class CachingBackDispatcher {
- private IOnBackInvokedCallback mOnBackCallback;
- private boolean mTriggerBack;
- // Whether we are waiting to receive onAnimationStart
- private boolean mWaitingAnimation;
- void startWaitingAnimation() {
- mWaitingAnimation = true;
- }
-
- boolean set(IOnBackInvokedCallback callback, boolean triggerBack) {
- if (mWaitingAnimation) {
- mOnBackCallback = callback;
- mTriggerBack = triggerBack;
- return true;
- }
- return false;
- }
-
- boolean consume() {
- boolean consumed = false;
- if (mWaitingAnimation && mOnBackCallback != null) {
- if (mTriggerBack) {
- final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(1);
- dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
- dispatchOnBackInvoked(mOnBackCallback);
- } else {
- final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(0);
- dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
- dispatchOnBackCancelled(mOnBackCallback);
- }
- startTransition();
- consumed = true;
- }
- mOnBackCallback = null;
- mWaitingAnimation = false;
- return consumed;
- }
- }
+ private final BackAnimationBackground mAnimationBackground;
+ private StatusBarCustomizer mCustomizer;
public BackAnimationController(
@NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@NonNull @ShellMainThread ShellExecutor shellExecutor,
@NonNull @ShellBackgroundThread Handler backgroundHandler,
- Context context) {
+ Context context,
+ @NonNull BackAnimationBackground backAnimationBackground) {
this(shellInit, shellController, shellExecutor, backgroundHandler,
- new SurfaceControl.Transaction(), ActivityTaskManager.getService(),
- context, context.getContentResolver());
+ ActivityTaskManager.getService(), context, context.getContentResolver(),
+ backAnimationBackground);
}
@VisibleForTesting
@@ -208,17 +181,22 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
@NonNull ShellController shellController,
@NonNull @ShellMainThread ShellExecutor shellExecutor,
@NonNull @ShellBackgroundThread Handler bgHandler,
- @NonNull SurfaceControl.Transaction transaction,
@NonNull IActivityTaskManager activityTaskManager,
- Context context, ContentResolver contentResolver) {
+ Context context, ContentResolver contentResolver,
+ @NonNull BackAnimationBackground backAnimationBackground) {
mShellController = shellController;
mShellExecutor = shellExecutor;
- mTransaction = transaction;
mActivityTaskManager = activityTaskManager;
mContext = context;
mContentResolver = contentResolver;
mBgHandler = bgHandler;
shellInit.addInitCallback(this::onInit, this);
+ mAnimationBackground = backAnimationBackground;
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mFlingAnimationUtils = new FlingAnimationUtils.Builder(displayMetrics)
+ .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
+ .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
+ .build();
}
@VisibleForTesting
@@ -228,8 +206,29 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private void onInit() {
setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
+ createAdapter();
mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
this::createExternalInterface, this);
+
+ initBackAnimationRunners();
+ }
+
+ private void initBackAnimationRunners() {
+ if (!IS_U_ANIMATION_ENABLED) {
+ return;
+ }
+
+ final CrossTaskBackAnimation crossTaskAnimation =
+ new CrossTaskBackAnimation(mContext, mAnimationBackground);
+ mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
+ crossTaskAnimation.mBackAnimationRunner);
+ mDefaultActivityAnimation =
+ new CrossActivityAnimation(mContext, mAnimationBackground);
+ mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ mDefaultActivityAnimation.mBackAnimationRunner);
+ mCustomizeActivityAnimation =
+ new CustomizeActivityAnimation(mContext, mAnimationBackground);
+ // TODO (236760237): register dialog close animation when it's completed.
}
private void setupAnimationDeveloperSettingsObserver(
@@ -254,8 +253,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
boolean isEnabled = settingValue == SETTING_VALUE_ON;
mEnableAnimations.set(isEnabled);
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
- isEnabled);
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
}
public BackAnimation getBackAnimationImpl() {
@@ -281,8 +279,20 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private class BackAnimationImpl implements BackAnimation {
@Override
public void onBackMotion(
- float touchX, float touchY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) {
- mShellExecutor.execute(() -> onMotionEvent(touchX, touchY, keyAction, swipeEdge));
+ float touchX,
+ float touchY,
+ float velocityX,
+ float velocityY,
+ int keyAction,
+ @BackEvent.SwipeEdge int swipeEdge
+ ) {
+ mShellExecutor.execute(() -> onMotionEvent(
+ /* touchX = */ touchX,
+ /* touchY = */ touchY,
+ /* velocityX = */ velocityX,
+ /* velocityY = */ velocityY,
+ /* keyAction = */ keyAction,
+ /* swipeEdge = */ swipeEdge));
}
@Override
@@ -291,9 +301,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
@Override
- public void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
+ public void setSwipeThresholds(
+ float linearDistance,
+ float maxDistance,
+ float nonLinearFactor) {
mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds(
- triggerThreshold, progressThreshold));
+ linearDistance, maxDistance, nonLinearFactor));
+ }
+
+ @Override
+ public void setStatusBarCustomizer(StatusBarCustomizer customizer) {
+ mCustomizer = customizer;
+ mAnimationBackground.setStatusBarCustomizer(customizer);
}
}
@@ -306,21 +325,24 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
@Override
- public void setBackToLauncherCallback(IOnBackInvokedCallback callback) {
+ public void setBackToLauncherCallback(IOnBackInvokedCallback callback,
+ IRemoteAnimationRunner runner) {
executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
- (controller) -> controller.setBackToLauncherCallback(callback));
+ (controller) -> controller.registerAnimation(
+ BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ new BackAnimationRunner(callback, runner)));
}
@Override
public void clearBackToLauncherCallback() {
executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback",
- (controller) -> controller.clearBackToLauncherCallback());
+ (controller) -> controller.unregisterAnimation(
+ BackNavigationInfo.TYPE_RETURN_TO_HOME));
}
- @Override
- public void onBackToLauncherAnimationFinished() {
- executeRemoteCallWithTaskPermission(mController, "onBackToLauncherAnimationFinished",
- (controller) -> controller.onBackToLauncherAnimationFinished());
+ public void customizeStatusBarAppearance(AppearanceRegion appearance) {
+ executeRemoteCallWithTaskPermission(mController, "useLauncherSysBarFlags",
+ (controller) -> controller.customizeStatusBarAppearance(appearance));
}
@Override
@@ -329,45 +351,37 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
}
- @VisibleForTesting
- void setBackToLauncherCallback(IOnBackInvokedCallback callback) {
- mBackToLauncherCallback = callback;
- if (USE_TRANSITION) {
- createAdaptor();
+ private void customizeStatusBarAppearance(AppearanceRegion appearance) {
+ if (mCustomizer != null) {
+ mCustomizer.customizeStatusBarAppearance(appearance);
}
}
- private void clearBackToLauncherCallback() {
- mBackToLauncherCallback = null;
+ void registerAnimation(@BackNavigationInfo.BackTargetType int type,
+ @NonNull BackAnimationRunner runner) {
+ mAnimationDefinition.set(type, runner);
}
- @VisibleForTesting
- void onBackToLauncherAnimationFinished() {
- final boolean triggerBack = mTriggerBack;
- IOnBackInvokedCallback callback = mBackNavigationInfo != null
- ? mBackNavigationInfo.getOnBackInvokedCallback() : null;
- // Make sure the notification sequence should be controller > client.
- finishAnimation();
- if (callback != null) {
- if (triggerBack) {
- dispatchOnBackInvoked(callback);
- } else {
- dispatchOnBackCancelled(callback);
- }
- }
+ void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) {
+ mAnimationDefinition.remove(type);
}
/**
* Called when a new motion event needs to be transferred to this
* {@link BackAnimationController}
*/
- public void onMotionEvent(float touchX, float touchY, int keyAction,
+ public void onMotionEvent(
+ float touchX,
+ float touchY,
+ float velocityX,
+ float velocityY,
+ int keyAction,
@BackEvent.SwipeEdge int swipeEdge) {
- if (mTransitionInProgress) {
+ if (mPostCommitAnimationInProgress) {
return;
}
- mTouchTracker.update(touchX, touchY);
+ mTouchTracker.update(touchX, touchY, velocityX, velocityY);
if (keyAction == MotionEvent.ACTION_DOWN) {
if (!mBackGestureStarted) {
mShouldStartOnNextMoveEvent = true;
@@ -380,7 +394,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
onGestureStarted(touchX, touchY, swipeEdge);
mShouldStartOnNextMoveEvent = false;
}
- onMove(touchX, touchY, swipeEdge);
+ onMove();
} else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"Finishing gesture with event action: %d", keyAction);
@@ -395,20 +409,19 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
if (mBackGestureStarted || mBackNavigationInfo != null) {
Log.e(TAG, "Animation is being initialized but is already started.");
- finishAnimation();
+ finishBackNavigation();
}
mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
mBackGestureStarted = true;
try {
- boolean requestAnimation = mEnableAnimations.get();
- mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation,
- mFocusObserver, mBackAnimationAdaptor);
+ mBackNavigationInfo = mActivityTaskManager.startBackNavigation(
+ mNavigationObserver, mEnableAnimations.get() ? mBackAnimationAdapter : null);
onBackNavigationInfoReceived(mBackNavigationInfo);
} catch (RemoteException remoteException) {
Log.e(TAG, "Failed to initAnimation", remoteException);
- finishAnimation();
+ finishBackNavigation();
}
}
@@ -418,82 +431,27 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
Log.e(TAG, "Received BackNavigationInfo is null.");
return;
}
- int backType = backNavigationInfo.getType();
- IOnBackInvokedCallback targetCallback = null;
- final boolean dispatchToLauncher = shouldDispatchToLauncher(backType);
- if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) {
- HardwareBuffer hardwareBuffer = backNavigationInfo.getScreenshotHardwareBuffer();
- if (hardwareBuffer != null) {
- displayTargetScreenshot(hardwareBuffer,
- backNavigationInfo.getTaskWindowConfiguration());
- }
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
- mTransaction.apply();
- } else if (dispatchToLauncher) {
- targetCallback = mBackToLauncherCallback;
- if (USE_TRANSITION) {
- mCachingBackDispatcher.startWaitingAnimation();
+ final int backType = backNavigationInfo.getType();
+ final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
+ if (shouldDispatchToAnimator) {
+ if (mAnimationDefinition.contains(backType)) {
+ mAnimationDefinition.get(backType).startGesture();
+ } else {
+ mActiveCallback = null;
}
- } else if (backType == BackNavigationInfo.TYPE_CALLBACK) {
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
- }
- if (!USE_TRANSITION || !dispatchToLauncher) {
- dispatchOnBackStarted(
- targetCallback,
- mTouchTracker.createStartEvent(
- mBackNavigationInfo.getDepartingAnimationTarget()));
+ } else {
+ mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+ dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null));
}
}
- /**
- * Display the screenshot of the activity beneath.
- *
- * @param hardwareBuffer The buffer containing the screenshot.
- */
- private void displayTargetScreenshot(@NonNull HardwareBuffer hardwareBuffer,
- WindowConfiguration taskWindowConfiguration) {
- SurfaceControl screenshotSurface =
- mBackNavigationInfo == null ? null : mBackNavigationInfo.getScreenshotSurface();
- if (screenshotSurface == null) {
- Log.e(TAG, "BackNavigationInfo doesn't contain a surface for the screenshot. ");
+ private void onMove() {
+ if (!mBackGestureStarted || mBackNavigationInfo == null || mActiveCallback == null) {
return;
}
- // Scale the buffer to fill the whole Task
- float sx = 1;
- float sy = 1;
- float w = taskWindowConfiguration.getBounds().width();
- float h = taskWindowConfiguration.getBounds().height();
-
- if (w != hardwareBuffer.getWidth()) {
- sx = w / hardwareBuffer.getWidth();
- }
-
- if (h != hardwareBuffer.getHeight()) {
- sy = h / hardwareBuffer.getHeight();
- }
- mTransaction.setScale(screenshotSurface, sx, sy);
- mTransaction.setBuffer(screenshotSurface, hardwareBuffer);
- mTransaction.setVisibility(screenshotSurface, true);
- }
-
- private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
- if (!mBackGestureStarted || mBackNavigationInfo == null) {
- return;
- }
final BackMotionEvent backEvent = mTouchTracker.createProgressEvent();
- if (USE_TRANSITION && mBackAnimationController != null && mAnimationTarget != null) {
- dispatchOnBackProgressed(mBackToLauncherCallback, backEvent);
- } else if (mEnableAnimations.get()) {
- int backType = mBackNavigationInfo.getType();
- IOnBackInvokedCallback targetCallback;
- if (shouldDispatchToLauncher(backType)) {
- targetCallback = mBackToLauncherCallback;
- } else {
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
- }
- dispatchOnBackProgressed(targetCallback, backEvent);
- }
+ dispatchOnBackProgressed(mActiveCallback, backEvent);
}
private void injectBackKey() {
@@ -509,68 +467,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
InputDevice.SOURCE_KEYBOARD);
ev.setDisplayId(mContext.getDisplay().getDisplayId());
- if (!InputManager.getInstance()
+ if (!mContext.getSystemService(InputManager.class)
.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
Log.e(TAG, "Inject input event fail");
}
}
- private void onGestureFinished(boolean fromTouch) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
- if (!mBackGestureStarted) {
- finishAnimation();
- return;
- }
-
- if (fromTouch) {
- // Let touch reset the flag otherwise it will start a new back navigation and refresh
- // the info when received a new move event.
- mBackGestureStarted = false;
- }
-
- if (mTransitionInProgress) {
- return;
- }
-
- if (mBackNavigationInfo == null) {
- // No focus window found or core are running recents animation, inject back key as
- // legacy behavior.
- if (mTriggerBack) {
- injectBackKey();
- }
- finishAnimation();
- return;
- }
-
- int backType = mBackNavigationInfo.getType();
- boolean shouldDispatchToLauncher = shouldDispatchToLauncher(backType);
- IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher
- ? mBackToLauncherCallback
- : mBackNavigationInfo.getOnBackInvokedCallback();
- if (mCachingBackDispatcher.set(targetCallback, mTriggerBack)) {
- return;
- }
- if (shouldDispatchToLauncher) {
- startTransition();
- }
- if (mTriggerBack) {
- dispatchOnBackInvoked(targetCallback);
- } else {
- dispatchOnBackCancelled(targetCallback);
- }
- if (backType != BackNavigationInfo.TYPE_RETURN_TO_HOME || !shouldDispatchToLauncher) {
- // Launcher callback missing. Simply finish animation.
- finishAnimation();
- }
- }
-
- private boolean shouldDispatchToLauncher(int backType) {
- return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
- && mBackToLauncherCallback != null
- && mEnableAnimations.get()
+ private boolean shouldDispatchToAnimator() {
+ return mEnableAnimations.get()
&& mBackNavigationInfo != null
- && ((USE_TRANSITION && mBackNavigationInfo.isPrepareRemoteAnimation())
- || mBackNavigationInfo.getDepartingAnimationTarget() != null);
+ && mBackNavigationInfo.isPrepareRemoteAnimation();
}
private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
@@ -579,14 +485,84 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
- callback.onBackStarted(backEvent);
- }
+ callback.onBackStarted(backEvent);
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackStarted error: ", e);
}
}
+
+ /**
+ * Allows us to manage the fling gesture, it smoothly animates the current progress value to
+ * the final position, calculated based on the current velocity.
+ *
+ * @param callback the callback to be invoked when the animation ends.
+ */
+ private void dispatchOrAnimateOnBackInvoked(IOnBackInvokedCallback callback) {
+ if (callback == null) {
+ return;
+ }
+
+ boolean animationStarted = false;
+
+ if (mBackNavigationInfo != null && mBackNavigationInfo.isAnimationCallback()) {
+
+ final BackMotionEvent backMotionEvent = mTouchTracker.createProgressEvent();
+ if (backMotionEvent != null) {
+ // Constraints - absolute values
+ float minVelocity = mFlingAnimationUtils.getMinVelocityPxPerSecond();
+ float maxVelocity = mFlingAnimationUtils.getHighVelocityPxPerSecond();
+ float maxX = mTouchTracker.getMaxDistance(); // px
+ float maxFlingDistance = maxX * MAX_FLING_PROGRESS; // px
+
+ // Current state
+ float currentX = backMotionEvent.getTouchX();
+ float velocity = MathUtils.constrain(backMotionEvent.getVelocityX(),
+ -maxVelocity, maxVelocity);
+
+ // Target state
+ float animationFaction = velocity / maxVelocity; // value between -1 and 1
+ float flingDistance = animationFaction * maxFlingDistance; // px
+ float endX = MathUtils.constrain(currentX + flingDistance, 0f, maxX);
+
+ if (!Float.isNaN(endX)
+ && currentX != endX
+ && Math.abs(velocity) >= minVelocity) {
+ ValueAnimator animator = ValueAnimator.ofFloat(currentX, endX);
+
+ mFlingAnimationUtils.apply(
+ /* animator = */ animator,
+ /* currValue = */ currentX,
+ /* endValue = */ endX,
+ /* velocity = */ velocity,
+ /* maxDistance = */ maxFlingDistance
+ );
+
+ animator.addUpdateListener(animation -> {
+ Float animatedValue = (Float) animation.getAnimatedValue();
+ float progress = mTouchTracker.getProgress(animatedValue);
+ final BackMotionEvent backEvent = mTouchTracker
+ .createProgressEvent(progress);
+ dispatchOnBackProgressed(mActiveCallback, backEvent);
+ });
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ dispatchOnBackInvoked(callback);
+ }
+ });
+ animator.start();
+ animationStarted = true;
+ }
+ }
+ }
+
+ if (!animationStarted) {
+ dispatchOnBackInvoked(callback);
+ }
+ }
+
private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
if (callback == null) {
return;
@@ -603,9 +579,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
- callback.onBackCancelled();
- }
+ callback.onBackCancelled();
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackCancelled error: ", e);
}
@@ -617,128 +591,245 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
- callback.onBackProgressed(backEvent);
- }
+ callback.onBackProgressed(backEvent);
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackProgressed error: ", e);
}
}
- private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
- return (IS_U_ANIMATION_ENABLED || callback == mBackToLauncherCallback)
- && mEnableAnimations.get();
- }
-
/**
* Sets to true when the back gesture has passed the triggering threshold, false otherwise.
*/
public void setTriggerBack(boolean triggerBack) {
- if (mTransitionInProgress) {
+ if (mPostCommitAnimationInProgress) {
return;
}
mTriggerBack = triggerBack;
mTouchTracker.setTriggerBack(triggerBack);
}
- private void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
- mTouchTracker.setProgressThreshold(progressThreshold);
- mTriggerThreshold = triggerThreshold;
+ private void setSwipeThresholds(
+ float linearDistance,
+ float maxDistance,
+ float nonLinearFactor) {
+ mTouchTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor);
}
- private void finishAnimation() {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishAnimation()");
- mTouchTracker.reset();
- BackNavigationInfo backNavigationInfo = mBackNavigationInfo;
- boolean triggerBack = mTriggerBack;
- mBackNavigationInfo = null;
- mAnimationTarget = null;
- mTriggerBack = false;
- mShouldStartOnNextMoveEvent = false;
- if (backNavigationInfo == null) {
- return;
+ private void invokeOrCancelBack() {
+ // Make a synchronized call to core before dispatch back event to client side.
+ // If the close transition happens before the core receives onAnimationFinished, there will
+ // play a second close animation for that transition.
+ if (mBackAnimationFinishedCallback != null) {
+ try {
+ mBackAnimationFinishedCallback.onAnimationFinished(mTriggerBack);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e);
+ }
+ mBackAnimationFinishedCallback = null;
}
- if (!USE_TRANSITION) {
- RemoteAnimationTarget animationTarget = backNavigationInfo
- .getDepartingAnimationTarget();
- if (animationTarget != null) {
- if (animationTarget.leash != null && animationTarget.leash.isValid()) {
- mTransaction.remove(animationTarget.leash);
- }
- }
- SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface();
- if (screenshotSurface != null && screenshotSurface.isValid()) {
- mTransaction.remove(screenshotSurface);
- }
- mTransaction.apply();
- }
- stopTransition();
- backNavigationInfo.onBackNavigationFinished(triggerBack);
- if (USE_TRANSITION) {
- final IBackNaviAnimationController controller = mBackAnimationController;
- if (controller != null) {
- try {
- controller.finish(triggerBack);
- } catch (RemoteException r) {
- // Oh no!
- }
+ if (mBackNavigationInfo != null) {
+ final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
+ if (mTriggerBack) {
+ dispatchOrAnimateOnBackInvoked(callback);
+ } else {
+ dispatchOnBackCancelled(callback);
}
- mBackAnimationController = null;
}
+ finishBackNavigation();
}
- private void startTransition() {
- if (mTransitionInProgress) {
+ /**
+ * Called when the gesture is released, then it could start the post commit animation.
+ */
+ private void onGestureFinished(boolean fromTouch) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
+ if (!mBackGestureStarted) {
+ finishBackNavigation();
+ return;
+ }
+
+ if (fromTouch) {
+ // Let touch reset the flag otherwise it will start a new back navigation and refresh
+ // the info when received a new move event.
+ mBackGestureStarted = false;
+ }
+
+ if (mPostCommitAnimationInProgress) {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
+ return;
+ }
+
+ if (mBackNavigationInfo == null) {
+ // No focus window found or core are running recents animation, inject back key as
+ // legacy behavior.
+ if (mTriggerBack) {
+ injectBackKey();
+ }
+ finishBackNavigation();
return;
}
- mTransitionInProgress = true;
- mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION);
+
+ final int backType = mBackNavigationInfo.getType();
+ final BackAnimationRunner runner = mAnimationDefinition.get(backType);
+ // Simply trigger and finish back navigation when no animator defined.
+ if (!shouldDispatchToAnimator() || runner == null) {
+ invokeOrCancelBack();
+ return;
+ }
+ if (runner.isWaitingAnimation()) {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
+ // Supposed it is in post commit animation state, and start the timeout to watch
+ // if the animation is ready.
+ mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
+ return;
+ } else if (runner.isAnimationCancelled()) {
+ invokeOrCancelBack();
+ return;
+ }
+ startPostCommitAnimation();
}
- private void stopTransition() {
- if (!mTransitionInProgress) {
+ /**
+ * Start the phase 2 animation when gesture is released.
+ * Callback to {@link #onBackAnimationFinished} when it is finished or timeout.
+ */
+ private void startPostCommitAnimation() {
+ if (mPostCommitAnimationInProgress) {
return;
}
- mShellExecutor.removeCallbacks(mResetTransitionRunnable);
- mTransitionInProgress = false;
+
+ mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()");
+ mPostCommitAnimationInProgress = true;
+ mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
+
+ // The next callback should be {@link #onBackAnimationFinished}.
+ if (mTriggerBack) {
+ dispatchOrAnimateOnBackInvoked(mActiveCallback);
+ } else {
+ dispatchOnBackCancelled(mActiveCallback);
+ }
}
- private void createAdaptor() {
- mIBackAnimationRunner = new IBackAnimationRunner.Stub() {
- @Override
- public void onAnimationCancelled() {
- // no op for now
+ /**
+ * Called when the post commit animation is completed or timeout.
+ * This will trigger the real {@link IOnBackInvokedCallback} behavior.
+ */
+ @VisibleForTesting
+ void onBackAnimationFinished() {
+ // Stop timeout runner.
+ mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
+ mPostCommitAnimationInProgress = false;
+
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
+
+ // Trigger the real back.
+ invokeOrCancelBack();
+ }
+
+ /**
+ * This should be called after the whole back navigation is completed.
+ */
+ @VisibleForTesting
+ void finishBackNavigation() {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
+ mShouldStartOnNextMoveEvent = false;
+ mTouchTracker.reset();
+ mActiveCallback = null;
+ // reset to default
+ if (mDefaultActivityAnimation != null
+ && mAnimationDefinition.contains(BackNavigationInfo.TYPE_CROSS_ACTIVITY)) {
+ mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ mDefaultActivityAnimation.mBackAnimationRunner);
+ }
+ if (mBackNavigationInfo != null) {
+ mBackNavigationInfo.onBackNavigationFinished(mTriggerBack);
+ mBackNavigationInfo = null;
+ }
+ mTriggerBack = false;
+ }
+
+ private BackAnimationRunner getAnimationRunnerAndInit() {
+ int type = mBackNavigationInfo.getType();
+ // Initiate customized cross-activity animation, or fall back to cross activity animation
+ if (type == BackNavigationInfo.TYPE_CROSS_ACTIVITY && mAnimationDefinition.contains(type)) {
+ final BackNavigationInfo.CustomAnimationInfo animationInfo =
+ mBackNavigationInfo.getCustomAnimationInfo();
+ if (animationInfo != null && mCustomizeActivityAnimation != null
+ && mCustomizeActivityAnimation.prepareNextAnimation(animationInfo)) {
+ mAnimationDefinition.get(type).resetWaitingAnimation();
+ mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ mCustomizeActivityAnimation.mBackAnimationRunner);
}
- @Override // Binder interface
- public void onAnimationStart(IBackNaviAnimationController controller, int type,
- RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps) {
+ }
+ return mAnimationDefinition.get(type);
+ }
+
+ private void createAdapter() {
+ IBackAnimationRunner runner = new IBackAnimationRunner.Stub() {
+ @Override
+ public void onAnimationStart(RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+ IBackAnimationFinishedCallback finishedCallback) {
mShellExecutor.execute(() -> {
- mBackAnimationController = controller;
- for (int i = 0; i < apps.length; i++) {
- final RemoteAnimationTarget target = apps[i];
- if (MODE_CLOSING == target.mode) {
- mAnimationTarget = target;
- } else if (MODE_OPENING == target.mode) {
- // TODO Home activity should handle the visibility for itself
- // once it finish relayout for orientation change
- SurfaceControl.Transaction tx =
- new SurfaceControl.Transaction();
- tx.setAlpha(target.leash, 1);
- tx.apply();
+ if (mBackNavigationInfo == null) {
+ Log.e(TAG, "Lack of navigation info to start animation.");
+ return;
+ }
+ final int type = mBackNavigationInfo.getType();
+ final BackAnimationRunner runner = getAnimationRunnerAndInit();
+ if (runner == null) {
+ Log.e(TAG, "Animation didn't be defined for type "
+ + BackNavigationInfo.typeToString(type));
+ if (finishedCallback != null) {
+ try {
+ finishedCallback.onAnimationFinished(false);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call IBackNaviAnimationController", e);
+ }
}
+ return;
+ }
+ mActiveCallback = runner.getCallback();
+ mBackAnimationFinishedCallback = finishedCallback;
+
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
+ runner.startAnimation(apps, wallpapers, nonApps, () -> mShellExecutor.execute(
+ BackAnimationController.this::onBackAnimationFinished));
+
+ if (apps.length >= 1) {
+ dispatchOnBackStarted(
+ mActiveCallback, mTouchTracker.createStartEvent(apps[0]));
+ }
+
+ // Dispatch the first progress after animation start for smoothing the initial
+ // animation, instead of waiting for next onMove.
+ final BackMotionEvent backFinish = mTouchTracker.createProgressEvent();
+ dispatchOnBackProgressed(mActiveCallback, backFinish);
+ if (!mBackGestureStarted) {
+ // if the down -> up gesture happened before animation start, we have to
+ // trigger the uninterruptible transition to finish the back animation.
+ startPostCommitAnimation();
+ }
+ });
+ }
+
+ @Override
+ public void onAnimationCancelled() {
+ mShellExecutor.execute(() -> {
+ final BackAnimationRunner runner = mAnimationDefinition.get(
+ mBackNavigationInfo.getType());
+ if (runner == null) {
+ return;
}
- dispatchOnBackStarted(mBackToLauncherCallback,
- mTouchTracker.createStartEvent(mAnimationTarget));
- final BackMotionEvent backInit = mTouchTracker.createProgressEvent();
- if (!mCachingBackDispatcher.consume()) {
- dispatchOnBackProgressed(mBackToLauncherCallback, backInit);
+ runner.cancelAnimation();
+ if (!mBackGestureStarted) {
+ invokeOrCancelBack();
}
});
}
};
- mBackAnimationAdaptor = new BackAnimationAdaptor(mIBackAnimationRunner,
- BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ mBackAnimationAdapter = new BackAnimationAdapter(runner);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
new file mode 100644
index 000000000000..913239f74bf2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.back;
+
+import static android.view.WindowManager.TRANSIT_OLD_UNSET;
+
+import android.annotation.NonNull;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.window.IBackAnimationRunner;
+import android.window.IOnBackInvokedCallback;
+
+/**
+ * Used to register the animation callback and runner, it will trigger result if gesture was finish
+ * before it received IBackAnimationRunner#onAnimationStart, so the controller could continue
+ * trigger the real back behavior.
+ */
+class BackAnimationRunner {
+ private static final String TAG = "ShellBackPreview";
+
+ private final IOnBackInvokedCallback mCallback;
+ private final IRemoteAnimationRunner mRunner;
+
+ // Whether we are waiting to receive onAnimationStart
+ private boolean mWaitingAnimation;
+
+ /** True when the back animation is cancelled */
+ private boolean mAnimationCancelled;
+
+ BackAnimationRunner(@NonNull IOnBackInvokedCallback callback,
+ @NonNull IRemoteAnimationRunner runner) {
+ mCallback = callback;
+ mRunner = runner;
+ }
+
+ /** Returns the registered animation runner */
+ IRemoteAnimationRunner getRunner() {
+ return mRunner;
+ }
+
+ /** Returns the registered animation callback */
+ IOnBackInvokedCallback getCallback() {
+ return mCallback;
+ }
+
+ /**
+ * Called from {@link IBackAnimationRunner}, it will deliver these
+ * {@link RemoteAnimationTarget}s to the corresponding runner.
+ */
+ void startAnimation(RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps, Runnable finishedCallback) {
+ final IRemoteAnimationFinishedCallback callback =
+ new IRemoteAnimationFinishedCallback.Stub() {
+ @Override
+ public void onAnimationFinished() {
+ finishedCallback.run();
+ }
+ };
+ mWaitingAnimation = false;
+ try {
+ getRunner().onAnimationStart(TRANSIT_OLD_UNSET, apps, wallpapers,
+ nonApps, callback);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call onAnimationStart", e);
+ }
+ }
+
+ void startGesture() {
+ mWaitingAnimation = true;
+ mAnimationCancelled = false;
+ }
+
+ boolean isWaitingAnimation() {
+ return mWaitingAnimation;
+ }
+
+ void cancelAnimation() {
+ mWaitingAnimation = false;
+ mAnimationCancelled = true;
+ }
+
+ boolean isAnimationCancelled() {
+ return mAnimationCancelled;
+ }
+
+ void resetWaitingAnimation() {
+ mWaitingAnimation = false;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
new file mode 100644
index 000000000000..edefe9e3ab06
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
@@ -0,0 +1,411 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.back;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
+import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.RemoteException;
+import android.util.FloatProperty;
+import android.util.TypedValue;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.window.BackEvent;
+import android.window.BackMotionEvent;
+import android.window.BackProgressAnimator;
+import android.window.IOnBackInvokedCallback;
+
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+/** Class that defines cross-activity animation. */
+@ShellMainThread
+class CrossActivityAnimation {
+ /**
+ * Minimum scale of the entering/closing window.
+ */
+ private static final float MIN_WINDOW_SCALE = 0.9f;
+
+ /** Duration of post animation after gesture committed. */
+ private static final int POST_ANIMATION_DURATION = 350;
+ private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
+ private static final FloatProperty<CrossActivityAnimation> ENTER_PROGRESS_PROP =
+ new FloatProperty<>("enter-alpha") {
+ @Override
+ public void setValue(CrossActivityAnimation anim, float value) {
+ anim.setEnteringProgress(value);
+ }
+
+ @Override
+ public Float get(CrossActivityAnimation object) {
+ return object.getEnteringProgress();
+ }
+ };
+ private static final FloatProperty<CrossActivityAnimation> LEAVE_PROGRESS_PROP =
+ new FloatProperty<>("leave-alpha") {
+ @Override
+ public void setValue(CrossActivityAnimation anim, float value) {
+ anim.setLeavingProgress(value);
+ }
+
+ @Override
+ public Float get(CrossActivityAnimation object) {
+ return object.getLeavingProgress();
+ }
+ };
+ private static final float MIN_WINDOW_ALPHA = 0.01f;
+ private static final float WINDOW_X_SHIFT_DP = 96;
+ private static final int SCALE_FACTOR = 100;
+ // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
+ private static final float TARGET_COMMIT_PROGRESS = 0.5f;
+ private static final float ENTER_ALPHA_THRESHOLD = 0.22f;
+
+ private final Rect mStartTaskRect = new Rect();
+ private final float mCornerRadius;
+
+ // The closing window properties.
+ private final RectF mClosingRect = new RectF();
+
+ // The entering window properties.
+ private final Rect mEnteringStartRect = new Rect();
+ private final RectF mEnteringRect = new RectF();
+ private final SpringAnimation mEnteringProgressSpring;
+ private final SpringAnimation mLeavingProgressSpring;
+ // Max window x-shift in pixels.
+ private final float mWindowXShift;
+
+ private float mEnteringProgress = 0f;
+ private float mLeavingProgress = 0f;
+
+ private final PointF mInitialTouchPos = new PointF();
+
+ private final Matrix mTransformMatrix = new Matrix();
+
+ private final float[] mTmpFloat9 = new float[9];
+
+ private RemoteAnimationTarget mEnteringTarget;
+ private RemoteAnimationTarget mClosingTarget;
+ private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+
+ private boolean mBackInProgress = false;
+
+ private PointF mTouchPos = new PointF();
+ private IRemoteAnimationFinishedCallback mFinishCallback;
+
+ private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+ final BackAnimationRunner mBackAnimationRunner;
+
+ private final BackAnimationBackground mBackground;
+
+ CrossActivityAnimation(Context context, BackAnimationBackground background) {
+ mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+ mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+ mBackground = background;
+ mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
+ mEnteringProgressSpring.setSpring(new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+ mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP);
+ mLeavingProgressSpring.setSpring(new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+ mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP,
+ context.getResources().getDisplayMetrics());
+ }
+
+ /**
+ * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two.
+ * From https://en.wikipedia.org/wiki/Smoothstep
+ */
+ private static float smoothstep(float edge0, float edge1, float x) {
+ if (x < edge0) return 0;
+ if (x >= edge1) return 1;
+
+ x = (x - edge0) / (edge1 - edge0);
+ return x * x * (3 - 2 * x);
+ }
+
+ /**
+ * Linearly map x from range (a1, a2) to range (b1, b2).
+ */
+ private static float mapLinear(float x, float a1, float a2, float b1, float b2) {
+ return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
+ }
+
+ /**
+ * Linearly map a normalized value from (0, 1) to (min, max).
+ */
+ private static float mapRange(float value, float min, float max) {
+ return min + (value * (max - min));
+ }
+
+ private void startBackAnimation() {
+ if (mEnteringTarget == null || mClosingTarget == null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
+ return;
+ }
+ mTransaction.setAnimationTransaction();
+
+ // Offset start rectangle to align task bounds.
+ mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
+ mStartTaskRect.offsetTo(0, 0);
+
+ // Draw background with task background color.
+ mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
+ mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
+ }
+
+ private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
+ final float scale = targetRect.width() / mStartTaskRect.width();
+ mTransformMatrix.reset();
+ mTransformMatrix.setScale(scale, scale);
+ mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
+ mTransaction.setAlpha(leash, targetAlpha)
+ .setMatrix(leash, mTransformMatrix, mTmpFloat9)
+ .setWindowCrop(leash, mStartTaskRect)
+ .setCornerRadius(leash, mCornerRadius);
+ }
+
+ private void finishAnimation() {
+ if (mEnteringTarget != null) {
+ mEnteringTarget.leash.release();
+ mEnteringTarget = null;
+ }
+ if (mClosingTarget != null) {
+ mClosingTarget.leash.release();
+ mClosingTarget = null;
+ }
+ if (mBackground != null) {
+ mBackground.removeBackground(mTransaction);
+ }
+
+ mTransaction.apply();
+ mBackInProgress = false;
+ mTransformMatrix.reset();
+ mInitialTouchPos.set(0, 0);
+
+ if (mFinishCallback != null) {
+ try {
+ mFinishCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ mFinishCallback = null;
+ }
+ mEnteringProgressSpring.animateToFinalPosition(0);
+ mEnteringProgressSpring.skipToEnd();
+ mLeavingProgressSpring.animateToFinalPosition(0);
+ mLeavingProgressSpring.skipToEnd();
+ }
+
+ private void onGestureProgress(@NonNull BackEvent backEvent) {
+ if (!mBackInProgress) {
+ mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+ mBackInProgress = true;
+ }
+ mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+
+ float progress = backEvent.getProgress();
+ float springProgress = (progress > PROGRESS_COMMIT_THRESHOLD
+ ? mapLinear(progress, 0.1f, 1, TARGET_COMMIT_PROGRESS, 1)
+ : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
+ mLeavingProgressSpring.animateToFinalPosition(springProgress);
+ mEnteringProgressSpring.animateToFinalPosition(springProgress);
+ mBackground.onBackProgressed(progress);
+ }
+
+ private void onGestureCommitted() {
+ if (mEnteringTarget == null || mClosingTarget == null) {
+ finishAnimation();
+ return;
+ }
+ // End the fade animations
+ mLeavingProgressSpring.cancel();
+ mEnteringProgressSpring.cancel();
+
+ // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+ // coordinate of the gesture driven phase.
+ mEnteringRect.round(mEnteringStartRect);
+ mTransaction.hide(mClosingTarget.leash);
+
+ ValueAnimator valueAnimator =
+ ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION);
+ valueAnimator.setInterpolator(new DecelerateInterpolator());
+ valueAnimator.addUpdateListener(animation -> {
+ float progress = animation.getAnimatedFraction();
+ updatePostCommitEnteringAnimation(progress);
+ mTransaction.apply();
+ });
+
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finishAnimation();
+ }
+ });
+ valueAnimator.start();
+ }
+
+ private void updatePostCommitEnteringAnimation(float progress) {
+ float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
+ float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
+ float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
+ float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
+ float alpha = mapRange(progress, mEnteringProgress, 1.0f);
+
+ mEnteringRect.set(left, top, left + width, top + height);
+ applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
+ }
+
+ private float getEnteringProgress() {
+ return mEnteringProgress * SCALE_FACTOR;
+ }
+
+ private void setEnteringProgress(float value) {
+ mEnteringProgress = value / SCALE_FACTOR;
+ if (mEnteringTarget != null && mEnteringTarget.leash != null) {
+ transformWithProgress(
+ mEnteringProgress,
+ Math.max(
+ smoothstep(ENTER_ALPHA_THRESHOLD, 1, mEnteringProgress),
+ MIN_WINDOW_ALPHA), /* alpha */
+ mEnteringTarget.leash,
+ mEnteringRect,
+ -mWindowXShift,
+ 0
+ );
+ }
+ }
+
+ private float getLeavingProgress() {
+ return mLeavingProgress * SCALE_FACTOR;
+ }
+
+ private void setLeavingProgress(float value) {
+ mLeavingProgress = value / SCALE_FACTOR;
+ if (mClosingTarget != null && mClosingTarget.leash != null) {
+ transformWithProgress(
+ mLeavingProgress,
+ Math.max(
+ 1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
+ MIN_WINDOW_ALPHA),
+ mClosingTarget.leash,
+ mClosingRect,
+ 0,
+ mWindowXShift
+ );
+ }
+ }
+
+ private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
+ RectF targetRect, float deltaXMin, float deltaXMax) {
+ final float touchY = mTouchPos.y;
+
+ final int width = mStartTaskRect.width();
+ final int height = mStartTaskRect.height();
+
+ final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress);
+ final float closingScale = MIN_WINDOW_SCALE
+ + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE);
+ final float closingWidth = closingScale * width;
+ final float closingHeight = (float) height / width * closingWidth;
+
+ // Move the window along the X axis.
+ float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2;
+ closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax);
+
+ // Move the window along the Y axis.
+ final float deltaYRatio = (touchY - mInitialTouchPos.y) / height;
+ final float closingTop = (height - closingHeight) * 0.5f;
+ targetRect.set(
+ closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight);
+
+ applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA));
+ mTransaction.apply();
+ }
+
+ private final class Callback extends IOnBackInvokedCallback.Default {
+ @Override
+ public void onBackStarted(BackMotionEvent backEvent) {
+ mProgressAnimator.onBackStarted(backEvent,
+ CrossActivityAnimation.this::onGestureProgress);
+ }
+
+ @Override
+ public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
+ mProgressAnimator.onBackProgressed(backEvent);
+ }
+
+ @Override
+ public void onBackCancelled() {
+ mProgressAnimator.onBackCancelled(CrossActivityAnimation.this::finishAnimation);
+ }
+
+ @Override
+ public void onBackInvoked() {
+ mProgressAnimator.reset();
+ onGestureCommitted();
+ }
+ }
+
+ private final class Runner extends IRemoteAnimationRunner.Default {
+ @Override
+ public void onAnimationStart(
+ int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation.");
+ for (RemoteAnimationTarget a : apps) {
+ if (a.mode == MODE_CLOSING) {
+ mClosingTarget = a;
+ }
+ if (a.mode == MODE_OPENING) {
+ mEnteringTarget = a;
+ }
+ }
+
+ startBackAnimation();
+ mFinishCallback = finishedCallback;
+ }
+
+ @Override
+ public void onAnimationCancelled() {
+ finishAnimation();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
new file mode 100644
index 000000000000..a7dd27a0784f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -0,0 +1,364 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.back;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.window.BackEvent.EDGE_RIGHT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.RemoteException;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.window.BackEvent;
+import android.window.BackMotionEvent;
+import android.window.BackProgressAnimator;
+import android.window.IOnBackInvokedCallback;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+/**
+ * Controls the animation of swiping back and returning to another task.
+ *
+ * This is a two part animation. The first part is an animation that tracks gesture location to
+ * scale and move the closing and entering app windows.
+ * Once the gesture is committed, the second part remains the closing window in place.
+ * The entering window plays the rest of app opening transition to enter full screen.
+ *
+ * This animation is used only for apps that enable back dispatching via
+ * {@link android.window.OnBackInvokedDispatcher}. The controller registers
+ * an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back
+ * navigation to launcher starts.
+ */
+@ShellMainThread
+class CrossTaskBackAnimation {
+ private static final int BACKGROUNDCOLOR = 0x43433A;
+
+ /**
+ * Minimum scale of the entering window.
+ */
+ private static final float ENTERING_MIN_WINDOW_SCALE = 0.85f;
+
+ /**
+ * Minimum scale of the closing window.
+ */
+ private static final float CLOSING_MIN_WINDOW_SCALE = 0.75f;
+
+ /**
+ * Minimum color scale of the closing window.
+ */
+ private static final float CLOSING_MIN_WINDOW_COLOR_SCALE = 0.1f;
+
+ /**
+ * The margin between the entering window and the closing window
+ */
+ private static final int WINDOW_MARGIN = 35;
+
+ /** Max window translation in the Y axis. */
+ private static final int WINDOW_MAX_DELTA_Y = 160;
+
+ private final Rect mStartTaskRect = new Rect();
+ private final float mCornerRadius;
+
+ // The closing window properties.
+ private final RectF mClosingCurrentRect = new RectF();
+
+ // The entering window properties.
+ private final Rect mEnteringStartRect = new Rect();
+ private final RectF mEnteringCurrentRect = new RectF();
+
+ private final PointF mInitialTouchPos = new PointF();
+ private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
+
+ private final Matrix mTransformMatrix = new Matrix();
+
+ private final float[] mTmpFloat9 = new float[9];
+ private final float[] mTmpTranslate = {0, 0, 0};
+
+ private RemoteAnimationTarget mEnteringTarget;
+ private RemoteAnimationTarget mClosingTarget;
+ private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+
+ private boolean mBackInProgress = false;
+
+ private boolean mIsRightEdge;
+ private float mProgress = 0;
+ private PointF mTouchPos = new PointF();
+ private IRemoteAnimationFinishedCallback mFinishCallback;
+ private BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+ final BackAnimationRunner mBackAnimationRunner;
+
+ private final BackAnimationBackground mBackground;
+
+ CrossTaskBackAnimation(Context context, BackAnimationBackground background) {
+ mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+ mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+ mBackground = background;
+ }
+
+ private float getInterpolatedProgress(float backProgress) {
+ return 1 - (1 - backProgress) * (1 - backProgress) * (1 - backProgress);
+ }
+
+ private void startBackAnimation() {
+ if (mEnteringTarget == null || mClosingTarget == null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
+ return;
+ }
+
+ // Offset start rectangle to align task bounds.
+ mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
+ mStartTaskRect.offsetTo(0, 0);
+
+ // Draw background.
+ mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
+ BACKGROUNDCOLOR, mTransaction);
+ }
+
+ private void updateGestureBackProgress(float progress, BackEvent event) {
+ if (mEnteringTarget == null || mClosingTarget == null) {
+ return;
+ }
+
+ float touchX = event.getTouchX();
+ float touchY = event.getTouchY();
+ float dX = Math.abs(touchX - mInitialTouchPos.x);
+
+ // The 'follow width' is the width of the window if it completely matches
+ // the gesture displacement.
+ final int width = mStartTaskRect.width();
+ final int height = mStartTaskRect.height();
+
+ // The 'progress width' is the width of the window if it strictly linearly interpolates
+ // to minimum scale base on progress.
+ float enteringScale = mapRange(progress, 1, ENTERING_MIN_WINDOW_SCALE);
+ float closingScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_SCALE);
+ float closingColorScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_COLOR_SCALE);
+
+ // The final width is derived from interpolating between the follow with and progress width
+ // using gesture progress.
+ float enteringWidth = enteringScale * width;
+ float closingWidth = closingScale * width;
+ float enteringHeight = (float) height / width * enteringWidth;
+ float closingHeight = (float) height / width * closingWidth;
+
+ float deltaYRatio = (touchY - mInitialTouchPos.y) / height;
+ // Base the window movement in the Y axis on the touch movement in the Y axis.
+ float deltaY = (float) Math.sin(deltaYRatio * Math.PI * 0.5f) * WINDOW_MAX_DELTA_Y;
+ // Move the window along the Y axis.
+ float closingTop = (height - closingHeight) * 0.5f + deltaY;
+ float enteringTop = (height - enteringHeight) * 0.5f + deltaY;
+ // Move the window along the X axis.
+ float right = width - (progress * WINDOW_MARGIN);
+ float left = right - closingWidth;
+
+ mClosingCurrentRect.set(left, closingTop, right, closingTop + closingHeight);
+ mEnteringCurrentRect.set(left - enteringWidth - WINDOW_MARGIN, enteringTop,
+ left - WINDOW_MARGIN, enteringTop + enteringHeight);
+
+ applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
+ applyColorTransform(mClosingTarget.leash, closingColorScale);
+ applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
+ mTransaction.apply();
+
+ mBackground.onBackProgressed(progress);
+ }
+
+ private void updatePostCommitClosingAnimation(float progress) {
+ mTransaction.setLayer(mClosingTarget.leash, 0);
+ float alpha = mapRange(progress, 1, 0);
+ mTransaction.setAlpha(mClosingTarget.leash, alpha);
+ }
+
+ private void updatePostCommitEnteringAnimation(float progress) {
+ float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
+ float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
+ float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
+ float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
+
+ mEnteringCurrentRect.set(left, top, left + width, top + height);
+ applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
+ }
+
+ /** Transform the target window to match the target rect. */
+ private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) {
+ if (leash == null) {
+ return;
+ }
+
+ final float scale = targetRect.width() / mStartTaskRect.width();
+ mTransformMatrix.reset();
+ mTransformMatrix.setScale(scale, scale);
+ mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
+ mTransaction.setMatrix(leash, mTransformMatrix, mTmpFloat9)
+ .setWindowCrop(leash, mStartTaskRect)
+ .setCornerRadius(leash, cornerRadius);
+ }
+
+ private void applyColorTransform(SurfaceControl leash, float colorScale) {
+ if (leash == null) {
+ return;
+ }
+ computeScaleTransformMatrix(colorScale, mTmpFloat9);
+ mTransaction.setColorTransform(leash, mTmpFloat9, mTmpTranslate);
+ }
+
+ static void computeScaleTransformMatrix(float scale, float[] matrix) {
+ matrix[0] = scale;
+ matrix[1] = 0;
+ matrix[2] = 0;
+ matrix[3] = 0;
+ matrix[4] = scale;
+ matrix[5] = 0;
+ matrix[6] = 0;
+ matrix[7] = 0;
+ matrix[8] = scale;
+ }
+
+ private void finishAnimation() {
+ if (mEnteringTarget != null) {
+ mEnteringTarget.leash.release();
+ mEnteringTarget = null;
+ }
+ if (mClosingTarget != null) {
+ mClosingTarget.leash.release();
+ mClosingTarget = null;
+ }
+
+ if (mBackground != null) {
+ mBackground.removeBackground(mTransaction);
+ }
+
+ mTransaction.apply();
+ mBackInProgress = false;
+ mTransformMatrix.reset();
+ mClosingCurrentRect.setEmpty();
+ mInitialTouchPos.set(0, 0);
+
+ if (mFinishCallback != null) {
+ try {
+ mFinishCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ mFinishCallback = null;
+ }
+ }
+
+ private void onGestureProgress(@NonNull BackEvent backEvent) {
+ if (!mBackInProgress) {
+ mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+ mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
+ mBackInProgress = true;
+ }
+ mProgress = backEvent.getProgress();
+ mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+ updateGestureBackProgress(getInterpolatedProgress(mProgress), backEvent);
+ }
+
+ private void onGestureCommitted() {
+ if (mEnteringTarget == null || mClosingTarget == null) {
+ finishAnimation();
+ return;
+ }
+
+ // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+ // coordinate of the gesture driven phase.
+ mEnteringCurrentRect.round(mEnteringStartRect);
+
+ ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(300);
+ valueAnimator.setInterpolator(mInterpolator);
+ valueAnimator.addUpdateListener(animation -> {
+ float progress = animation.getAnimatedFraction();
+ updatePostCommitEnteringAnimation(progress);
+ updatePostCommitClosingAnimation(progress);
+ mTransaction.apply();
+ });
+
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finishAnimation();
+ }
+ });
+ valueAnimator.start();
+ }
+
+ private static float mapRange(float value, float min, float max) {
+ return min + (value * (max - min));
+ }
+
+ private final class Callback extends IOnBackInvokedCallback.Default {
+ @Override
+ public void onBackStarted(BackMotionEvent backEvent) {
+ mProgressAnimator.onBackStarted(backEvent,
+ CrossTaskBackAnimation.this::onGestureProgress);
+ }
+
+ @Override
+ public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
+ mProgressAnimator.onBackProgressed(backEvent);
+ }
+
+ @Override
+ public void onBackCancelled() {
+ mProgressAnimator.onBackCancelled(CrossTaskBackAnimation.this::finishAnimation);
+ }
+
+ @Override
+ public void onBackInvoked() {
+ mProgressAnimator.reset();
+ onGestureCommitted();
+ }
+ };
+
+ private final class Runner extends IRemoteAnimationRunner.Default {
+ @Override
+ public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to task animation.");
+ for (RemoteAnimationTarget a : apps) {
+ if (a.mode == MODE_CLOSING) {
+ mClosingTarget = a;
+ }
+ if (a.mode == MODE_OPENING) {
+ mEnteringTarget = a;
+ }
+ }
+
+ startBackAnimation();
+ mFinishCallback = finishedCallback;
+ }
+ };
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
new file mode 100644
index 000000000000..2d6ec7547187
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
@@ -0,0 +1,419 @@
+/*
+ * 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.back;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.FloatProperty;
+import android.view.Choreographer;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.WindowManager.LayoutParams;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Transformation;
+import android.window.BackEvent;
+import android.window.BackMotionEvent;
+import android.window.BackNavigationInfo;
+import android.window.BackProgressAnimator;
+import android.window.IOnBackInvokedCallback;
+
+import com.android.internal.R;
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.policy.TransitionAnimation;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+/**
+ * Class that handle customized close activity transition animation.
+ */
+@ShellMainThread
+class CustomizeActivityAnimation {
+ private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+ final BackAnimationRunner mBackAnimationRunner;
+ private final float mCornerRadius;
+ private final SurfaceControl.Transaction mTransaction;
+ private final BackAnimationBackground mBackground;
+ private RemoteAnimationTarget mEnteringTarget;
+ private RemoteAnimationTarget mClosingTarget;
+ private IRemoteAnimationFinishedCallback mFinishCallback;
+ /** Duration of post animation after gesture committed. */
+ private static final int POST_ANIMATION_DURATION = 250;
+
+ private static final int SCALE_FACTOR = 1000;
+ private final SpringAnimation mProgressSpring;
+ private float mLatestProgress = 0.0f;
+
+ private static final float TARGET_COMMIT_PROGRESS = 0.5f;
+
+ private final float[] mTmpFloat9 = new float[9];
+ private final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
+
+ final CustomAnimationLoader mCustomAnimationLoader;
+ private Animation mEnterAnimation;
+ private Animation mCloseAnimation;
+ private int mNextBackgroundColor;
+ final Transformation mTransformation = new Transformation();
+
+ private final Choreographer mChoreographer;
+
+ CustomizeActivityAnimation(Context context, BackAnimationBackground background) {
+ this(context, background, new SurfaceControl.Transaction(), null);
+ }
+
+ CustomizeActivityAnimation(Context context, BackAnimationBackground background,
+ SurfaceControl.Transaction transaction, Choreographer choreographer) {
+ mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+ mBackground = background;
+ mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+ mCustomAnimationLoader = new CustomAnimationLoader(context);
+
+ mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
+ mProgressSpring.setSpring(new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+ mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction;
+ mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance();
+ }
+
+ private float getLatestProgress() {
+ return mLatestProgress * SCALE_FACTOR;
+ }
+ private void setLatestProgress(float value) {
+ mLatestProgress = value / SCALE_FACTOR;
+ applyTransformTransaction(mLatestProgress);
+ }
+
+ private static final FloatProperty<CustomizeActivityAnimation> ENTER_PROGRESS_PROP =
+ new FloatProperty<>("enter") {
+ @Override
+ public void setValue(CustomizeActivityAnimation anim, float value) {
+ anim.setLatestProgress(value);
+ }
+
+ @Override
+ public Float get(CustomizeActivityAnimation object) {
+ return object.getLatestProgress();
+ }
+ };
+
+ // The target will lose focus when alpha == 0, so keep a minimum value for it.
+ private static float keepMinimumAlpha(float transAlpha) {
+ return Math.max(transAlpha, 0.005f);
+ }
+
+ private static void initializeAnimation(Animation animation, Rect bounds) {
+ final int width = bounds.width();
+ final int height = bounds.height();
+ animation.initialize(width, height, width, height);
+ }
+
+ private void startBackAnimation() {
+ if (mEnteringTarget == null || mClosingTarget == null
+ || mCloseAnimation == null || mEnterAnimation == null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
+ return;
+ }
+ initializeAnimation(mCloseAnimation, mClosingTarget.localBounds);
+ initializeAnimation(mEnterAnimation, mEnteringTarget.localBounds);
+
+ // Draw background with task background color.
+ if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) {
+ mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
+ mNextBackgroundColor == Color.TRANSPARENT
+ ? mEnteringTarget.taskInfo.taskDescription.getBackgroundColor()
+ : mNextBackgroundColor,
+ mTransaction);
+ }
+ }
+
+ private void applyTransformTransaction(float progress) {
+ if (mClosingTarget == null || mEnteringTarget == null) {
+ return;
+ }
+ applyTransform(mClosingTarget.leash, progress, mCloseAnimation);
+ applyTransform(mEnteringTarget.leash, progress, mEnterAnimation);
+ mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+ mTransaction.apply();
+ }
+
+ private void applyTransform(SurfaceControl leash, float progress, Animation animation) {
+ mTransformation.clear();
+ animation.getTransformationAt(progress, mTransformation);
+ mTransaction.setMatrix(leash, mTransformation.getMatrix(), mTmpFloat9);
+ mTransaction.setAlpha(leash, keepMinimumAlpha(mTransformation.getAlpha()));
+ mTransaction.setCornerRadius(leash, mCornerRadius);
+ }
+
+ void finishAnimation() {
+ if (mCloseAnimation != null) {
+ mCloseAnimation.reset();
+ mCloseAnimation = null;
+ }
+ if (mEnterAnimation != null) {
+ mEnterAnimation.reset();
+ mEnterAnimation = null;
+ }
+ if (mEnteringTarget != null) {
+ mEnteringTarget.leash.release();
+ mEnteringTarget = null;
+ }
+ if (mClosingTarget != null) {
+ mClosingTarget.leash.release();
+ mClosingTarget = null;
+ }
+ if (mBackground != null) {
+ mBackground.removeBackground(mTransaction);
+ }
+ mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+ mTransaction.apply();
+ mTransformation.clear();
+ mLatestProgress = 0;
+ mNextBackgroundColor = Color.TRANSPARENT;
+ if (mFinishCallback != null) {
+ try {
+ mFinishCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ mFinishCallback = null;
+ }
+ mProgressSpring.animateToFinalPosition(0);
+ mProgressSpring.skipToEnd();
+ }
+
+ void onGestureProgress(@NonNull BackEvent backEvent) {
+ if (mEnteringTarget == null || mClosingTarget == null
+ || mCloseAnimation == null || mEnterAnimation == null) {
+ return;
+ }
+
+ final float progress = backEvent.getProgress();
+
+ float springProgress = (progress > 0.1f
+ ? mapLinear(progress, 0.1f, 1f, TARGET_COMMIT_PROGRESS, 1f)
+ : mapLinear(progress, 0, 1f, 0f, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
+
+ mProgressSpring.animateToFinalPosition(springProgress);
+ }
+
+ static float mapLinear(float x, float a1, float a2, float b1, float b2) {
+ return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
+ }
+
+ void onGestureCommitted() {
+ if (mEnteringTarget == null || mClosingTarget == null
+ || mCloseAnimation == null || mEnterAnimation == null) {
+ finishAnimation();
+ return;
+ }
+ mProgressSpring.cancel();
+
+ // Enter phase 2 of the animation
+ final ValueAnimator valueAnimator = ValueAnimator.ofFloat(mLatestProgress, 1f)
+ .setDuration(POST_ANIMATION_DURATION);
+ valueAnimator.setInterpolator(mDecelerateInterpolator);
+ valueAnimator.addUpdateListener(animation -> {
+ float progress = (float) animation.getAnimatedValue();
+ applyTransformTransaction(progress);
+ });
+
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finishAnimation();
+ }
+ });
+ valueAnimator.start();
+ }
+
+ /**
+ * Load customize animation before animation start.
+ */
+ boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) {
+ final AnimationLoadResult result = mCustomAnimationLoader.loadAll(animationInfo);
+ if (result != null) {
+ mCloseAnimation = result.mCloseAnimation;
+ mEnterAnimation = result.mEnterAnimation;
+ mNextBackgroundColor = result.mBackgroundColor;
+ return true;
+ }
+ return false;
+ }
+
+ private final class Callback extends IOnBackInvokedCallback.Default {
+ @Override
+ public void onBackStarted(BackMotionEvent backEvent) {
+ mProgressAnimator.onBackStarted(backEvent,
+ CustomizeActivityAnimation.this::onGestureProgress);
+ }
+
+ @Override
+ public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
+ mProgressAnimator.onBackProgressed(backEvent);
+ }
+
+ @Override
+ public void onBackCancelled() {
+ mProgressAnimator.onBackCancelled(CustomizeActivityAnimation.this::finishAnimation);
+ }
+
+ @Override
+ public void onBackInvoked() {
+ mProgressAnimator.reset();
+ onGestureCommitted();
+ }
+ }
+
+ private final class Runner extends IRemoteAnimationRunner.Default {
+ @Override
+ public void onAnimationStart(
+ int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to customize animation.");
+ for (RemoteAnimationTarget a : apps) {
+ if (a.mode == MODE_CLOSING) {
+ mClosingTarget = a;
+ }
+ if (a.mode == MODE_OPENING) {
+ mEnteringTarget = a;
+ }
+ }
+ if (mCloseAnimation == null || mEnterAnimation == null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW,
+ "No animation loaded, should choose cross-activity animation?");
+ }
+
+ startBackAnimation();
+ mFinishCallback = finishedCallback;
+ }
+
+ @Override
+ public void onAnimationCancelled() {
+ finishAnimation();
+ }
+ }
+
+
+ static final class AnimationLoadResult {
+ Animation mCloseAnimation;
+ Animation mEnterAnimation;
+ int mBackgroundColor;
+ }
+
+ /**
+ * Helper class to load custom animation.
+ */
+ static class CustomAnimationLoader {
+ final TransitionAnimation mTransitionAnimation;
+
+ CustomAnimationLoader(Context context) {
+ mTransitionAnimation = new TransitionAnimation(
+ context, false /* debug */, "CustomizeBackAnimation");
+ }
+
+ /**
+ * Load both enter and exit animation for the close activity transition.
+ * Note that the result is only valid if the exit animation has set and loaded success.
+ * If the entering animation has not set(i.e. 0), here will load the default entering
+ * animation for it.
+ *
+ * @param animationInfo The information of customize animation, which can be set from
+ * {@link Activity#overrideActivityTransition} and/or
+ * {@link LayoutParams#windowAnimations}
+ */
+ AnimationLoadResult loadAll(BackNavigationInfo.CustomAnimationInfo animationInfo) {
+ if (animationInfo.getPackageName().isEmpty()) {
+ return null;
+ }
+ final Animation close = loadAnimation(animationInfo, false);
+ if (close == null) {
+ return null;
+ }
+ final Animation open = loadAnimation(animationInfo, true);
+ AnimationLoadResult result = new AnimationLoadResult();
+ result.mCloseAnimation = close;
+ result.mEnterAnimation = open;
+ result.mBackgroundColor = animationInfo.getCustomBackground();
+ return result;
+ }
+
+ /**
+ * Load enter or exit animation from CustomAnimationInfo
+ * @param animationInfo The information for customize animation.
+ * @param enterAnimation true when load for enter animation, false for exit animation.
+ * @return Loaded animation.
+ */
+ @Nullable
+ Animation loadAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo,
+ boolean enterAnimation) {
+ Animation a = null;
+ // Activity#overrideActivityTransition has higher priority than windowAnimations
+ // Try to get animation from Activity#overrideActivityTransition
+ if ((enterAnimation && animationInfo.getCustomEnterAnim() != 0)
+ || (!enterAnimation && animationInfo.getCustomExitAnim() != 0)) {
+ a = mTransitionAnimation.loadAppTransitionAnimation(
+ animationInfo.getPackageName(),
+ enterAnimation ? animationInfo.getCustomEnterAnim()
+ : animationInfo.getCustomExitAnim());
+ } else if (animationInfo.getWindowAnimations() != 0) {
+ // try to get animation from LayoutParams#windowAnimations
+ a = mTransitionAnimation.loadAnimationAttr(animationInfo.getPackageName(),
+ animationInfo.getWindowAnimations(), enterAnimation
+ ? R.styleable.WindowAnimation_activityCloseEnterAnimation
+ : R.styleable.WindowAnimation_activityCloseExitAnimation,
+ false /* translucent */);
+ }
+ // Only allow to load default animation for opening target.
+ if (a == null && enterAnimation) {
+ a = loadDefaultOpenAnimation();
+ }
+ if (a != null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a);
+ } else {
+ ProtoLog.e(WM_SHELL_BACK_PREVIEW, "No custom animation loaded");
+ }
+ return a;
+ }
+
+ private Animation loadDefaultOpenAnimation() {
+ return mTransitionAnimation.loadDefaultAnimationAttr(
+ R.styleable.WindowAnimation_activityCloseEnterAnimation,
+ false /* translucent */);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl
index 6311f879fd45..1a35de47e977 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/IBackAnimation.aidl
@@ -16,18 +16,20 @@
package com.android.wm.shell.back;
+import com.android.internal.view.AppearanceRegion;
+import android.view.IRemoteAnimationRunner;
import android.window.IOnBackInvokedCallback;
/**
* Interface for Launcher process to register back invocation callbacks.
*/
interface IBackAnimation {
-
/**
- * Sets a {@link IOnBackInvokedCallback} to be invoked when
+ * Sets a {@link IOnBackInvokedCallback} and a {@link IRemoteAnimationRunner} to be invoked when
* back navigation has type {@link BackNavigationInfo#TYPE_RETURN_TO_HOME}.
*/
- void setBackToLauncherCallback(in IOnBackInvokedCallback callback);
+ void setBackToLauncherCallback(in IOnBackInvokedCallback callback,
+ in IRemoteAnimationRunner runner);
/**
* Clears the previously registered {@link IOnBackInvokedCallback}.
@@ -35,11 +37,7 @@ interface IBackAnimation {
void clearBackToLauncherCallback();
/**
- * Notifies Shell that the back to launcher animation has fully finished
- * (including the transition animation that runs after the finger is lifted).
- *
- * At this point the top window leash (if one was created) should be ready to be released.
- * //TODO: Remove once we play the transition animation through shell transitions.
+ * Uses launcher flags to update the system bar color.
*/
- void onBackToLauncherAnimationFinished();
+ void customizeStatusBarAppearance(in AppearanceRegion appearance);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/StatusBarCustomizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/StatusBarCustomizer.java
new file mode 100644
index 000000000000..5e876127e9b4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/StatusBarCustomizer.java
@@ -0,0 +1,30 @@
+/*
+ * 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.back;
+
+import com.android.internal.view.AppearanceRegion;
+
+/**
+ * Interface to customize the system bar color.
+ */
+public interface StatusBarCustomizer {
+ /**
+ * Called when the status bar color needs to be changed.
+ * @param appearance The region of appearance.
+ */
+ void customizeStatusBarAppearance(AppearanceRegion appearance);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING
new file mode 100644
index 000000000000..837d5ff3b073
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING
@@ -0,0 +1,32 @@
+{
+ "presubmit": [
+ {
+ "name": "WMShellUnitTests",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "include-filter": "com.android.wm.shell.back"
+ }
+ ]
+ },
+ {
+ "name": "CtsWindowManagerDeviceTestCases",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "include-filter": "android.server.wm.BackGestureInvokedTest"
+ },
+ {
+ "include-filter": "android.server.wm.BackNavigationTests"
+ },
+ {
+ "include-filter": "android.server.wm.OnBackInvokedCallbackGestureTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
index 695ef4e66302..a0ada39b459e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -16,7 +16,10 @@
package com.android.wm.shell.back;
+import android.annotation.FloatRange;
import android.os.SystemProperties;
+import android.util.MathUtils;
+import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import android.window.BackEvent;
import android.window.BackMotionEvent;
@@ -25,11 +28,13 @@ import android.window.BackMotionEvent;
* Helper class to record the touch location for gesture and generate back events.
*/
class TouchTracker {
- private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
- "persist.wm.debug.predictive_back_progress_threshold";
- private static final int PROGRESS_THRESHOLD = SystemProperties
- .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
- private float mProgressThreshold;
+ private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP =
+ "persist.wm.debug.predictive_back_linear_distance";
+ private static final int LINEAR_DISTANCE = SystemProperties
+ .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1);
+ private float mLinearDistance = LINEAR_DISTANCE;
+ private float mMaxDistance;
+ private float mNonLinearFactor;
/**
* Location of the latest touch event
*/
@@ -42,11 +47,13 @@ class TouchTracker {
*/
private float mInitTouchX;
private float mInitTouchY;
+ private float mLatestVelocityX;
+ private float mLatestVelocityY;
private float mStartThresholdX;
private int mSwipeEdge;
private boolean mCancelled;
- void update(float touchX, float touchY) {
+ void update(float touchX, float touchY, float velocityX, float velocityY) {
/**
* If back was previously cancelled but the user has started swiping in the forward
* direction again, restart back.
@@ -58,6 +65,8 @@ class TouchTracker {
}
mLatestTouchX = touchX;
mLatestTouchY = touchY;
+ mLatestVelocityX = velocityX;
+ mLatestVelocityY = velocityY;
}
void setTriggerBack(boolean triggerBack) {
@@ -84,37 +93,97 @@ class TouchTracker {
}
BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
- return new BackMotionEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
+ return new BackMotionEvent(
+ /* touchX = */ mInitTouchX,
+ /* touchY = */ mInitTouchY,
+ /* progress = */ 0,
+ /* velocityX = */ 0,
+ /* velocityY = */ 0,
+ /* swipeEdge = */ mSwipeEdge,
+ /* departingAnimationTarget = */ target);
}
BackMotionEvent createProgressEvent() {
- float progressThreshold = PROGRESS_THRESHOLD >= 0
- ? PROGRESS_THRESHOLD : mProgressThreshold;
- progressThreshold = progressThreshold == 0 ? 1 : progressThreshold;
float progress = 0;
// Progress is always 0 when back is cancelled and not restarted.
if (!mCancelled) {
- // If back is committed, progress is the distance between the last and first touch
- // point, divided by the max drag distance. Otherwise, it's the distance between
- // the last touch point and the starting threshold, divided by max drag distance.
- // The starting threshold is initially the first touch location, and updated to
- // the location everytime back is restarted after being cancelled.
- float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
- float deltaX = Math.max(
- mSwipeEdge == BackEvent.EDGE_LEFT
- ? mLatestTouchX - startX
- : startX - mLatestTouchX,
- 0);
- progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1);
+ progress = getProgress(mLatestTouchX);
}
return createProgressEvent(progress);
}
+ /**
+ * Progress value computed from the touch position.
+ *
+ * @param touchX the X touch position of the {@link MotionEvent}.
+ * @return progress value
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ float getProgress(float touchX) {
+ // If back is committed, progress is the distance between the last and first touch
+ // point, divided by the max drag distance. Otherwise, it's the distance between
+ // the last touch point and the starting threshold, divided by max drag distance.
+ // The starting threshold is initially the first touch location, and updated to
+ // the location everytime back is restarted after being cancelled.
+ float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
+ float deltaX = Math.abs(startX - touchX);
+ float linearDistance = mLinearDistance;
+ float maxDistance = getMaxDistance();
+ maxDistance = maxDistance == 0 ? 1 : maxDistance;
+ float progress;
+ if (linearDistance < maxDistance) {
+ // Up to linearDistance it behaves linearly, then slowly reaches 1f.
+
+ // maxDistance is composed of linearDistance + nonLinearDistance
+ float nonLinearDistance = maxDistance - linearDistance;
+ float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor;
+
+ boolean isLinear = deltaX <= linearDistance;
+ if (isLinear) {
+ progress = deltaX / initialTarget;
+ } else {
+ float nonLinearDeltaX = deltaX - linearDistance;
+ float nonLinearProgress = nonLinearDeltaX / nonLinearDistance;
+ float currentTarget = MathUtils.lerp(
+ /* start = */ initialTarget,
+ /* stop = */ maxDistance,
+ /* amount = */ nonLinearProgress);
+ progress = deltaX / currentTarget;
+ }
+ } else {
+ // Always linear behavior.
+ progress = deltaX / maxDistance;
+ }
+ return MathUtils.constrain(progress, 0, 1);
+ }
+
+ /**
+ * Maximum distance in pixels.
+ * Progress is considered to be completed (1f) when this limit is exceeded.
+ */
+ float getMaxDistance() {
+ return mMaxDistance;
+ }
+
BackMotionEvent createProgressEvent(float progress) {
- return new BackMotionEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
+ return new BackMotionEvent(
+ /* touchX = */ mLatestTouchX,
+ /* touchY = */ mLatestTouchY,
+ /* progress = */ progress,
+ /* velocityX = */ mLatestVelocityX,
+ /* velocityY = */ mLatestVelocityY,
+ /* swipeEdge = */ mSwipeEdge,
+ /* departingAnimationTarget = */ null);
}
- public void setProgressThreshold(float progressThreshold) {
- mProgressThreshold = progressThreshold;
+ public void setProgressThresholds(float linearDistance, float maxDistance,
+ float nonLinearFactor) {
+ if (LINEAR_DISTANCE >= 0) {
+ mLinearDistance = LINEAR_DISTANCE;
+ } else {
+ mLinearDistance = linearDistance;
+ }
+ mMaxDistance = maxDistance;
+ mNonLinearFactor = nonLinearFactor;
}
}
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 e24c2286013d..102f2cb4b8d0 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
@@ -47,6 +47,10 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
+import com.android.launcher3.icons.BubbleIconFactory;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.common.bubbles.BubbleInfo;
import java.io.PrintWriter;
import java.util.List;
@@ -60,7 +64,11 @@ import java.util.concurrent.Executor;
public class Bubble implements BubbleViewProvider {
private static final String TAG = "Bubble";
- public static final String KEY_APP_BUBBLE = "key_app_bubble";
+ /** A string suffix used in app bubbles' {@link #mKey}. */
+ private static final String KEY_APP_BUBBLE = "key_app_bubble";
+
+ /** Whether the bubble is an app bubble. */
+ private final boolean mIsAppBubble;
private final String mKey;
@Nullable
@@ -86,8 +94,18 @@ public class Bubble implements BubbleViewProvider {
private String mAppName;
private ShortcutInfo mShortcutInfo;
private String mMetadataShortcutId;
+
+ /**
+ * If {@link BubbleController#isShowingAsBubbleBar()} is true, the only view that will be
+ * populated will be {@link #mBubbleBarExpandedView}. If it is false, {@link #mIconView}
+ * and {@link #mExpandedView} will be populated.
+ */
+ @Nullable
private BadgedImageView mIconView;
+ @Nullable
private BubbleExpandedView mExpandedView;
+ @Nullable
+ private BubbleBarExpandedView mBubbleBarExpandedView;
private BubbleViewInfoTask mInflationTask;
private boolean mInflateSynchronously;
@@ -167,7 +185,7 @@ public class Bubble implements BubbleViewProvider {
private PendingIntent mDeleteIntent;
/**
- * Used only for a special bubble in the stack that has the key {@link #KEY_APP_BUBBLE}.
+ * Used only for a special bubble in the stack that has {@link #mIsAppBubble} set to true.
* There can only be one of these bubbles in the stack and this intent will be populated for
* that bubble.
*/
@@ -202,22 +220,54 @@ public class Bubble implements BubbleViewProvider {
mMainExecutor = mainExecutor;
mTaskId = taskId;
mBubbleMetadataFlagListener = listener;
+ mIsAppBubble = false;
}
- public Bubble(Intent intent,
+ private Bubble(
+ Intent intent,
UserHandle user,
+ @Nullable Icon icon,
+ boolean isAppBubble,
+ String key,
Executor mainExecutor) {
- mKey = KEY_APP_BUBBLE;
mGroupKey = null;
mLocusId = null;
mFlags = 0;
mUser = user;
+ mIcon = icon;
+ mIsAppBubble = isAppBubble;
+ mKey = key;
mShowBubbleUpdateDot = false;
mMainExecutor = mainExecutor;
mTaskId = INVALID_TASK_ID;
mAppIntent = intent;
mDesiredHeight = Integer.MAX_VALUE;
mPackageName = intent.getPackage();
+
+ }
+
+ /** Creates an app bubble. */
+ public static Bubble createAppBubble(
+ Intent intent,
+ UserHandle user,
+ @Nullable Icon icon,
+ Executor mainExecutor) {
+ return new Bubble(intent,
+ user,
+ icon,
+ /* isAppBubble= */ true,
+ /* key= */ getAppBubbleKeyForApp(intent.getPackage(), user),
+ mainExecutor);
+ }
+
+ /**
+ * Returns the key for an app bubble from an app with package name, {@code packageName} on an
+ * Android user, {@code user}.
+ */
+ public static String getAppBubbleKeyForApp(String packageName, UserHandle user) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(user);
+ return KEY_APP_BUBBLE + ":" + user.getIdentifier() + ":" + packageName;
}
@VisibleForTesting(visibility = PRIVATE)
@@ -225,6 +275,7 @@ public class Bubble implements BubbleViewProvider {
final Bubbles.BubbleMetadataFlagListener listener,
final Bubbles.PendingIntentCanceledListener intentCancelListener,
Executor mainExecutor) {
+ mIsAppBubble = false;
mKey = entry.getKey();
mGroupKey = entry.getGroupKey();
mLocusId = entry.getLocusId();
@@ -242,6 +293,18 @@ public class Bubble implements BubbleViewProvider {
setEntry(entry);
}
+ /** Converts this bubble into a {@link BubbleInfo} object to be shared with external callers. */
+ public BubbleInfo asBubbleBarBubble() {
+ return new BubbleInfo(getKey(),
+ getFlags(),
+ getShortcutId(),
+ getIcon(),
+ getUser().getIdentifier(),
+ getPackageName(),
+ getTitle(),
+ isImportantConversation());
+ }
+
@Override
public String getKey() {
return mKey;
@@ -314,13 +377,19 @@ public class Bubble implements BubbleViewProvider {
return mIconView;
}
- @Override
@Nullable
+ @Override
public BubbleExpandedView getExpandedView() {
return mExpandedView;
}
@Nullable
+ @Override
+ public BubbleBarExpandedView getBubbleBarExpandedView() {
+ return mBubbleBarExpandedView;
+ }
+
+ @Nullable
public String getTitle() {
return mTitle;
}
@@ -351,6 +420,9 @@ public class Bubble implements BubbleViewProvider {
mExpandedView.cleanUpExpandedState();
mExpandedView = null;
}
+ if (mBubbleBarExpandedView != null) {
+ mBubbleBarExpandedView.cleanUpExpandedState();
+ }
if (mIntent != null) {
mIntent.unregisterCancelListener(mIntentCancelListener);
}
@@ -397,16 +469,16 @@ public class Bubble implements BubbleViewProvider {
* @param callback the callback to notify one the bubble is ready to be displayed.
* @param context the context for the bubble.
* @param controller the bubble controller.
- * @param stackView the stackView the bubble is eventually added to.
+ * @param stackView the view the bubble is added to, iff showing as floating.
+ * @param layerView the layer the bubble is added to, iff showing in the bubble bar.
* @param iconFactory the icon factory use to create images for the bubble.
- * @param badgeIconFactory the icon factory to create app badges for the bubble.
*/
void inflate(BubbleViewInfoTask.Callback callback,
Context context,
BubbleController controller,
- BubbleStackView stackView,
+ @Nullable BubbleStackView stackView,
+ @Nullable BubbleBarLayerView layerView,
BubbleIconFactory iconFactory,
- BubbleBadgeIconFactory badgeIconFactory,
boolean skipInflation) {
if (isBubbleLoading()) {
mInflationTask.cancel(true /* mayInterruptIfRunning */);
@@ -415,8 +487,8 @@ public class Bubble implements BubbleViewProvider {
context,
controller,
stackView,
+ layerView,
iconFactory,
- badgeIconFactory,
skipInflation,
callback,
mMainExecutor);
@@ -432,7 +504,7 @@ public class Bubble implements BubbleViewProvider {
}
boolean isInflated() {
- return mIconView != null && mExpandedView != null;
+ return (mIconView != null && mExpandedView != null) || mBubbleBarExpandedView != null;
}
void stopInflation() {
@@ -446,6 +518,7 @@ public class Bubble implements BubbleViewProvider {
if (!isInflated()) {
mIconView = info.imageView;
mExpandedView = info.expandedView;
+ mBubbleBarExpandedView = info.bubbleBarExpandedView;
}
mShortcutInfo = info.shortcutInfo;
@@ -456,7 +529,7 @@ public class Bubble implements BubbleViewProvider {
mFlyoutMessage = info.flyoutMessage;
mBadgeBitmap = info.badgeBitmap;
- mRawBadgeBitmap = info.mRawBadgeBitmap;
+ mRawBadgeBitmap = info.rawBadgeBitmap;
mBubbleBitmap = info.bubbleBitmap;
mDotColor = info.dotColor;
@@ -465,6 +538,9 @@ public class Bubble implements BubbleViewProvider {
if (mExpandedView != null) {
mExpandedView.update(this /* bubble */);
}
+ if (mBubbleBarExpandedView != null) {
+ mBubbleBarExpandedView.update(this /* bubble */);
+ }
if (mIconView != null) {
mIconView.setRenderedBubble(this /* bubble */);
}
@@ -543,8 +619,13 @@ public class Bubble implements BubbleViewProvider {
}
}
+ /**
+ * @return the icon set on BubbleMetadata, if it exists. This is only non-null for bubbles
+ * created via a PendingIntent. This is null for bubbles created by a shortcut, as we use the
+ * icon from the shortcut.
+ */
@Nullable
- Icon getIcon() {
+ public Icon getIcon() {
return mIcon;
}
@@ -589,6 +670,9 @@ public class Bubble implements BubbleViewProvider {
*/
@Override
public int getTaskId() {
+ if (mBubbleBarExpandedView != null) {
+ return mBubbleBarExpandedView.getTaskId();
+ }
return mExpandedView != null ? mExpandedView.getTaskId() : mTaskId;
}
@@ -638,6 +722,13 @@ public class Bubble implements BubbleViewProvider {
}
/**
+ * Whether this bubble is conversation
+ */
+ public boolean isConversation() {
+ return null != mShortcutInfo;
+ }
+
+ /**
* Sets whether this notification should be suppressed in the shade.
*/
@VisibleForTesting
@@ -761,7 +852,7 @@ public class Bubble implements BubbleViewProvider {
}
boolean isAppBubble() {
- return KEY_APP_BUBBLE.equals(mKey);
+ return mIsAppBubble;
}
Intent getSettingsIntent(final Context context) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
deleted file mode 100644
index 56b13b8dcd46..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.bubbles;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Path;
-import android.graphics.Rect;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.Drawable;
-
-import com.android.launcher3.icons.BaseIconFactory;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.wm.shell.R;
-
-/**
- * Factory for creating app badge icons that are shown on bubbles.
- */
-public class BubbleBadgeIconFactory extends BaseIconFactory {
-
- public BubbleBadgeIconFactory(Context context) {
- super(context, context.getResources().getConfiguration().densityDpi,
- context.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size));
- }
-
- /**
- * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This
- * will include the workprofile indicator on the badge if appropriate.
- */
- BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) {
- if (userBadgedAppIcon instanceof AdaptiveIconDrawable) {
- AdaptiveIconDrawable ad = (AdaptiveIconDrawable) userBadgedAppIcon;
- userBadgedAppIcon = new CircularAdaptiveIcon(ad.getBackground(), ad.getForeground());
- }
- if (isImportantConversation) {
- userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
- }
- Bitmap userBadgedBitmap = createIconBitmap(
- userBadgedAppIcon, 1, MODE_WITH_SHADOW);
- return createIconBitmap(userBadgedBitmap);
- }
-
- private class CircularRingDrawable extends CircularAdaptiveIcon {
-
- final int mImportantConversationColor;
- final int mRingWidth;
- final Rect mInnerBounds = new Rect();
-
- final Drawable mDr;
-
- CircularRingDrawable(Drawable dr) {
- super(null, null);
- mDr = dr;
- mImportantConversationColor = mContext.getResources().getColor(
- R.color.important_conversation, null);
- mRingWidth = mContext.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.importance_ring_stroke_width);
- }
-
- @Override
- public void draw(Canvas canvas) {
- int save = canvas.save();
- canvas.clipPath(getIconMask());
- canvas.drawColor(mImportantConversationColor);
- mInnerBounds.set(getBounds());
- mInnerBounds.inset(mRingWidth, mRingWidth);
- canvas.translate(mInnerBounds.left, mInnerBounds.top);
- mDr.setBounds(0, 0, mInnerBounds.width(), mInnerBounds.height());
- mDr.draw(canvas);
- canvas.restoreToCount(save);
- }
- }
-
- private static class CircularAdaptiveIcon extends AdaptiveIconDrawable {
-
- final Path mPath = new Path();
-
- CircularAdaptiveIcon(Drawable bg, Drawable fg) {
- super(bg, fg);
- }
-
- @Override
- public Path getIconMask() {
- mPath.reset();
- Rect bounds = getBounds();
- mPath.addOval(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
- return mPath;
- }
-
- @Override
- public void draw(Canvas canvas) {
- int save = canvas.save();
- canvas.clipPath(getIconMask());
-
- Drawable d;
- if ((d = getBackground()) != null) {
- d.draw(canvas);
- }
- if ((d = getForeground()) != null) {
- d.draw(canvas);
- }
- canvas.restoreToCount(save);
- }
- }
-}
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 2b3ee76b1ab5..f235cd7e8a6e 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
@@ -24,7 +24,6 @@ import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
@@ -38,7 +37,9 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
+import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -57,7 +58,9 @@ import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
+import android.graphics.drawable.Icon;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -69,27 +72,38 @@ import android.service.notification.NotificationListenerService.RankingMap;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
+import android.view.IWindowManager;
+import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewRootImpl;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.window.ScreenCapture;
+import android.window.ScreenCapture.SynchronousScreenCaptureListener;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.launcher3.icons.BubbleIconFactory;
+import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -98,6 +112,8 @@ import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewTransitions;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -118,7 +134,8 @@ import java.util.function.IntConsumer;
*
* The controller manages addition, removal, and visible state of bubbles on screen.
*/
-public class BubbleController implements ConfigurationChangeListener {
+public class BubbleController implements ConfigurationChangeListener,
+ RemoteCallable<BubbleController> {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
@@ -177,6 +194,7 @@ public class BubbleController implements ConfigurationChangeListener {
private final SyncTransactionQueue mSyncQueue;
private final ShellController mShellController;
private final ShellCommandHandler mShellCommandHandler;
+ private final IWindowManager mWmService;
// Used to post to main UI thread
private final ShellExecutor mMainExecutor;
@@ -186,8 +204,8 @@ public class BubbleController implements ConfigurationChangeListener {
private BubbleLogger mLogger;
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
+ @Nullable private BubbleBarLayerView mLayerView;
private BubbleIconFactory mBubbleIconFactory;
- private BubbleBadgeIconFactory mBubbleBadgeIconFactory;
private BubblePositioner mBubblePositioner;
private Bubbles.SysuiProxy mSysuiProxy;
@@ -241,7 +259,12 @@ public class BubbleController implements ConfigurationChangeListener {
/** One handed mode controller to register transition listener. */
private Optional<OneHandedController> mOneHandedOptional;
/** Drag and drop controller to register listener for onDragStarted. */
- private DragAndDropController mDragAndDropController;
+ private Optional<DragAndDropController> mDragAndDropController;
+ /** Used to send bubble events to launcher. */
+ private Bubbles.BubbleStateListener mBubbleStateListener;
+
+ /** Used to send updates to the views from {@link #mBubbleDataListener}. */
+ private BubbleViewCallback mBubbleViewCallback;
public BubbleController(Context context,
ShellInit shellInit,
@@ -262,12 +285,13 @@ public class BubbleController implements ConfigurationChangeListener {
BubblePositioner positioner,
DisplayController displayController,
Optional<OneHandedController> oneHandedOptional,
- DragAndDropController dragAndDropController,
+ Optional<DragAndDropController> dragAndDropController,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellBackgroundThread ShellExecutor bgExecutor,
TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue) {
+ SyncTransactionQueue syncQueue,
+ IWindowManager wmService) {
mContext = context;
mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
@@ -292,13 +316,18 @@ public class BubbleController implements ConfigurationChangeListener {
mBubblePositioner = positioner;
mBubbleData = data;
mSavedUserBubbleData = new SparseArray<>();
- mBubbleIconFactory = new BubbleIconFactory(context);
- mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context);
+ mBubbleIconFactory = new BubbleIconFactory(context,
+ context.getResources().getDimensionPixelSize(R.dimen.bubble_size),
+ context.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
+ context.getResources().getColor(R.color.important_conversation),
+ context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.importance_ring_stroke_width));
mDisplayController = displayController;
mTaskViewTransitions = taskViewTransitions;
mOneHandedOptional = oneHandedOptional;
mDragAndDropController = dragAndDropController;
mSyncQueue = syncQueue;
+ mWmService = wmService;
shellInit.addInitCallback(this::onInit, this);
}
@@ -326,6 +355,9 @@ public class BubbleController implements ConfigurationChangeListener {
}
protected void onInit() {
+ mBubbleViewCallback = isShowingAsBubbleBar()
+ ? mBubbleBarViewCallback
+ : mBubbleStackViewCallback;
mBubbleData.setListener(mBubbleDataListener);
mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
@@ -440,7 +472,7 @@ public class BubbleController implements ConfigurationChangeListener {
});
mOneHandedOptional.ifPresent(this::registerOneHandedState);
- mDragAndDropController.addListener(this::collapseStack);
+ mDragAndDropController.ifPresent(controller -> controller.addListener(this::collapseStack));
// Clear out any persisted bubbles on disk that no longer have a valid user.
List<UserInfo> users = mUserManager.getAliveUsers();
@@ -454,9 +486,15 @@ public class BubbleController implements ConfigurationChangeListener {
mCurrentProfiles = userProfiles;
mShellController.addConfigurationChangeListener(this);
+ mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES,
+ this::createExternalInterface, this);
mShellCommandHandler.addDumpCallback(this::dump, this);
}
+ private ExternalInterfaceBinder createExternalInterface() {
+ return new BubbleController.IBubblesImpl(this);
+ }
+
@VisibleForTesting
public Bubbles asBubbles() {
return mImpl;
@@ -471,6 +509,48 @@ public class BubbleController implements ConfigurationChangeListener {
return mMainExecutor;
}
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ /**
+ * Sets a listener to be notified of bubble updates. This is used by launcher so that
+ * it may render bubbles in itself. Only one listener is supported.
+ */
+ public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) {
+ if (isShowingAsBubbleBar()) {
+ // Only set the listener if bubble bar is showing.
+ mBubbleStateListener = listener;
+ sendInitialListenerUpdate();
+ } else {
+ mBubbleStateListener = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@link Bubbles.BubbleStateListener}.
+ */
+ public void unregisterBubbleStateListener() {
+ mBubbleStateListener = null;
+ }
+
+ /**
+ * If a {@link Bubbles.BubbleStateListener} is present, this will send the current bubble
+ * state to it.
+ */
+ private void sendInitialListenerUpdate() {
+ if (mBubbleStateListener != null) {
+ BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar();
+ mBubbleStateListener.onBubbleStateChange(update);
+ }
+ }
+
/**
* Hides the current input method, wherever it may be focused, via InputMethodManagerInternal.
*/
@@ -483,7 +563,7 @@ public class BubbleController implements ConfigurationChangeListener {
}
private void openBubbleOverflow() {
- ensureStackViewCreated();
+ ensureBubbleViewsAndWindowCreated();
mBubbleData.setShowingOverflow(true);
mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
mBubbleData.setExpanded(true);
@@ -523,7 +603,7 @@ public class BubbleController implements ConfigurationChangeListener {
expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
}
- updateStack();
+ updateBubbleViews();
}
@VisibleForTesting
@@ -619,39 +699,63 @@ public class BubbleController implements ConfigurationChangeListener {
return mBubblePositioner;
}
- Bubbles.SysuiProxy getSysuiProxy() {
+ BubbleIconFactory getIconFactory() {
+ return mBubbleIconFactory;
+ }
+
+ public Bubbles.SysuiProxy getSysuiProxy() {
return mSysuiProxy;
}
/**
- * BubbleStackView is lazily created by this method the first time a Bubble is added. This
- * method initializes the stack view and adds it to window manager.
+ * The view and window for bubbles is lazily created by this method the first time a Bubble
+ * is added. Depending on the device state, this method will:
+ * - initialize a {@link BubbleStackView} and add it to window manager OR
+ * - initialize a {@link com.android.wm.shell.bubbles.bar.BubbleBarLayerView} and adds
+ * it to window manager.
*/
- private void ensureStackViewCreated() {
- if (mStackView == null) {
- mStackView = new BubbleStackView(
- mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
- mMainExecutor);
- mStackView.onOrientationChanged();
- if (mExpandListener != null) {
- mStackView.setExpandListener(mExpandListener);
+ private void ensureBubbleViewsAndWindowCreated() {
+ mBubblePositioner.setShowingInBubbleBar(isShowingAsBubbleBar());
+ if (isShowingAsBubbleBar()) {
+ // When we're showing in launcher / bubble bar is enabled, we don't have bubble stack
+ // view, instead we just show the expanded bubble view as necessary. We still need a
+ // window to show this in, but we use a separate code path.
+ // TODO(b/273312602): consider foldables where we do need a stack view when folded
+ if (mLayerView == null) {
+ mLayerView = new BubbleBarLayerView(mContext, this);
+ }
+ } else {
+ if (mStackView == null) {
+ mStackView = new BubbleStackView(
+ mContext, this, mBubbleData, mSurfaceSynchronizer,
+ mFloatingContentCoordinator,
+ mMainExecutor);
+ mStackView.onOrientationChanged();
+ if (mExpandListener != null) {
+ mStackView.setExpandListener(mExpandListener);
+ }
+ mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
- mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
-
addToWindowManagerMaybe();
}
- /** Adds the BubbleStackView to the WindowManager if it's not already there. */
+ /** Adds the appropriate view to WindowManager if it's not already there. */
private void addToWindowManagerMaybe() {
- // If the stack is null, or already added, don't add it.
- if (mStackView == null || mAddedToWindowManager) {
+ // If already added, don't add it.
+ if (mAddedToWindowManager) {
+ return;
+ }
+ // If the appropriate view is null, don't add it.
+ if (isShowingAsBubbleBar() && mLayerView == null) {
+ return;
+ } else if (!isShowingAsBubbleBar() && mStackView == null) {
return;
}
mWmLayoutParams = new WindowManager.LayoutParams(
// Fill the screen so we can use translation animations to position the bubble
- // stack. We'll use touchable regions to ignore touches that are not on the bubbles
+ // views. We'll use touchable regions to ignore touches that are not on the bubbles
// themselves.
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -674,17 +778,30 @@ public class BubbleController implements ConfigurationChangeListener {
mAddedToWindowManager = true;
registerBroadcastReceiver();
mBubbleData.getOverflow().initialize(this);
- mWindowManager.addView(mStackView, mWmLayoutParams);
- mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
- if (!windowInsets.equals(mWindowInsets)) {
- mWindowInsets = windowInsets;
- mBubblePositioner.update();
- mStackView.onDisplaySizeChanged();
- }
- return windowInsets;
- });
+ // (TODO: b/273314541) some duplication in the inset listener
+ if (isShowingAsBubbleBar()) {
+ mWindowManager.addView(mLayerView, mWmLayoutParams);
+ mLayerView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ if (!windowInsets.equals(mWindowInsets)) {
+ mWindowInsets = windowInsets;
+ mBubblePositioner.update();
+ mLayerView.onDisplaySizeChanged();
+ }
+ return windowInsets;
+ });
+ } else {
+ mWindowManager.addView(mStackView, mWmLayoutParams);
+ mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ if (!windowInsets.equals(mWindowInsets)) {
+ mWindowInsets = windowInsets;
+ mBubblePositioner.update();
+ mStackView.onDisplaySizeChanged();
+ }
+ return windowInsets;
+ });
+ }
} catch (IllegalStateException e) {
- // This means the stack has already been added. This shouldn't happen...
+ // This means the view has already been added. This shouldn't happen...
e.printStackTrace();
}
}
@@ -707,21 +824,31 @@ public class BubbleController implements ConfigurationChangeListener {
}
}
- /** Removes the BubbleStackView from the WindowManager if it's there. */
+ /** Removes any bubble views from the WindowManager that exist. */
private void removeFromWindowManagerMaybe() {
if (!mAddedToWindowManager) {
return;
}
+ mAddedToWindowManager = false;
+ // Put on background for this binder call, was causing jank
+ mBackgroundExecutor.execute(() -> {
+ try {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ } catch (IllegalArgumentException e) {
+ // Not sure if this happens in production, but was happening in tests
+ // (b/253647225)
+ e.printStackTrace();
+ }
+ });
try {
- mAddedToWindowManager = false;
- // Put on background for this binder call, was causing jank
- mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver));
if (mStackView != null) {
mWindowManager.removeView(mStackView);
mBubbleData.getOverflow().cleanUpExpandedState();
- } else {
- Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
+ }
+ if (mLayerView != null) {
+ mWindowManager.removeView(mLayerView);
+ mBubbleData.getOverflow().cleanUpExpandedState();
}
} catch (IllegalArgumentException e) {
// This means the stack has already been removed - it shouldn't happen, but ignore if it
@@ -734,7 +861,7 @@ public class BubbleController implements ConfigurationChangeListener {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
filter.addAction(Intent.ACTION_SCREEN_OFF);
- mContext.registerReceiver(mBroadcastReceiver, filter);
+ mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED);
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -811,18 +938,30 @@ public class BubbleController implements ConfigurationChangeListener {
if (mStackView != null) {
mStackView.onThemeChanged();
}
- mBubbleIconFactory = new BubbleIconFactory(mContext);
- mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext);
+ mBubbleIconFactory = new BubbleIconFactory(mContext,
+ mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size),
+ mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
+ mContext.getResources().getColor(R.color.important_conversation),
+ mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.importance_ring_stroke_width));
// Reload each bubble
for (Bubble b : mBubbleData.getBubbles()) {
- b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
- mBubbleBadgeIconFactory,
+ b.inflate(null /* callback */,
+ mContext,
+ this,
+ mStackView,
+ mLayerView,
+ mBubbleIconFactory,
false /* skipInflation */);
}
for (Bubble b : mBubbleData.getOverflowBubbles()) {
- b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
- mBubbleBadgeIconFactory,
+ b.inflate(null /* callback */,
+ mContext,
+ this,
+ mStackView,
+ mLayerView,
+ mBubbleIconFactory,
false /* skipInflation */);
}
}
@@ -838,8 +977,12 @@ public class BubbleController implements ConfigurationChangeListener {
mDensityDpi = newConfig.densityDpi;
mScreenBounds.set(newConfig.windowConfiguration.getBounds());
mBubbleData.onMaxBubblesChanged();
- mBubbleIconFactory = new BubbleIconFactory(mContext);
- mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext);
+ mBubbleIconFactory = new BubbleIconFactory(mContext,
+ mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size),
+ mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
+ mContext.getResources().getColor(R.color.important_conversation),
+ mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.importance_ring_stroke_width));
mStackView.onDisplaySizeChanged();
}
if (newConfig.fontScale != mFontScale) {
@@ -888,7 +1031,7 @@ public class BubbleController implements ConfigurationChangeListener {
*/
@VisibleForTesting
public boolean hasBubbles() {
- if (mStackView == null) {
+ if (mStackView == null && mLayerView == null) {
return false;
}
return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
@@ -926,7 +1069,27 @@ public class BubbleController implements ConfigurationChangeListener {
* Expands and selects the provided bubble as long as it already exists in the stack or the
* overflow.
*
- * This is currently only used when opening a bubble via clicking on a conversation widget.
+ * This is used by external callers (launcher).
+ */
+ public void expandStackAndSelectBubbleFromLauncher(String key) {
+ Bubble b = mBubbleData.getAnyBubbleWithkey(key);
+ if (b == null) {
+ return;
+ }
+ if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) {
+ // already in the stack
+ mBubbleData.setSelectedBubbleFromLauncher(b);
+ mLayerView.showExpandedView(b);
+ } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) {
+ // TODO: (b/271468319) handle overflow
+ } else {
+ Log.w(TAG, "didn't add bubble from launcher: " + key);
+ }
+ }
+
+ /**
+ * Expands and selects the provided bubble as long as it already exists in the stack or the
+ * overflow. This is currently used when opening a bubble via clicking on a conversation widget.
*/
public void expandStackAndSelectBubble(Bubble b) {
if (b == null) {
@@ -1017,26 +1180,30 @@ public class BubbleController implements ConfigurationChangeListener {
* the bubble or bubble stack.
*
* Some notes:
- * - Only one app bubble is supported at a time
+ * - Only one app bubble is supported at a time, regardless of users. Multi-users support is
+ * tracked in b/273533235.
* - Calling this method with a different intent than the existing app bubble will do nothing
*
* @param intent the intent to display in the bubble expanded view.
+ * @param user the {@link UserHandle} of the user to start this activity for.
+ * @param icon the {@link Icon} to use for the bubble view.
*/
- public void showOrHideAppBubble(Intent intent) {
+ public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
if (intent == null || intent.getPackage() == null) {
Log.w(TAG, "App bubble failed to show, invalid intent: " + intent
+ ((intent != null) ? " with package: " + intent.getPackage() : " "));
return;
}
- PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId);
- if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return;
+ String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
+ PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
+ if (!isResizableActivity(intent, packageManager, appBubbleKey)) return;
- Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE);
+ Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey);
if (existingAppBubble != null) {
BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
if (isStackExpanded()) {
- if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) {
+ if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) {
// App bubble is expanded, lets collapse
collapseStack();
} else {
@@ -1050,13 +1217,45 @@ public class BubbleController implements ConfigurationChangeListener {
}
} else {
// App bubble does not exist, lets add and expand it
- Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
+ Bubble b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
b.setShouldAutoExpand(true);
inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
}
}
/**
+ * Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
+ * can be access via the supplied {@link SynchronousScreenCaptureListener#getBuffer()}
+ * asynchronously.
+ */
+ public void getScreenshotExcludingBubble(int displayId,
+ SynchronousScreenCaptureListener screenCaptureListener) {
+ try {
+ ScreenCapture.CaptureArgs args = null;
+ if (mStackView != null) {
+ ViewRootImpl viewRoot = mStackView.getViewRootImpl();
+ if (viewRoot != null) {
+ SurfaceControl bubbleLayer = viewRoot.getSurfaceControl();
+ if (bubbleLayer != null) {
+ args = new ScreenCapture.CaptureArgs.Builder<>()
+ .setExcludeLayers(new SurfaceControl[] {bubbleLayer})
+ .build();
+ }
+ }
+ }
+
+ mWmService.captureDisplay(displayId, args, screenCaptureListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to capture screenshot");
+ }
+ }
+
+ /** Sets the app bubble's taskId which is cached for SysUI. */
+ public void setAppBubbleTaskId(String key, int taskId) {
+ mImpl.mCachedState.setAppBubbleTaskId(key, taskId);
+ }
+
+ /**
* Fills the overflow bubbles by loading them from disk.
*/
void loadOverflowBubblesFromDisk() {
@@ -1072,7 +1271,11 @@ public class BubbleController implements ConfigurationChangeListener {
}
bubble.inflate(
(b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble),
- mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
+ mContext,
+ this,
+ mStackView,
+ mLayerView,
+ mBubbleIconFactory,
true /* skipInflation */);
});
return null;
@@ -1144,10 +1347,11 @@ public class BubbleController implements ConfigurationChangeListener {
@VisibleForTesting
public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
// Lazy init stack view when a bubble is created
- ensureStackViewCreated();
+ ensureBubbleViewsAndWindowCreated();
bubble.setInflateSynchronously(mInflateSynchronously);
bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
- mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
+ mContext, this, mStackView, mLayerView,
+ mBubbleIconFactory,
false /* skipInflation */);
}
@@ -1312,7 +1516,8 @@ public class BubbleController implements ConfigurationChangeListener {
});
}
- private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() {
+ /** When bubbles are floating, this will be used to notify the floating views. */
+ private final BubbleViewCallback mBubbleStackViewCallback = new BubbleViewCallback() {
@Override
public void removeBubble(Bubble removedBubble) {
if (mStackView != null) {
@@ -1364,6 +1569,62 @@ public class BubbleController implements ConfigurationChangeListener {
}
};
+ /** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */
+ private final BubbleViewCallback mBubbleBarViewCallback = new BubbleViewCallback() {
+ @Override
+ public void removeBubble(Bubble removedBubble) {
+ if (mLayerView != null) {
+ // TODO: need to check if there's something that needs to happen here, e.g. if
+ // the currently selected & expanded bubble is removed?
+ }
+ }
+
+ @Override
+ public void addBubble(Bubble addedBubble) {
+ // Nothing to do for adds, these are handled by launcher / in the bubble bar.
+ }
+
+ @Override
+ public void updateBubble(Bubble updatedBubble) {
+ // Nothing to do for updates, these are handled by launcher / in the bubble bar.
+ }
+
+ @Override
+ public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) {
+ // Nothing to do for order changes, these are handled by launcher / in the bubble bar.
+ }
+
+ @Override
+ public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
+ if (mLayerView != null) {
+ // TODO (b/273316505) handle suppression changes, although might not need to
+ // to do anything on the layerview side for this...
+ }
+ }
+
+ @Override
+ public void expansionChanged(boolean isExpanded) {
+ if (mLayerView != null) {
+ if (!isExpanded) {
+ mLayerView.collapse();
+ } else {
+ BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
+ if (selectedBubble != null) {
+ mLayerView.showExpandedView(selectedBubble);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void selectionChanged(BubbleViewProvider selectedBubble) {
+ // Only need to update the layer view if we're currently expanded for selection changes.
+ if (mLayerView != null && isStackExpanded()) {
+ mLayerView.showExpandedView(selectedBubble);
+ }
+ }
+ };
+
@SuppressWarnings("FieldCanBeLocal")
private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
@@ -1381,7 +1642,7 @@ public class BubbleController implements ConfigurationChangeListener {
+ " unsuppressed=" + (update.unsuppressedBubble != null));
}
- ensureStackViewCreated();
+ ensureBubbleViewsAndWindowCreated();
// Lazy load overflow bubbles from disk
loadOverflowBubblesFromDisk();
@@ -1476,10 +1737,18 @@ public class BubbleController implements ConfigurationChangeListener {
}
mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate");
- updateStack();
+ updateBubbleViews();
// Update the cached state for queries from SysUI
mImpl.mCachedState.update(update);
+
+ if (isShowingAsBubbleBar() && mBubbleStateListener != null) {
+ BubbleBarUpdate bubbleBarUpdate = update.toBubbleBarUpdate();
+ // Some updates aren't relevant to the bubble bar so check first.
+ if (bubbleBarUpdate.anythingChanged()) {
+ mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
+ }
+ }
}
};
@@ -1556,28 +1825,42 @@ public class BubbleController implements ConfigurationChangeListener {
/**
* Updates the visibility of the bubbles based on current state.
- * Does not un-bubble, just hides or un-hides.
- * Updates stack description for TalkBack focus.
- * Updates bubbles' icon views clickable states
+ * Does not un-bubble, just hides or un-hides the views themselves.
+ *
+ * Updates view description for TalkBack focus.
+ * Updates bubbles' icon views clickable states (when floating).
*/
- public void updateStack() {
- if (mStackView == null) {
+ public void updateBubbleViews() {
+ if (mStackView == null && mLayerView == null) {
return;
}
if (!mIsStatusBarShade) {
- // Bubbles don't appear over the locked shade.
- mStackView.setVisibility(INVISIBLE);
+ // Bubbles don't appear when the device is locked.
+ if (mStackView != null) {
+ mStackView.setVisibility(INVISIBLE);
+ }
+ if (mLayerView != null) {
+ mLayerView.setVisibility(INVISIBLE);
+ }
} else if (hasBubbles()) {
// If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the
// stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate
// out.
- mStackView.setVisibility(VISIBLE);
+ if (mStackView != null) {
+ mStackView.setVisibility(VISIBLE);
+ }
+ if (mLayerView != null && isStackExpanded()) {
+ mLayerView.setVisibility(VISIBLE);
+ }
}
- mStackView.updateContentDescription();
-
- mStackView.updateBubblesAcessibillityStates();
+ if (mStackView != null) {
+ mStackView.updateContentDescription();
+ mStackView.updateBubblesAcessibillityStates();
+ } else if (mLayerView != null) {
+ // TODO(b/273313561): handle a11y for BubbleBarLayerView
+ }
}
@VisibleForTesting
@@ -1613,7 +1896,7 @@ public class BubbleController implements ConfigurationChangeListener {
/**
* Whether an intent is properly configured to display in a
- * {@link com.android.wm.shell.TaskView}.
+ * {@link TaskView}.
*
* Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically
* that should filter out any invalid bubbles, but should protect SysUI side just in case.
@@ -1687,6 +1970,76 @@ public class BubbleController implements ConfigurationChangeListener {
}
}
+ /**
+ * The interface for calls from outside the host process.
+ */
+ @BinderThread
+ private class IBubblesImpl extends IBubbles.Stub implements ExternalInterfaceBinder {
+ private BubbleController mController;
+ private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener;
+ private final Bubbles.BubbleStateListener mBubbleListener =
+ new Bubbles.BubbleStateListener() {
+
+ @Override
+ public void onBubbleStateChange(BubbleBarUpdate update) {
+ Bundle b = new Bundle();
+ b.setClassLoader(BubbleBarUpdate.class.getClassLoader());
+ b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update);
+ mListener.call(l -> l.onBubbleStateChange(b));
+ }
+ };
+
+ IBubblesImpl(BubbleController controller) {
+ mController = controller;
+ mListener = new SingleInstanceRemoteListener<>(mController,
+ c -> c.registerBubbleStateListener(mBubbleListener),
+ c -> c.unregisterBubbleStateListener());
+ }
+
+ /**
+ * Invalidates this instance, preventing future calls from updating the controller.
+ */
+ @Override
+ public void invalidate() {
+ mController = null;
+ }
+
+ @Override
+ public void registerBubbleListener(IBubblesListener listener) {
+ mMainExecutor.execute(() -> {
+ mListener.register(listener);
+ });
+ }
+
+ @Override
+ public void unregisterBubbleListener(IBubblesListener listener) {
+ mMainExecutor.execute(() -> mListener.unregister());
+ }
+
+ @Override
+ public void showBubble(String key, boolean onLauncherHome) {
+ mMainExecutor.execute(() -> {
+ mBubblePositioner.setShowingInBubbleBar(onLauncherHome);
+ mController.expandStackAndSelectBubbleFromLauncher(key);
+ });
+ }
+
+ @Override
+ public void removeBubble(String key, int reason) {
+ // TODO (b/271466616) allow removals from launcher
+ }
+
+ @Override
+ public void collapseBubbles() {
+ mMainExecutor.execute(() -> mController.collapseStack());
+ }
+
+ @Override
+ public void onTaskbarStateChanged(int newState) {
+ // TODO (b/269670598)
+ }
+ }
+
private class BubblesImpl implements Bubbles {
// Up-to-date cached state of bubbles data for SysUI to query from the calling thread
@VisibleForTesting
@@ -1697,6 +2050,8 @@ public class BubbleController implements ConfigurationChangeListener {
private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>();
private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>();
+ private HashMap<String, Integer> mAppBubbleTaskIds = new HashMap();
+
private ArrayList<Bubble> mTmpBubbles = new ArrayList<>();
/**
@@ -1727,12 +2082,22 @@ public class BubbleController implements ConfigurationChangeListener {
mSuppressedBubbleKeys.clear();
mShortcutIdToBubble.clear();
+ mAppBubbleTaskIds.clear();
for (Bubble b : mTmpBubbles) {
mShortcutIdToBubble.put(b.getShortcutId(), b);
updateBubbleSuppressedState(b);
+
+ if (b.isAppBubble()) {
+ mAppBubbleTaskIds.put(b.getKey(), b.getTaskId());
+ }
}
}
+ /** Sets the app bubble's taskId which is cached for SysUI. */
+ synchronized void setAppBubbleTaskId(String key, int taskId) {
+ mAppBubbleTaskIds.put(key, taskId);
+ }
+
/**
* Updates a specific bubble suppressed state. This is used mainly because notification
* suppression changes don't go through the same BubbleData update mechanism.
@@ -1782,11 +2147,24 @@ public class BubbleController implements ConfigurationChangeListener {
for (String key : mSuppressedGroupToNotifKeys.keySet()) {
pw.println(" suppressing: " + key);
}
+
+ pw.print("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values());
}
}
private CachedState mCachedState = new CachedState();
+ private IBubblesImpl mIBubbles;
+
+ @Override
+ public IBubbles createExternalInterface() {
+ if (mIBubbles != null) {
+ mIBubbles.invalidate();
+ }
+ mIBubbles = new IBubblesImpl(BubbleController.this);
+ return mIBubbles;
+ }
+
@Override
public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey);
@@ -1825,10 +2203,27 @@ public class BubbleController implements ConfigurationChangeListener {
}
@Override
- public void showOrHideAppBubble(Intent intent) {
- mMainExecutor.execute(() -> {
- BubbleController.this.showOrHideAppBubble(intent);
- });
+ public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
+ mMainExecutor.execute(
+ () -> BubbleController.this.showOrHideAppBubble(intent, user, icon));
+ }
+
+ @Override
+ public boolean isAppBubbleTaskId(int taskId) {
+ return mCachedState.mAppBubbleTaskIds.values().contains(taskId);
+ }
+
+ @Override
+ @Nullable
+ public SynchronousScreenCaptureListener getScreenshotExcludingBubble(int displayId) {
+ SynchronousScreenCaptureListener screenCaptureListener =
+ ScreenCapture.createSyncCaptureListener();
+
+ mMainExecutor.execute(
+ () -> BubbleController.this.getScreenshotExcludingBubble(displayId,
+ screenCaptureListener));
+
+ return screenCaptureListener;
}
@Override
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 3fd09675a245..cc8f50e09fcb 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
@@ -17,7 +17,6 @@ package com.android.wm.shell.bubbles;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -38,8 +37,11 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.Bubbles.DismissReason;
+import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
+import com.android.wm.shell.common.bubbles.RemovedBubble;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -113,6 +115,61 @@ public class BubbleData {
void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
removedBubbles.add(new Pair<>(bubbleToRemove, reason));
}
+
+ /**
+ * Converts the update to a {@link BubbleBarUpdate} which contains updates relevant
+ * to the bubble bar. Only used when {@link BubbleController#isShowingAsBubbleBar()} is
+ * true.
+ */
+ BubbleBarUpdate toBubbleBarUpdate() {
+ BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
+
+ bubbleBarUpdate.expandedChanged = expandedChanged;
+ bubbleBarUpdate.expanded = expanded;
+ if (selectionChanged) {
+ bubbleBarUpdate.selectedBubbleKey = selectedBubble != null
+ ? selectedBubble.getKey()
+ : null;
+ }
+ bubbleBarUpdate.addedBubble = addedBubble != null
+ ? addedBubble.asBubbleBarBubble()
+ : null;
+ // TODO(b/269670235): We need to handle updates better, I think for the bubble bar only
+ // certain updates need to be sent instead of any updatedBubble.
+ bubbleBarUpdate.updatedBubble = updatedBubble != null
+ ? updatedBubble.asBubbleBarBubble()
+ : null;
+ bubbleBarUpdate.suppressedBubbleKey = suppressedBubble != null
+ ? suppressedBubble.getKey()
+ : null;
+ bubbleBarUpdate.unsupressedBubbleKey = unsuppressedBubble != null
+ ? unsuppressedBubble.getKey()
+ : null;
+ for (int i = 0; i < removedBubbles.size(); i++) {
+ Pair<Bubble, Integer> pair = removedBubbles.get(i);
+ bubbleBarUpdate.removedBubbles.add(
+ new RemovedBubble(pair.first.getKey(), pair.second));
+ }
+ if (orderChanged) {
+ // Include the new order
+ for (int i = 0; i < bubbles.size(); i++) {
+ bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey());
+ }
+ }
+ return bubbleBarUpdate;
+ }
+
+ /**
+ * Gets the current state of active bubbles and populates the update with that. Only
+ * used when {@link BubbleController#isShowingAsBubbleBar()} is true.
+ */
+ BubbleBarUpdate getInitialState() {
+ BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
+ for (int i = 0; i < bubbles.size(); i++) {
+ bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble());
+ }
+ return bubbleBarUpdate;
+ }
}
/**
@@ -190,6 +247,13 @@ public class BubbleData {
mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
}
+ /**
+ * Returns a bubble bar update populated with the current list of active bubbles.
+ */
+ public BubbleBarUpdate getInitialStateForBubbleBar() {
+ return mStateChange.getInitialState();
+ }
+
public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) {
mBubbleMetadataFlagListener = listener;
}
@@ -270,6 +334,35 @@ public class BubbleData {
dispatchPendingChanges();
}
+ /**
+ * Sets the selected bubble and expands it, but doesn't dispatch changes
+ * to {@link BubbleData.Listener}. This is used for updates coming from launcher whose views
+ * will already be updated so we don't need to notify them again, but BubbleData should be
+ * updated to have the correct state.
+ */
+ public void setSelectedBubbleFromLauncher(BubbleViewProvider bubble) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "setSelectedBubbleFromLauncher: " + bubble);
+ }
+ mExpanded = true;
+ if (Objects.equals(bubble, mSelectedBubble)) {
+ return;
+ }
+ boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey());
+ if (bubble != null
+ && !mBubbles.contains(bubble)
+ && !mOverflowBubbles.contains(bubble)
+ && !isOverflow) {
+ Log.e(TAG, "Cannot select bubble which doesn't exist!"
+ + " (" + bubble + ") bubbles=" + mBubbles);
+ return;
+ }
+ if (bubble != null && !isOverflow) {
+ ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
+ }
+ mSelectedBubble = bubble;
+ }
+
public void setSelectedBubble(BubbleViewProvider bubble) {
if (DEBUG_BUBBLE_DATA) {
Log.d(TAG, "setSelectedBubble: " + bubble);
@@ -686,7 +779,7 @@ public class BubbleData {
|| !(reason == Bubbles.DISMISS_AGED
|| reason == Bubbles.DISMISS_USER_GESTURE
|| reason == Bubbles.DISMISS_RELOAD_FROM_DISK)
- || KEY_APP_BUBBLE.equals(bubble.getKey())) {
+ || bubble.isAppBubble()) {
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 5ea370b65407..e1a3f3a1ac5d 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.bubbles;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -26,6 +27,7 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPAND
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
@@ -44,6 +46,7 @@ import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.PointF;
+import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.os.RemoteException;
@@ -52,23 +55,25 @@ import android.util.FloatProperty;
import android.util.IntProperty;
import android.util.Log;
import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
-import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
+import android.window.ScreenCapture;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.R;
-import com.android.wm.shell.TaskView;
import com.android.wm.shell.common.AlphaOptimizedButton;
import com.android.wm.shell.common.TriangleShape;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewTaskController;
import java.io.PrintWriter;
@@ -140,6 +145,7 @@ public class BubbleExpandedView extends LinearLayout {
private AlphaOptimizedButton mManageButton;
private TaskView mTaskView;
+ private TaskViewTaskController mTaskViewTaskController;
private BubbleOverflowContainerView mOverflowView;
private int mTaskId = INVALID_TASK_ID;
@@ -211,9 +217,6 @@ public class BubbleExpandedView extends LinearLayout {
ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
0 /* enterResId */, 0 /* exitResId */);
- Rect launchBounds = new Rect();
- mTaskView.getBoundsOnScreen(launchBounds);
-
// TODO: I notice inconsistencies in lifecycle
// Post to keep the lifecycle normal
post(() -> {
@@ -222,8 +225,14 @@ public class BubbleExpandedView extends LinearLayout {
+ getBubbleKey());
}
try {
+ Rect launchBounds = new Rect();
+ mTaskView.getBoundsOnScreen(launchBounds);
+
options.setTaskAlwaysOnTop(true);
options.setLaunchedFromBubble(true);
+ options.setPendingIntentBackgroundActivityStartMode(
+ MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
Intent fillInIntent = new Intent();
// Apply flags to make behaviour match documentLaunchMode=always.
@@ -231,11 +240,19 @@ public class BubbleExpandedView extends LinearLayout {
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
if (mBubble.isAppBubble()) {
- PendingIntent pi = PendingIntent.getActivity(mContext, 0,
- mBubble.getAppBubbleIntent(),
- PendingIntent.FLAG_MUTABLE,
- null);
- mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+ Context context =
+ mContext.createContextAsUser(
+ mBubble.getUser(), Context.CONTEXT_RESTRICTED);
+ PendingIntent pi = PendingIntent.getActivity(
+ context,
+ /* requestCode= */ 0,
+ mBubble.getAppBubbleIntent()
+ .addFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
+ .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
+ /* options= */ null);
+ mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
+ launchBounds);
} else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
options.setApplyActivityFlagsForBubbles(true);
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
@@ -273,6 +290,11 @@ public class BubbleExpandedView extends LinearLayout {
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
+ if (mBubble != null && mBubble.isAppBubble()) {
+ // Let the controller know sooner what the taskId is.
+ mController.setAppBubbleTaskId(mBubble.getKey(), mTaskId);
+ }
+
// With the task org, the taskAppeared callback will only happen once the task has
// already drawn
setContentVisibility(true);
@@ -406,8 +428,10 @@ public class BubbleExpandedView extends LinearLayout {
bringChildToFront(mOverflowView);
mManageButton.setVisibility(GONE);
} else {
- mTaskView = new TaskView(mContext, mController.getTaskOrganizer(),
+ mTaskViewTaskController = new TaskViewTaskController(mContext,
+ mController.getTaskOrganizer(),
mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
+ mTaskView = new TaskView(mContext, mTaskViewTaskController);
mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
mExpandedViewContainer.addView(mTaskView);
bringChildToFront(mTaskView);
@@ -436,7 +460,9 @@ public class BubbleExpandedView extends LinearLayout {
if (mManageButton != null) {
int visibility = mManageButton.getVisibility();
removeView(mManageButton);
- mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate(
+ ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(),
+ com.android.internal.R.style.Theme_DeviceDefault_DayNight);
+ mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate(
R.layout.bubble_manage_button, this /* parent */, false /* attach */);
addView(mManageButton);
mManageButton.setVisibility(visibility);
@@ -457,13 +483,18 @@ public class BubbleExpandedView extends LinearLayout {
void applyThemeAttrs() {
final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
android.R.attr.dialogCornerRadius,
- android.R.attr.colorBackgroundFloating});
+ com.android.internal.R.attr.materialColorSurfaceBright,
+ com.android.internal.R.attr.materialColorSurfaceContainerHigh});
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
mContext.getResources());
mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
mBackgroundColorFloating = ta.getColor(1, Color.WHITE);
mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating);
+ final int manageMenuBg = ta.getColor(2, Color.WHITE);
ta.recycle();
+ if (mManageButton != null) {
+ mManageButton.getBackground().setColorFilter(manageMenuBg, PorterDuff.Mode.SRC_IN);
+ }
if (mTaskView != null) {
mTaskView.setCornerRadius(mCornerRadius);
@@ -516,7 +547,7 @@ public class BubbleExpandedView extends LinearLayout {
/** Return a GraphicBuffer with the contents of the task view surface. */
@Nullable
- SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() {
+ ScreenCapture.ScreenshotHardwareBuffer snapshotActivitySurface() {
if (mIsOverflow) {
// For now, just snapshot the view and return it as a hw buffer so that the animation
// code for both the tasks and overflow can be the same
@@ -525,7 +556,7 @@ public class BubbleExpandedView extends LinearLayout {
p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight()));
p.endRecording();
Bitmap snapshot = Bitmap.createBitmap(p);
- return new SurfaceControl.ScreenshotHardwareBuffer(
+ return new ScreenCapture.ScreenshotHardwareBuffer(
snapshot.getHardwareBuffer(),
snapshot.getColorSpace(),
false /* containsSecureLayers */,
@@ -534,7 +565,7 @@ public class BubbleExpandedView extends LinearLayout {
if (mTaskView == null || mTaskView.getSurfaceControl() == null) {
return null;
}
- return SurfaceControl.captureLayers(
+ return ScreenCapture.captureLayers(
mTaskView.getSurfaceControl(),
new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()),
1 /* scale */);
@@ -1036,13 +1067,21 @@ public class BubbleExpandedView extends LinearLayout {
Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
}
if (getTaskId() != INVALID_TASK_ID) {
- try {
- ActivityTaskManager.getService().removeTask(getTaskId());
- } catch (RemoteException e) {
- Log.w(TAG, e.getMessage());
+ // Ensure the task is removed from WM
+ if (ENABLE_SHELL_TRANSITIONS) {
+ if (mTaskView != null) {
+ mTaskView.removeTask();
+ }
+ } else {
+ try {
+ ActivityTaskManager.getService().removeTask(getTaskId());
+ } catch (RemoteException e) {
+ Log.w(TAG, e.getMessage());
+ }
}
}
if (mTaskView != null) {
+ // Release the surface & other task view related things
mTaskView.release();
removeView(mTaskView);
mTaskView = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
index f878a46d26f2..6a5f785504c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
@@ -177,7 +177,7 @@ public class BubbleFlyoutView extends FrameLayout {
final TypedArray ta = mContext.obtainStyledAttributes(
new int[] {
- com.android.internal.R.attr.colorSurface,
+ com.android.internal.R.attr.materialColorSurfaceContainer,
android.R.attr.dialogCornerRadius});
mFloatingBackgroundColor = ta.getColor(0, Color.WHITE);
mCornerRadius = ta.getDimensionPixelSize(1, 0);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
deleted file mode 100644
index 4ded3ea951e5..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.wm.shell.bubbles;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.icons.BaseIconFactory;
-import com.android.wm.shell.R;
-
-/**
- * Factory for creating normalized bubble icons.
- * We are not using Launcher's IconFactory because bubbles only runs on the UI thread,
- * so there is no need to manage a pool across multiple threads.
- */
-@VisibleForTesting
-public class BubbleIconFactory extends BaseIconFactory {
-
- public BubbleIconFactory(Context context) {
- super(context, context.getResources().getConfiguration().densityDpi,
- context.getResources().getDimensionPixelSize(R.dimen.bubble_size));
- }
-
- /**
- * Returns the drawable that the developer has provided to display in the bubble.
- */
- Drawable getBubbleDrawable(@NonNull final Context context,
- @Nullable final ShortcutInfo shortcutInfo, @Nullable final Icon ic) {
- if (shortcutInfo != null) {
- LauncherApps launcherApps =
- (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
- int density = context.getResources().getConfiguration().densityDpi;
- return launcherApps.getShortcutIconDrawable(shortcutInfo, density);
- } else {
- if (ic != null) {
- if (ic.getType() == Icon.TYPE_URI
- || ic.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
- context.grantUriPermission(context.getPackageName(),
- ic.getUri(),
- Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
- return ic.loadDrawable(context);
- }
- return null;
- }
- }
-
- /**
- * Creates the bitmap for the provided drawable and returns the scale used for
- * drawing the actual drawable.
- */
- public Bitmap createIconBitmap(@NonNull Drawable icon, float[] outScale) {
- if (outScale == null) {
- outScale = new float[1];
- }
- icon = normalizeAndWrapToAdaptiveIcon(icon,
- true /* shrinkNonAdaptiveIcons */,
- null /* outscale */,
- outScale);
- return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
index eb7929b8ca54..df7f5b4f0150 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
@@ -19,21 +19,21 @@ package com.android.wm.shell.bubbles
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.content.Context
import android.graphics.Bitmap
+import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.InsetDrawable
import android.util.PathParser
-import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.FrameLayout
+import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView
-class BubbleOverflow(
- private val context: Context,
- private val positioner: BubblePositioner
-) : BubbleViewProvider {
+class BubbleOverflow(private val context: Context, private val positioner: BubblePositioner) :
+ BubbleViewProvider {
private lateinit var bitmap: Bitmap
private lateinit var dotPath: Path
@@ -52,7 +52,7 @@ class BubbleOverflow(
overflowBtn = null
}
- /** Call before use and again if cleanUpExpandedState was called. */
+ /** Call before use and again if cleanUpExpandedState was called. */
fun initialize(controller: BubbleController) {
createExpandedView()
getExpandedView()?.initialize(controller, controller.stackView, true /* isOverflow */)
@@ -72,10 +72,10 @@ class BubbleOverflow(
}
fun updateResources() {
- overflowIconInset = context.resources.getDimensionPixelSize(
- R.dimen.bubble_overflow_icon_inset)
- overflowBtn?.layoutParams = FrameLayout.LayoutParams(positioner.bubbleSize,
- positioner.bubbleSize)
+ overflowIconInset =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_overflow_icon_inset)
+ overflowBtn?.layoutParams =
+ FrameLayout.LayoutParams(positioner.bubbleSize, positioner.bubbleSize)
expandedView?.updateDimensions()
}
@@ -83,31 +83,58 @@ class BubbleOverflow(
val res = context.resources
// Set overflow button accent color, dot color
- val typedValue = TypedValue()
- context.theme.resolveAttribute(com.android.internal.R.attr.colorAccentPrimary,
- typedValue, true)
- val colorAccent = res.getColor(typedValue.resourceId, null)
- dotColor = colorAccent
- val shapeColor = res.getColor(android.R.color.system_accent1_1000)
+ val typedArray =
+ context.obtainStyledAttributes(
+ intArrayOf(
+ com.android.internal.R.attr.materialColorPrimaryFixed,
+ com.android.internal.R.attr.materialColorOnPrimaryFixed
+ )
+ )
+
+ val colorAccent = typedArray.getColor(0, Color.WHITE)
+ val shapeColor = typedArray.getColor(1, Color.BLACK)
+ typedArray.recycle()
+
+ dotColor = colorAccent
overflowBtn?.iconDrawable?.setTint(shapeColor)
- val iconFactory = BubbleIconFactory(context)
+ val iconFactory =
+ BubbleIconFactory(
+ context,
+ res.getDimensionPixelSize(R.dimen.bubble_size),
+ res.getDimensionPixelSize(R.dimen.bubble_badge_size),
+ res.getColor(R.color.important_conversation),
+ res.getDimensionPixelSize(com.android.internal.R.dimen.importance_ring_stroke_width)
+ )
// Update bitmap
val fg = InsetDrawable(overflowBtn?.iconDrawable, overflowIconInset)
- bitmap = iconFactory.createBadgedIconBitmap(
- AdaptiveIconDrawable(ColorDrawable(colorAccent), fg)).icon
+ bitmap =
+ iconFactory
+ .createBadgedIconBitmap(AdaptiveIconDrawable(ColorDrawable(colorAccent), fg))
+ .icon
// Update dot path
- dotPath = PathParser.createPathFromPathData(
- res.getString(com.android.internal.R.string.config_icon_mask))
- val scale = iconFactory.normalizer.getScale(iconView!!.iconDrawable,
- null /* outBounds */, null /* path */, null /* outMaskShape */)
+ dotPath =
+ PathParser.createPathFromPathData(
+ res.getString(com.android.internal.R.string.config_icon_mask)
+ )
+ val scale =
+ iconFactory.normalizer.getScale(
+ iconView!!.iconDrawable,
+ null /* outBounds */,
+ null /* path */,
+ null /* outMaskShape */
+ )
val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f
val matrix = Matrix()
- matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
- radius /* pivot y */)
+ matrix.setScale(
+ scale /* x scale */,
+ scale /* y scale */,
+ radius /* pivot x */,
+ radius /* pivot y */
+ )
dotPath.transform(matrix)
// Attach BubbleOverflow to BadgedImageView
@@ -125,8 +152,12 @@ class BubbleOverflow(
}
fun createExpandedView(): BubbleExpandedView? {
- expandedView = inflater.inflate(R.layout.bubble_expanded_view,
- null /* root */, false /* attachToRoot */) as BubbleExpandedView
+ expandedView =
+ inflater.inflate(
+ R.layout.bubble_expanded_view,
+ null /* root */,
+ false /* attachToRoot */
+ ) as BubbleExpandedView
expandedView?.applyThemeAttrs()
updateResources()
return expandedView
@@ -136,6 +167,10 @@ class BubbleOverflow(
return expandedView
}
+ override fun getBubbleBarExpandedView(): BubbleBarExpandedView? {
+ return null
+ }
+
override fun getDotColor(): Int {
return dotColor
}
@@ -166,11 +201,15 @@ class BubbleOverflow(
override fun getIconView(): BadgedImageView? {
if (overflowBtn == null) {
- overflowBtn = inflater.inflate(R.layout.bubble_overflow_button,
- null /* root */, false /* attachToRoot */) as BadgedImageView
+ overflowBtn =
+ inflater.inflate(
+ R.layout.bubble_overflow_button,
+ null /* root */,
+ false /* attachToRoot */
+ ) as BadgedImageView
overflowBtn?.initialize(positioner)
- overflowBtn?.contentDescription = context.resources.getString(
- R.string.bubble_overflow_button_content_description)
+ overflowBtn?.contentDescription =
+ context.resources.getString(R.string.bubble_overflow_button_content_description)
val bubbleSize = positioner.bubbleSize
overflowBtn?.layoutParams = FrameLayout.LayoutParams(bubbleSize, bubbleSize)
updateBtnTheme()
@@ -189,4 +228,4 @@ class BubbleOverflow(
companion object {
const val KEY = "Overflow"
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
index 9aa285fff19c..9655470ce914 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
@@ -220,8 +220,8 @@ public class BubbleOverflowContainerView extends LinearLayout {
: res.getColor(R.color.bubbles_light));
final TypedArray typedArray = getContext().obtainStyledAttributes(new int[] {
- android.R.attr.colorBackgroundFloating,
- android.R.attr.textColorSecondary});
+ com.android.internal.R.attr.materialColorSurfaceBright,
+ com.android.internal.R.attr.materialColorOnSurface});
int bgColor = typedArray.getColor(0, isNightMode ? Color.BLACK : Color.WHITE);
int textColor = typedArray.getColor(1, isNightMode ? Color.WHITE : Color.BLACK);
textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, isNightMode);
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 5ea2450114f0..d101b0c4d7e8 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
@@ -53,12 +53,16 @@ public class BubblePositioner {
public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f;
/** The max percent of screen width to use for the flyout on phone. */
public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f;
- /** The percent of screen width that should be used for the expanded view on a large screen. **/
+ /** The percent of screen width for the expanded view on a large screen. **/
private static final float EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT = 0.48f;
- /** The percent of screen width that should be used for the expanded view on a large screen. **/
+ /** The percent of screen width for the expanded view on a large screen. **/
private static final float EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT = 0.70f;
- /** The percent of screen width that should be used for the expanded view on a small tablet. **/
+ /** The percent of screen width for the expanded view on a small tablet. **/
private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f;
+ /** The percent of screen width for the expanded view when shown in the bubble bar. **/
+ private static final float EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT = 0.7f;
+ /** The percent of screen width for the expanded view when shown in the bubble bar. **/
+ private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f;
private Context mContext;
private WindowManager mWindowManager;
@@ -97,6 +101,12 @@ public class BubblePositioner {
private PointF mRestingStackPosition;
private int[] mPaddings = new int[4];
+ private boolean mShowingInBubbleBar;
+ private boolean mBubblesOnHome;
+ private int mBubbleBarSize;
+ private int mBubbleBarHomeAdjustment;
+ private final PointF mBubbleBarPosition = new PointF();
+
public BubblePositioner(Context context, WindowManager windowManager) {
mContext = context;
mWindowManager = windowManager;
@@ -133,6 +143,7 @@ public class BubblePositioner {
+ " insets: " + insets
+ " isLargeScreen: " + mIsLargeScreen
+ " isSmallTablet: " + mIsSmallTablet
+ + " showingInBubbleBar: " + mShowingInBubbleBar
+ " bounds: " + bounds);
}
updateInternal(mRotation, insets, bounds);
@@ -155,11 +166,17 @@ public class BubblePositioner {
mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
+ mBubbleBarHomeAdjustment = mExpandedViewPadding / 2;
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+ mBubbleBarSize = res.getDimensionPixelSize(R.dimen.bubblebar_size);
- if (mIsSmallTablet) {
+ if (mShowingInBubbleBar) {
+ 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) {
mExpandedViewLargeScreenWidth = (int) (bounds.width()
* EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT);
} else {
@@ -693,4 +710,65 @@ public class BubblePositioner {
screen.right,
screen.bottom);
}
+
+ //
+ // Bubble bar specific sizes below.
+ //
+
+ /**
+ * Sets whether bubbles are showing in the bubble bar from launcher.
+ */
+ public void setShowingInBubbleBar(boolean showingInBubbleBar) {
+ mShowingInBubbleBar = showingInBubbleBar;
+ }
+
+ /**
+ * Sets whether bubbles are showing on launcher home, in which case positions are different.
+ */
+ public void setBubblesOnHome(boolean bubblesOnHome) {
+ mBubblesOnHome = bubblesOnHome;
+ }
+
+ /**
+ * How wide the expanded view should be when showing from the bubble bar.
+ */
+ public int getExpandedViewWidthForBubbleBar() {
+ return mExpandedViewLargeScreenWidth;
+ }
+
+ /**
+ * How tall the expanded view should be when showing from the bubble bar.
+ */
+ public int getExpandedViewHeightForBubbleBar() {
+ return getAvailableRect().height()
+ - mBubbleBarSize
+ - mExpandedViewPadding * 2
+ - getBubbleBarHomeAdjustment();
+ }
+
+ /**
+ * The amount of padding from the edge of the screen to the expanded view when in bubble bar.
+ */
+ public int getBubbleBarExpandedViewPadding() {
+ return mExpandedViewPadding;
+ }
+
+ /**
+ * Returns the on screen co-ordinates of the bubble bar.
+ */
+ public PointF getBubbleBarPosition() {
+ mBubbleBarPosition.set(getAvailableRect().width() - mBubbleBarSize,
+ getAvailableRect().height() - mBubbleBarSize
+ - mExpandedViewPadding - getBubbleBarHomeAdjustment());
+ return mBubbleBarPosition;
+ }
+
+ /**
+ * When bubbles are shown on launcher home, there's an extra bit of padding that needs to
+ * be applied between the expanded view and the bubble bar. This returns the adjustment value
+ * if bubbles are showing on home.
+ */
+ private int getBubbleBarHomeAdjustment() {
+ return mBubblesOnHome ? mBubbleBarHomeAdjustment : 0;
+ }
}
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 5ecbd6b596b6..68fea41e134e 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
@@ -33,14 +33,15 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
-import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.PointF;
+import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
@@ -51,7 +52,6 @@ import android.util.Log;
import android.view.Choreographer;
import android.view.LayoutInflater;
import android.view.MotionEvent;
-import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
@@ -64,6 +64,7 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
+import android.window.ScreenCapture;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -238,7 +239,7 @@ public class BubbleStackView extends FrameLayout
* Buffer containing a screenshot of the animating-out bubble. This is drawn into the
* SurfaceView during animations.
*/
- private SurfaceControl.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer;
+ private ScreenCapture.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer;
private BubbleFlyoutView mFlyout;
/** Runnable that fades out the flyout and then sets it to GONE. */
@@ -283,6 +284,11 @@ public class BubbleStackView extends FrameLayout
/** Whether the expanded view has been hidden, because we are dragging out a bubble. */
private boolean mExpandedViewTemporarilyHidden = false;
+ /**
+ * Whether the last bubble is being removed when expanded, which impacts the collapse animation.
+ */
+ private boolean mRemovingLastBubbleWhileExpanded = false;
+
/** Animator for animating the expanded view's alpha (including the TaskView inside it). */
private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f);
@@ -575,7 +581,7 @@ public class BubbleStackView extends FrameLayout
if (maybeShowStackEdu()) {
mShowedUserEducationInTouchListenerActive = true;
return true;
- } else if (isStackEduShowing()) {
+ } else if (isStackEduVisible()) {
mStackEduView.hide(false /* fromExpansion */);
}
@@ -651,7 +657,7 @@ public class BubbleStackView extends FrameLayout
mExpandedAnimationController.dragBubbleOut(
v, viewInitialX + dx, viewInitialY + dy);
} else {
- if (isStackEduShowing()) {
+ if (isStackEduVisible()) {
mStackEduView.hide(false /* fromExpansion */);
}
mStackAnimationController.moveStackFromTouch(
@@ -733,8 +739,7 @@ public class BubbleStackView extends FrameLayout
@Override
public void onMove(float dx, float dy) {
- if ((mManageEduView != null && mManageEduView.getVisibility() == VISIBLE)
- || isStackEduShowing()) {
+ if (isManageEduVisible() || isStackEduVisible()) {
return;
}
@@ -767,7 +772,7 @@ public class BubbleStackView extends FrameLayout
// Update scrim
if (!mScrimAnimating) {
- showScrim(true);
+ showScrim(true, null /* runnable */);
}
}
}
@@ -844,6 +849,8 @@ public class BubbleStackView extends FrameLayout
private DismissView mDismissView;
private ViewGroup mManageMenu;
+ private ViewGroup mManageDontBubbleView;
+ private ViewGroup mManageSettingsView;
private ImageView mManageSettingsIcon;
private TextView mManageSettingsText;
private boolean mShowingManage = false;
@@ -880,6 +887,7 @@ public class BubbleStackView extends FrameLayout
final Runnable onBubbleAnimatedOut = () -> {
if (getBubbleCount() == 0) {
+ mExpandedViewTemporarilyHidden = false;
mBubbleController.onAllBubblesAnimatedOut();
}
};
@@ -918,7 +926,6 @@ public class BubbleStackView extends FrameLayout
addView(mAnimatingOutSurfaceContainer);
mAnimatingOutSurfaceView = new SurfaceView(getContext());
- mAnimatingOutSurfaceView.setUseAlpha();
mAnimatingOutSurfaceView.setZOrderOnTop(true);
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
mContext.getResources());
@@ -995,7 +1002,7 @@ public class BubbleStackView extends FrameLayout
mStackAnimationController.updateResources();
mBubbleOverflow.updateResources();
- if (!isStackEduShowing() && mRelativeStackPositionBeforeRotation != null) {
+ if (!isStackEduVisible() && mRelativeStackPositionBeforeRotation != null) {
mStackAnimationController.setStackPosition(
mRelativeStackPositionBeforeRotation);
mRelativeStackPositionBeforeRotation = null;
@@ -1008,6 +1015,7 @@ public class BubbleStackView extends FrameLayout
updatePointerPosition(false /* forIme */);
mExpandedAnimationController.expandFromStack(() -> {
afterExpandedViewAnimation();
+ mExpandedViewContainer.setVisibility(VISIBLE);
showManageMenu(mShowingManage);
} /* after */);
PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
@@ -1045,9 +1053,9 @@ public class BubbleStackView extends FrameLayout
setOnClickListener(view -> {
if (mShowingManage) {
showManageMenu(false /* show */);
- } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ } else if (isManageEduVisible()) {
mManageEduView.hide();
- } else if (isStackEduShowing()) {
+ } else if (isStackEduVisible()) {
mStackEduView.hide(false /* isExpanding */);
} else if (mBubbleData.isExpanded()) {
mBubbleData.setExpanded(false);
@@ -1069,6 +1077,7 @@ public class BubbleStackView extends FrameLayout
// We need to be Z ordered on top in order for alpha animations to work.
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
mExpandedBubble.getExpandedView().setAnimating(true);
+ mExpandedViewContainer.setVisibility(VISIBLE);
}
}
@@ -1196,6 +1205,12 @@ public class BubbleStackView extends FrameLayout
R.layout.bubble_manage_menu, this, false);
mManageMenu.setVisibility(View.INVISIBLE);
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
+ com.android.internal.R.attr.materialColorSurfaceBright});
+ final int menuBackgroundColor = ta.getColor(0, Color.WHITE);
+ ta.recycle();
+ mManageMenu.getBackground().setColorFilter(menuBackgroundColor, PorterDuff.Mode.SRC_IN);
+
PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig);
mManageMenu.setOutlineProvider(new ViewOutlineProvider() {
@@ -1218,7 +1233,11 @@ public class BubbleStackView extends FrameLayout
mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey());
});
- mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener(
+ mManageDontBubbleView = mManageMenu
+ .findViewById(R.id.bubble_manage_menu_dont_bubble_container);
+
+ mManageSettingsView = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container);
+ mManageSettingsView.setOnClickListener(
view -> {
showManageMenu(false /* show */);
final BubbleViewProvider bubble = mBubbleData.getSelectedBubble();
@@ -1242,10 +1261,19 @@ public class BubbleStackView extends FrameLayout
}
/**
+ * Whether the selected bubble is conversation bubble
+ */
+ private boolean isConversationBubble() {
+ BubbleViewProvider bubble = mBubbleData.getSelectedBubble();
+ return bubble instanceof Bubble && ((Bubble) bubble).isConversation();
+ }
+
+ /**
* Whether the educational view should show for the expanded view "manage" menu.
*/
private boolean shouldShowManageEdu() {
- if (ActivityManager.isRunningInTestHarness()) {
+ if (!isConversationBubble()) {
+ // We only show user education for conversation bubbles right now
return false;
}
final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION);
@@ -1268,11 +1296,17 @@ public class BubbleStackView extends FrameLayout
mManageEduView.show(mExpandedBubble.getExpandedView());
}
+ @VisibleForTesting
+ public boolean isManageEduVisible() {
+ return mManageEduView != null && mManageEduView.getVisibility() == VISIBLE;
+ }
+
/**
* Whether education view should show for the collapsed stack.
*/
private boolean shouldShowStackEdu() {
- if (ActivityManager.isRunningInTestHarness()) {
+ if (!isConversationBubble()) {
+ // We only show user education for conversation bubbles right now
return false;
}
final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION);
@@ -1305,13 +1339,14 @@ public class BubbleStackView extends FrameLayout
return mStackEduView.show(mPositioner.getDefaultStartPosition());
}
- private boolean isStackEduShowing() {
+ @VisibleForTesting
+ public boolean isStackEduVisible() {
return mStackEduView != null && mStackEduView.getVisibility() == VISIBLE;
}
// Recreates & shows the education views. Call when a theme/config change happens.
private void updateUserEdu() {
- if (isStackEduShowing()) {
+ if (isStackEduVisible() && !mStackEduView.isHiding()) {
removeView(mStackEduView);
mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
addView(mStackEduView);
@@ -1320,7 +1355,7 @@ public class BubbleStackView extends FrameLayout
mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition());
mStackEduView.show(mPositioner.getDefaultStartPosition());
}
- if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ if (isManageEduVisible()) {
removeView(mManageEduView);
mManageEduView = new ManageEducationView(mContext, mPositioner);
addView(mManageEduView);
@@ -1424,7 +1459,7 @@ public class BubbleStackView extends FrameLayout
mStackAnimationController.updateResources();
mDismissView.updateResources();
mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2);
- if (!isStackEduShowing()) {
+ if (!isStackEduVisible()) {
mStackAnimationController.setStackPosition(
new RelativeStackPosition(
mPositioner.getRestingPosition(),
@@ -1752,6 +1787,24 @@ public class BubbleStackView extends FrameLayout
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "removeBubble: " + bubble);
}
+ if (isExpanded() && getBubbleCount() == 1) {
+ mRemovingLastBubbleWhileExpanded = true;
+ // We're expanded while the last bubble is being removed. Let the scrim animate away
+ // and then remove our views (removing the icon view triggers the removal of the
+ // bubble window so do that at the end of the animation so we see the scrim animate).
+ BadgedImageView iconView = bubble.getIconView();
+ showScrim(false, () -> {
+ mRemovingLastBubbleWhileExpanded = false;
+ bubble.cleanupExpandedView();
+ if (iconView != null) {
+ mBubbleContainer.removeView(iconView);
+ }
+ bubble.cleanupViews(); // cleans up the icon view
+ updateExpandedView(); // resets state for no expanded bubble
+ });
+ logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
+ return;
+ }
// Remove it from the views
for (int i = 0; i < getBubbleCount(); i++) {
View v = mBubbleContainer.getChildAt(i);
@@ -1775,6 +1828,7 @@ public class BubbleStackView extends FrameLayout
// If a bubble is suppressed, it is not attached to the container. Clean it up.
if (bubble.isSuppressed()) {
bubble.cleanupViews();
+ logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
} else {
Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
}
@@ -2008,7 +2062,7 @@ public class BubbleStackView extends FrameLayout
if (mIsExpanded) {
if (mShowingManage) {
showManageMenu(false);
- } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ } else if (isManageEduVisible()) {
mManageEduView.hide();
} else {
mBubbleData.setExpanded(false);
@@ -2122,7 +2176,7 @@ public class BubbleStackView extends FrameLayout
mExpandedViewAlphaAnimator.start();
}
- private void showScrim(boolean show) {
+ private void showScrim(boolean show, Runnable after) {
AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -2132,6 +2186,9 @@ public class BubbleStackView extends FrameLayout
@Override
public void onAnimationEnd(Animator animation) {
mScrimAnimating = false;
+ if (after != null) {
+ after.run();
+ }
}
};
if (show) {
@@ -2153,12 +2210,12 @@ public class BubbleStackView extends FrameLayout
cancelDelayedExpandCollapseSwitchAnimations();
final boolean showVertically = mPositioner.showBubblesVertically();
mIsExpanded = true;
- if (isStackEduShowing()) {
+ if (isStackEduVisible()) {
mStackEduView.hide(true /* fromExpansion */);
}
beforeExpandedViewAnimation();
- showScrim(true);
+ showScrim(true, null /* runnable */);
updateZOrder();
updateBadges(false /* setBadgeForCollapsedStack */);
mBubbleContainer.setActiveController(mExpandedAnimationController);
@@ -2275,14 +2332,17 @@ public class BubbleStackView extends FrameLayout
private void animateCollapse() {
cancelDelayedExpandCollapseSwitchAnimations();
- if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
+ if (isManageEduVisible()) {
mManageEduView.hide();
}
mIsExpanded = false;
mIsExpansionAnimating = true;
- showScrim(false);
+ if (!mRemovingLastBubbleWhileExpanded) {
+ // When we remove the last bubble it animates the scrim.
+ showScrim(false, null /* runnable */);
+ }
mBubbleContainer.cancelAllAnimations();
@@ -2458,6 +2518,7 @@ public class BubbleStackView extends FrameLayout
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition(false /* forIme */);
afterExpandedViewAnimation();
+ mExpandedViewContainer.setVisibility(VISIBLE);
mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
} /* after */);
return;
@@ -2672,7 +2733,7 @@ public class BubbleStackView extends FrameLayout
if (flyoutMessage == null
|| flyoutMessage.message == null
|| !bubble.showFlyout()
- || isStackEduShowing()
+ || isStackEduVisible()
|| isExpanded()
|| mIsExpansionAnimating
|| mIsGestureInProgress
@@ -2795,7 +2856,7 @@ public class BubbleStackView extends FrameLayout
* them.
*/
public void getTouchableRegion(Rect outRect) {
- if (isStackEduShowing()) {
+ if (isStackEduVisible()) {
// When user education shows then capture all touches
outRect.set(0, 0, getWidth(), getHeight());
return;
@@ -2837,6 +2898,7 @@ public class BubbleStackView extends FrameLayout
/** Hide or show the manage menu for the currently expanded bubble. */
@VisibleForTesting
public void showManageMenu(boolean show) {
+ if ((mManageMenu.getVisibility() == VISIBLE) == show) return;
mShowingManage = show;
// This should not happen, since the manage menu is only visible when there's an expanded
@@ -2869,10 +2931,21 @@ public class BubbleStackView extends FrameLayout
// name and icon.
if (show) {
final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey());
- if (bubble != null) {
+ if (bubble != null && !bubble.isAppBubble()) {
+ // Setup options for non app bubbles
+ mManageDontBubbleView.setVisibility(VISIBLE);
mManageSettingsIcon.setImageBitmap(bubble.getRawAppBadge());
mManageSettingsText.setText(getResources().getString(
R.string.bubbles_app_settings, bubble.getAppName()));
+ mManageSettingsView.setVisibility(VISIBLE);
+ } else {
+ // Setup options for app bubbles
+ // App bubbles have no conversations
+ // so we don't show the option to not bubble conversation
+ mManageDontBubbleView.setVisibility(GONE);
+ // App bubbles are not notification based
+ // so we don't show the option to go to notification settings
+ mManageSettingsView.setVisibility(GONE);
}
}
@@ -2892,14 +2965,15 @@ public class BubbleStackView extends FrameLayout
final float targetX = isLtr
? mTempRect.left - margin
: mTempRect.right + margin - mManageMenu.getWidth();
- final float targetY = mTempRect.bottom - mManageMenu.getHeight();
+ final float menuHeight = getVisibleManageMenuHeight();
+ final float targetY = mTempRect.bottom - menuHeight;
final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f;
if (show) {
mManageMenu.setScaleX(0.5f);
mManageMenu.setScaleY(0.5f);
mManageMenu.setTranslationX(targetX - xOffsetForAnimation);
- mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4f);
+ mManageMenu.setTranslationY(targetY + menuHeight / 4f);
mManageMenu.setAlpha(0f);
PhysicsAnimator.getInstance(mManageMenu)
@@ -2925,7 +2999,7 @@ public class BubbleStackView extends FrameLayout
.spring(DynamicAnimation.SCALE_X, 0.5f)
.spring(DynamicAnimation.SCALE_Y, 0.5f)
.spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation)
- .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f)
+ .spring(DynamicAnimation.TRANSLATION_Y, targetY + menuHeight / 4f)
.withEndActions(() -> {
mManageMenu.setVisibility(View.INVISIBLE);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
@@ -2937,6 +3011,24 @@ public class BubbleStackView extends FrameLayout
}
}
+ /**
+ * Checks whether manage menu don't bubble conversation action is available and visible
+ * Used for testing
+ */
+ @VisibleForTesting
+ public boolean isManageMenuDontBubbleVisible() {
+ return mManageDontBubbleView != null && mManageDontBubbleView.getVisibility() == VISIBLE;
+ }
+
+ /**
+ * Checks whether manage menu notification settings action is available and visible
+ * Used for testing
+ */
+ @VisibleForTesting
+ public boolean isManageMenuSettingsVisible() {
+ return mManageSettingsView != null && mManageSettingsView.getVisibility() == VISIBLE;
+ }
+
private void updateExpandedBubble() {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "updateExpandedBubble()");
@@ -3067,7 +3159,7 @@ public class BubbleStackView extends FrameLayout
mAnimatingOutBubbleBuffer.getColorSpace());
mAnimatingOutSurfaceView.setAlpha(1f);
- mExpandedViewContainer.setVisibility(View.GONE);
+ mExpandedViewContainer.setVisibility(View.INVISIBLE);
mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
post(() -> {
@@ -3097,9 +3189,6 @@ public class BubbleStackView extends FrameLayout
int[] paddings = mPositioner.getExpandedViewContainerPadding(
mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded);
mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
- if (mIsExpansionAnimating) {
- mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
- }
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
getState());
@@ -3224,10 +3313,30 @@ public class BubbleStackView extends FrameLayout
}
/**
+ * Menu height calculated for animation
+ * It takes into account view visibility to get the correct total height
+ */
+ private float getVisibleManageMenuHeight() {
+ float menuHeight = 0;
+
+ for (int i = 0; i < mManageMenu.getChildCount(); i++) {
+ View subview = mManageMenu.getChildAt(i);
+
+ if (subview.getVisibility() == VISIBLE) {
+ menuHeight += subview.getHeight();
+ }
+ }
+
+ return menuHeight;
+ }
+
+ /**
* @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
*/
public float getNormalizedXPosition() {
- return new BigDecimal(getStackPosition().x / mPositioner.getAvailableRect().width())
+ int width = mPositioner.getAvailableRect().width();
+ float stackPosition = width > 0 ? getStackPosition().x / width : 0;
+ return new BigDecimal(stackPosition)
.setScale(4, RoundingMode.CEILING.HALF_UP)
.floatValue();
}
@@ -3236,7 +3345,9 @@ public class BubbleStackView extends FrameLayout
* @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
*/
public float getNormalizedYPosition() {
- return new BigDecimal(getStackPosition().y / mPositioner.getAvailableRect().height())
+ int height = mPositioner.getAvailableRect().height();
+ float stackPosition = height > 0 ? getStackPosition().y / height : 0;
+ return new BigDecimal(stackPosition)
.setScale(4, RoundingMode.CEILING.HALF_UP)
.floatValue();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
new file mode 100644
index 000000000000..7a5815994dd0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -0,0 +1,282 @@
+/*
+ * 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 static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
+
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewTaskController;
+
+/**
+ * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}.
+ */
+public class BubbleTaskViewHelper {
+
+ private static final String TAG = BubbleTaskViewHelper.class.getSimpleName();
+
+ /**
+ * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events
+ * on the task.
+ */
+ public interface Listener {
+
+ /** Called when the task is first created. */
+ void onTaskCreated();
+
+ /** Called when the visibility of the task changes. */
+ void onContentVisibilityChanged(boolean visible);
+
+ /** Called when back is pressed on the task root. */
+ void onBackPressed();
+ }
+
+ private final Context mContext;
+ private final BubbleController mController;
+ private final @ShellMainThread ShellExecutor mMainExecutor;
+ private final BubbleTaskViewHelper.Listener mListener;
+ private final View mParentView;
+
+ @Nullable
+ private Bubble mBubble;
+ @Nullable
+ private PendingIntent mPendingIntent;
+ private TaskViewTaskController mTaskViewTaskController;
+ @Nullable
+ private TaskView mTaskView;
+ private int mTaskId = INVALID_TASK_ID;
+
+ private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
+ private boolean mInitialized = false;
+ private boolean mDestroyed = false;
+
+ @Override
+ public void onInitialized() {
+ if (DEBUG_BUBBLE_EXPANDED_VIEW) {
+ Log.d(TAG, "onInitialized: destroyed=" + mDestroyed
+ + " initialized=" + mInitialized
+ + " bubble=" + getBubbleKey());
+ }
+
+ if (mDestroyed || mInitialized) {
+ return;
+ }
+
+ // Custom options so there is no activity transition animation
+ ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext,
+ 0 /* enterResId */, 0 /* exitResId */);
+
+ Rect launchBounds = new Rect();
+ mTaskView.getBoundsOnScreen(launchBounds);
+
+ // TODO: I notice inconsistencies in lifecycle
+ // Post to keep the lifecycle normal
+ mParentView.post(() -> {
+ if (DEBUG_BUBBLE_EXPANDED_VIEW) {
+ Log.d(TAG, "onInitialized: calling startActivity, bubble="
+ + getBubbleKey());
+ }
+ try {
+ options.setTaskAlwaysOnTop(true);
+ options.setLaunchedFromBubble(true);
+
+ Intent fillInIntent = new Intent();
+ // Apply flags to make behaviour match documentLaunchMode=always.
+ fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+ if (mBubble.isAppBubble()) {
+ PendingIntent pi = PendingIntent.getActivity(mContext, 0,
+ mBubble.getAppBubbleIntent(),
+ PendingIntent.FLAG_MUTABLE,
+ null);
+ mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+ } else if (mBubble.hasMetadataShortcutId()) {
+ options.setApplyActivityFlagsForBubbles(true);
+ mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
+ options, launchBounds);
+ } else {
+ if (mBubble != null) {
+ mBubble.setIntentActive();
+ }
+ mTaskView.startActivity(mPendingIntent, fillInIntent, options,
+ launchBounds);
+ }
+ } catch (RuntimeException e) {
+ // If there's a runtime exception here then there's something
+ // wrong with the intent, we can't really recover / try to populate
+ // the bubble again so we'll just remove it.
+ Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
+ + ", " + e.getMessage() + "; removing bubble");
+ mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
+ }
+ mInitialized = true;
+ });
+ }
+
+ @Override
+ public void onReleased() {
+ mDestroyed = true;
+ }
+
+ @Override
+ public void onTaskCreated(int taskId, ComponentName name) {
+ if (DEBUG_BUBBLE_EXPANDED_VIEW) {
+ Log.d(TAG, "onTaskCreated: taskId=" + taskId
+ + " bubble=" + getBubbleKey());
+ }
+ // The taskId is saved to use for removeTask, preventing appearance in recent tasks.
+ mTaskId = taskId;
+
+ // With the task org, the taskAppeared callback will only happen once the task has
+ // already drawn
+ mListener.onTaskCreated();
+ }
+
+ @Override
+ public void onTaskVisibilityChanged(int taskId, boolean visible) {
+ mListener.onContentVisibilityChanged(visible);
+ }
+
+ @Override
+ public void onTaskRemovalStarted(int taskId) {
+ if (DEBUG_BUBBLE_EXPANDED_VIEW) {
+ Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
+ + " bubble=" + getBubbleKey());
+ }
+ if (mBubble != null) {
+ mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+ }
+ }
+
+ @Override
+ public void onBackPressedOnTaskRoot(int taskId) {
+ if (mTaskId == taskId && mController.isStackExpanded()) {
+ mListener.onBackPressed();
+ }
+ }
+ };
+
+ public BubbleTaskViewHelper(Context context,
+ BubbleController controller,
+ BubbleTaskViewHelper.Listener listener,
+ View parent) {
+ mContext = context;
+ mController = controller;
+ mMainExecutor = mController.getMainExecutor();
+ mListener = listener;
+ mParentView = parent;
+ mTaskViewTaskController = new TaskViewTaskController(mContext,
+ mController.getTaskOrganizer(),
+ mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
+ mTaskView = new TaskView(mContext, mTaskViewTaskController);
+ mTaskView.setListener(mMainExecutor, mTaskViewListener);
+ }
+
+ /**
+ * Sets the bubble or updates the bubble used to populate the view.
+ *
+ * @return true if the bubble is new, false if it was an update to the same bubble.
+ */
+ public boolean update(Bubble bubble) {
+ boolean isNew = mBubble == null || didBackingContentChange(bubble);
+ mBubble = bubble;
+ if (isNew) {
+ mPendingIntent = mBubble.getBubbleIntent();
+ return true;
+ }
+ return false;
+ }
+
+ /** Cleans up anything related to the task and {@code TaskView}. */
+ public void cleanUpTaskView() {
+ if (DEBUG_BUBBLE_EXPANDED_VIEW) {
+ Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
+ }
+ if (mTaskId != INVALID_TASK_ID) {
+ try {
+ ActivityTaskManager.getService().removeTask(mTaskId);
+ } catch (RemoteException e) {
+ Log.w(TAG, e.getMessage());
+ }
+ }
+ if (mTaskView != null) {
+ mTaskView.release();
+ mTaskView = null;
+ }
+ }
+
+ /** Returns the bubble key associated with this view. */
+ @Nullable
+ public String getBubbleKey() {
+ return mBubble != null ? mBubble.getKey() : null;
+ }
+
+ /** Returns the TaskView associated with this view. */
+ @Nullable
+ public TaskView getTaskView() {
+ return mTaskView;
+ }
+
+ /**
+ * Returns the task id associated with the task in this view. If the task doesn't exist then
+ * {@link ActivityTaskManager#INVALID_TASK_ID}.
+ */
+ public int getTaskId() {
+ return mTaskId;
+ }
+
+ /** Returns whether the bubble set on the helper is valid to populate the task view. */
+ public boolean isValidBubble() {
+ return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId());
+ }
+
+ // TODO (b/274980695): Is this still relevant?
+ /**
+ * Bubbles are backed by a pending intent or a shortcut, once the activity is
+ * started we never change it / restart it on notification updates -- unless the bubble's
+ * backing data switches.
+ *
+ * This indicates if the new bubble is backed by a different data source than what was
+ * previously shown here (e.g. previously a pending intent & now a shortcut).
+ *
+ * @param newBubble the bubble this view is being updated with.
+ * @return true if the backing content has changed.
+ */
+ private boolean didBackingContentChange(Bubble newBubble) {
+ boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
+ boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
+ return prevWasIntentBased != newIsIntentBased;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index f437553337ef..8ab9841ff0c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -22,6 +22,7 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -38,12 +39,13 @@ import android.util.Log;
import android.util.PathParser;
import android.view.LayoutInflater;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import java.lang.ref.WeakReference;
import java.util.Objects;
@@ -70,8 +72,8 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
private WeakReference<Context> mContext;
private WeakReference<BubbleController> mController;
private WeakReference<BubbleStackView> mStackView;
+ private WeakReference<BubbleBarLayerView> mLayerView;
private BubbleIconFactory mIconFactory;
- private BubbleBadgeIconFactory mBadgeIconFactory;
private boolean mSkipInflation;
private Callback mCallback;
private Executor mMainExecutor;
@@ -83,9 +85,9 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
BubbleViewInfoTask(Bubble b,
Context context,
BubbleController controller,
- BubbleStackView stackView,
+ @Nullable BubbleStackView stackView,
+ @Nullable BubbleBarLayerView layerView,
BubbleIconFactory factory,
- BubbleBadgeIconFactory badgeFactory,
boolean skipInflation,
Callback c,
Executor mainExecutor) {
@@ -93,8 +95,8 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
mContext = new WeakReference<>(context);
mController = new WeakReference<>(controller);
mStackView = new WeakReference<>(stackView);
+ mLayerView = new WeakReference<>(layerView);
mIconFactory = factory;
- mBadgeIconFactory = badgeFactory;
mSkipInflation = skipInflation;
mCallback = c;
mMainExecutor = mainExecutor;
@@ -102,8 +104,13 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
@Override
protected BubbleViewInfo doInBackground(Void... voids) {
- return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(),
- mIconFactory, mBadgeIconFactory, mBubble, mSkipInflation);
+ if (mController.get().isShowingAsBubbleBar()) {
+ return BubbleViewInfo.populateForBubbleBar(mContext.get(), mController.get(),
+ mLayerView.get(), mIconFactory, mBubble, mSkipInflation);
+ } else {
+ return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(),
+ mIconFactory, mBubble, mSkipInflation);
+ }
}
@Override
@@ -124,22 +131,75 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
*/
@VisibleForTesting
public static class BubbleViewInfo {
- BadgedImageView imageView;
- BubbleExpandedView expandedView;
+ // TODO(b/273312602): for foldables it might make sense to populate all of the views
+
+ // Always populated
ShortcutInfo shortcutInfo;
String appName;
- Bitmap bubbleBitmap;
- Bitmap badgeBitmap;
- Bitmap mRawBadgeBitmap;
+ Bitmap rawBadgeBitmap;
+
+ // Only populated when showing in taskbar
+ BubbleBarExpandedView bubbleBarExpandedView;
+
+ // These are only populated when not showing in taskbar
+ BadgedImageView imageView;
+ BubbleExpandedView expandedView;
int dotColor;
Path dotPath;
Bubble.FlyoutMessage flyoutMessage;
+ Bitmap bubbleBitmap;
+ Bitmap badgeBitmap;
+
+ @Nullable
+ public static BubbleViewInfo populateForBubbleBar(Context c, BubbleController controller,
+ BubbleBarLayerView layerView, BubbleIconFactory iconFactory, Bubble b,
+ boolean skipInflation) {
+ BubbleViewInfo info = new BubbleViewInfo();
+
+ if (!skipInflation && !b.isInflated()) {
+ LayoutInflater inflater = LayoutInflater.from(c);
+ info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
+ R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
+ info.bubbleBarExpandedView.initialize(controller);
+ }
+
+ if (b.getShortcutInfo() != null) {
+ info.shortcutInfo = b.getShortcutInfo();
+ }
+
+ // App name & app icon
+ PackageManager pm = BubbleController.getPackageManagerForUser(c,
+ b.getUser().getIdentifier());
+ ApplicationInfo appInfo;
+ Drawable badgedIcon;
+ Drawable appIcon;
+ try {
+ appInfo = pm.getApplicationInfo(
+ b.getPackageName(),
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+ if (appInfo != null) {
+ info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
+ }
+ appIcon = pm.getApplicationIcon(b.getPackageName());
+ badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser());
+ } catch (PackageManager.NameNotFoundException exception) {
+ // If we can't find package... don't think we should show the bubble.
+ Log.w(TAG, "Unable to find package: " + b.getPackageName());
+ return null;
+ }
+
+ info.rawBadgeBitmap = iconFactory.getBadgeBitmap(badgedIcon, false).icon;
+
+ return info;
+ }
@VisibleForTesting
@Nullable
public static BubbleViewInfo populate(Context c, BubbleController controller,
- BubbleStackView stackView, BubbleIconFactory iconFactory,
- BubbleBadgeIconFactory badgeIconFactory, Bubble b,
+ BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b,
boolean skipInflation) {
BubbleViewInfo info = new BubbleViewInfo();
@@ -191,16 +251,16 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
bubbleDrawable = appIcon;
}
- BitmapInfo badgeBitmapInfo = badgeIconFactory.getBadgeBitmap(badgedIcon,
+ BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon,
b.isImportantConversation());
info.badgeBitmap = badgeBitmapInfo.icon;
// Raw badge bitmap never includes the important conversation ring
- info.mRawBadgeBitmap = b.isImportantConversation()
- ? badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon
+ info.rawBadgeBitmap = b.isImportantConversation()
+ ? iconFactory.getBadgeBitmap(badgedIcon, false).icon
: badgeBitmapInfo.icon;
float[] bubbleBitmapScale = new float[1];
- info.bubbleBitmap = iconFactory.createIconBitmap(bubbleDrawable, bubbleBitmapScale);
+ info.bubbleBitmap = iconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
// Dot color & placement
Path iconPath = PathParser.createPathFromPathData(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
index 3f6d41bb2b68..6bdc3b9f5aec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
@@ -22,18 +22,40 @@ import android.view.View;
import androidx.annotation.Nullable;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
+
/**
* Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow.
*/
public interface BubbleViewProvider {
- @Nullable BubbleExpandedView getExpandedView();
+
+ /**
+ * Returns the icon view used for a bubble (the click target when collapsed). This is populated
+ * when bubbles are floating, i.e. when {@link BubbleController#isShowingAsBubbleBar()} is
+ * false.
+ */
+ @Nullable
+ View getIconView();
+
+ /**
+ * Returns the expanded view used for a bubble. This is populated when bubbles are floating,
+ * i.e. when {@link BubbleController#isShowingAsBubbleBar()} is false.
+ */
+ @Nullable
+ BubbleExpandedView getExpandedView();
+
+ /**
+ * Returns the expanded view used for a bubble being show in the bubble bar. This is populated
+ * when {@link BubbleController#isShowingAsBubbleBar()} is true.
+ */
+ @Nullable
+ BubbleBarExpandedView getBubbleBarExpandedView();
/**
* Sets whether the contents of the bubble's TaskView should be visible.
*/
void setTaskViewVisibility(boolean visible);
- @Nullable View getIconView();
String getKey();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index a5deac5a51da..4d329dd5d446 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -24,16 +24,21 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.app.NotificationChannel;
import android.content.Intent;
import android.content.pm.UserInfo;
+import android.graphics.drawable.Icon;
+import android.hardware.HardwareBuffer;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
import android.util.Pair;
import android.util.SparseArray;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
+import android.window.ScreenCapture.SynchronousScreenCaptureListener;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@@ -76,6 +81,11 @@ public interface Bubbles {
int DISMISS_RELOAD_FROM_DISK = 15;
int DISMISS_USER_REMOVED = 16;
+ /** Returns a binder that can be passed to an external process to manipulate Bubbles. */
+ default IBubbles createExternalInterface() {
+ return null;
+ }
+
/**
* @return {@code true} if there is a bubble associated with the provided key and if its
* notification is hidden from the shade or there is a group summary associated with the
@@ -125,12 +135,28 @@ public interface Bubbles {
* the bubble or bubble stack.
*
* Some notes:
- * - Only one app bubble is supported at a time
+ * - Only one app bubble is supported at a time, regardless of users. Multi-users support is
+ * tracked in b/273533235.
* - Calling this method with a different intent than the existing app bubble will do nothing
*
* @param intent the intent to display in the bubble expanded view.
+ * @param user the {@link UserHandle} of the user to start this activity for.
+ * @param icon the {@link Icon} to use for the bubble view.
+ */
+ void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon);
+
+ /** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */
+ boolean isAppBubbleTaskId(int taskId);
+
+ /**
+` * @return a {@link SynchronousScreenCaptureListener} after performing a screenshot that may
+ * exclude the bubble layer, if one is present. The underlying
+ * {@link ScreenshotHardwareBuffer} can be accessed via
+ * {@link SynchronousScreenCaptureListener#getBuffer()} asynchronously and care should be taken
+ * to {@link HardwareBuffer#close()} the associated
+ * {@link ScreenshotHardwareBuffer#getHardwareBuffer()} when no longer required.`
*/
- void showOrHideAppBubble(Intent intent);
+ SynchronousScreenCaptureListener getScreenshotExcludingBubble(int displayId);
/**
* @return a bubble that matches the provided shortcutId, if one exists.
@@ -257,6 +283,17 @@ public interface Bubbles {
*/
void onUserRemoved(int removedUserId);
+ /**
+ * A listener to be notified of bubble state changes, used by launcher to render bubbles in
+ * its process.
+ */
+ interface BubbleStateListener {
+ /**
+ * Called when the bubbles state changes.
+ */
+ void onBubbleStateChange(BubbleBarUpdate update);
+ }
+
/** Listener to find out about stack expansion / collapse events. */
interface BubbleExpandListener {
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
index e7beeeb06534..3a3a378e00d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java
@@ -64,8 +64,8 @@ class BubblesNavBarGestureTracker {
stopInternal();
- mInputMonitor = InputManager.getInstance().monitorGestureInput(GESTURE_MONITOR,
- mContext.getDisplayId());
+ mInputMonitor = mContext.getSystemService(InputManager.class)
+ .monitorGestureInput(GESTURE_MONITOR, mContext.getDisplayId());
InputChannel inputChannel = mInputMonitor.getInputChannel();
BubblesNavBarMotionEventHandler motionEventHandler =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
index ab194dfb3ce9..67ecb915e098 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
@@ -144,6 +144,7 @@ class DismissView(context: Context) : FrameLayout(context) {
val gd = GradientDrawable(
GradientDrawable.Orientation.BOTTOM_TOP,
intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT))
+ gd.setDither(true)
gd.setAlpha(0)
return gd
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
new file mode 100644
index 000000000000..862e818a998b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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.Intent;
+import com.android.wm.shell.bubbles.IBubblesListener;
+
+/**
+ * Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when
+ * showing in the bubble bar.
+ */
+interface IBubbles {
+
+ oneway void registerBubbleListener(in IBubblesListener listener) = 1;
+
+ oneway void unregisterBubbleListener(in IBubblesListener listener) = 2;
+
+ oneway void showBubble(in String key, in boolean onLauncherHome) = 3;
+
+ oneway void removeBubble(in String key, in int reason) = 4;
+
+ oneway void collapseBubbles() = 5;
+
+ oneway void onTaskbarStateChanged(in int newState) = 6;
+
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
index 9c82eea1e8b8..e48f8d5f1c84 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenActivity.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker.testapp;
-
-import android.app.Activity;
+package com.android.wm.shell.bubbles;
import android.os.Bundle;
-public class SplitScreenActivity extends Activity {
+/**
+ * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks.
+ */
+oneway interface IBubblesListener {
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- setContentView(R.layout.activity_splitscreen);
- }
-}
+ /**
+ * Called when the bubbles state changes.
+ */
+ void onBubbleStateChange(in Bundle update);
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 627273f093f3..d0598cd28582 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -37,8 +37,7 @@ class StackEducationView constructor(
context: Context,
positioner: BubblePositioner,
controller: BubbleController
-)
- : LinearLayout(context) {
+) : LinearLayout(context) {
private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
else BubbleDebugConfig.TAG_BUBBLES
@@ -53,7 +52,8 @@ class StackEducationView constructor(
private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) }
private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) }
- private var isHiding = false
+ var isHiding = false
+ private set
init {
LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this)
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 55052e614458..beb1c5fa6c10 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
@@ -523,6 +523,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
*/
@Nullable private SpringAnimation getSpringAnimationFromView(
DynamicAnimation.ViewProperty property, View view) {
+ if (view == null) return null;
return (SpringAnimation) view.getTag(getTagIdForProperty(property));
}
@@ -531,11 +532,13 @@ public class PhysicsAnimationLayout extends FrameLayout {
* system.
*/
@Nullable private ViewPropertyAnimator getViewPropertyAnimatorFromView(View view) {
+ if (view == null) return null;
return (ViewPropertyAnimator) view.getTag(R.id.reorder_animator_tag);
}
/** Retrieves the target animator from the view via the view tag system. */
@Nullable private ObjectAnimator getTargetAnimatorFromView(View view) {
+ if (view == null) return null;
return (ObjectAnimator) view.getTag(R.id.target_animator_tag);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
new file mode 100644
index 000000000000..23f65f943aa4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -0,0 +1,233 @@
+/*
+ * 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 static android.view.View.VISIBLE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.Log;
+import android.widget.FrameLayout;
+
+import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.BubbleViewProvider;
+import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
+
+/**
+ * Helper class to animate a {@link BubbleBarExpandedView} on a bubble.
+ */
+public class BubbleBarAnimationHelper {
+
+ private static final String TAG = BubbleBarAnimationHelper.class.getSimpleName();
+
+ private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f;
+ private static final float EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT = .75f;
+ private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
+
+ /** Spring config for the expanded view scale-in animation. */
+ private final PhysicsAnimator.SpringConfig mScaleInSpringConfig =
+ new PhysicsAnimator.SpringConfig(300f, 0.9f);
+
+ /** Spring config for the expanded view scale-out animation. */
+ private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig =
+ new PhysicsAnimator.SpringConfig(900f, 1f);
+
+ /** Matrix used to scale the expanded view container with a given pivot point. */
+ private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix();
+
+ /** Animator for animating the expanded view's alpha (including the TaskView inside it). */
+ private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f);
+
+ private final Context mContext;
+ private final BubbleBarLayerView mLayerView;
+ private final BubblePositioner mPositioner;
+
+ private BubbleViewProvider mExpandedBubble;
+ private boolean mIsExpanded = false;
+
+ public BubbleBarAnimationHelper(Context context,
+ BubbleBarLayerView bubbleBarLayerView,
+ BubblePositioner positioner) {
+ mContext = context;
+ mLayerView = bubbleBarLayerView;
+ mPositioner = positioner;
+
+ mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION);
+ mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
+ mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ // We need to be Z ordered on top in order for alpha animations to work.
+ mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(true);
+ mExpandedBubble.getBubbleBarExpandedView().setAnimating(true);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ // The surface needs to be Z ordered on top for alpha values to work on the
+ // TaskView, and if we're temporarily hidden, we are still on the screen
+ // with alpha = 0f until we animate back. Stay Z ordered on top so the alpha
+ // = 0f remains in effect.
+ if (mIsExpanded) {
+ mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(false);
+ }
+
+ mExpandedBubble.getBubbleBarExpandedView().setContentVisibility(mIsExpanded);
+ mExpandedBubble.getBubbleBarExpandedView().setAnimating(false);
+ }
+ }
+ });
+ mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
+ if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ float alpha = (float) valueAnimator.getAnimatedValue();
+ mExpandedBubble.getBubbleBarExpandedView().setTaskViewAlpha(alpha);
+ mExpandedBubble.getBubbleBarExpandedView().setAlpha(alpha);
+ }
+ });
+ }
+
+ /**
+ * Animates the provided bubble's expanded view to the expanded state.
+ */
+ public void animateExpansion(BubbleViewProvider expandedBubble) {
+ mExpandedBubble = expandedBubble;
+ if (mExpandedBubble == null) {
+ return;
+ }
+ BubbleBarExpandedView bev = mExpandedBubble.getBubbleBarExpandedView();
+ if (bev == null) {
+ return;
+ }
+ mIsExpanded = true;
+
+ mExpandedViewContainerMatrix.setScaleX(0f);
+ mExpandedViewContainerMatrix.setScaleY(0f);
+
+ updateExpandedView();
+ bev.setAnimating(true);
+ bev.setContentVisibility(false);
+ bev.setAlpha(0f);
+ bev.setTaskViewAlpha(0f);
+ bev.setVisibility(VISIBLE);
+
+ // Set the pivot point for the scale, so the view animates out from the bubble bar.
+ PointF bubbleBarPosition = mPositioner.getBubbleBarPosition();
+ mExpandedViewContainerMatrix.setScale(
+ 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
+ 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
+ bubbleBarPosition.x,
+ bubbleBarPosition.y);
+
+ bev.setAnimationMatrix(mExpandedViewContainerMatrix);
+
+ mExpandedViewAlphaAnimator.start();
+
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
+ .spring(AnimatableScaleMatrix.SCALE_X,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+ mScaleInSpringConfig)
+ .spring(AnimatableScaleMatrix.SCALE_Y,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+ mScaleInSpringConfig)
+ .addUpdateListener((target, values) -> {
+ mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(
+ mExpandedViewContainerMatrix);
+ })
+ .withEndActions(() -> {
+ bev.setAnimationMatrix(null);
+ updateExpandedView();
+ bev.setSurfaceZOrderedOnTop(false);
+ })
+ .start();
+ }
+
+ /**
+ * Collapses the currently expanded bubble.
+ *
+ * @param endRunnable a runnable to run at the end of the animation.
+ */
+ public void animateCollapse(Runnable endRunnable) {
+ mIsExpanded = false;
+ if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
+ Log.w(TAG, "Trying to animate collapse without a bubble");
+ return;
+ }
+
+ mExpandedViewContainerMatrix.setScaleX(1f);
+ mExpandedViewContainerMatrix.setScaleY(1f);
+
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
+ .spring(AnimatableScaleMatrix.SCALE_X,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
+ EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT),
+ mScaleOutSpringConfig)
+ .spring(AnimatableScaleMatrix.SCALE_Y,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
+ EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT),
+ mScaleOutSpringConfig)
+ .addUpdateListener((target, values) -> {
+ if (mExpandedBubble != null
+ && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(
+ mExpandedViewContainerMatrix);
+ }
+ })
+ .withEndActions(() -> {
+ if (mExpandedBubble != null
+ && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(null);
+ }
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ })
+ .start();
+ mExpandedViewAlphaAnimator.reverse();
+ }
+
+ private void updateExpandedView() {
+ if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
+ Log.w(TAG, "Trying to update the expanded view without a bubble");
+ return;
+ }
+ BubbleBarExpandedView bbev = mExpandedBubble.getBubbleBarExpandedView();
+
+ final int padding = mPositioner.getBubbleBarExpandedViewPadding();
+ final int width = mPositioner.getExpandedViewWidthForBubbleBar();
+ final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bbev.getLayoutParams();
+ lp.width = width;
+ lp.height = height;
+ bbev.setLayoutParams(lp);
+ if (mLayerView.isOnLeft()) {
+ bbev.setX(mPositioner.getInsets().left + padding);
+ } else {
+ bbev.setX(mPositioner.getAvailableRect().width() - width - padding);
+ }
+ bbev.setY(mPositioner.getInsets().top + padding);
+ bbev.updateLocation();
+ }
+}
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
new file mode 100644
index 000000000000..b8f049becb6f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -0,0 +1,267 @@
+/*
+ * 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.app.ActivityManager;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.Bubble;
+import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
+import com.android.wm.shell.taskview.TaskView;
+
+/**
+ * Expanded view of a bubble when it's part of the bubble bar.
+ *
+ * {@link BubbleController#isShowingAsBubbleBar()}
+ */
+public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener {
+
+ private static final String TAG = BubbleBarExpandedView.class.getSimpleName();
+ private static final int INVALID_TASK_ID = -1;
+
+ private BubbleController mController;
+ private BubbleTaskViewHelper mBubbleTaskViewHelper;
+
+ private HandleView mMenuView;
+ private TaskView mTaskView;
+
+ private int mMenuHeight;
+ private int mBackgroundColor;
+ private float mCornerRadius = 0f;
+
+ /**
+ * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
+ * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
+ * value until the animation ends.
+ */
+ private boolean mIsContentVisible = false;
+ private boolean mIsAnimating;
+
+ public BubbleBarExpandedView(Context context) {
+ this(context, null);
+ }
+
+ public BubbleBarExpandedView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ Context context = getContext();
+ setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
+ mMenuHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.bubblebar_expanded_view_menu_size);
+ mMenuView = new HandleView(context);
+ addView(mMenuView);
+
+ applyThemeAttrs();
+ setClipToOutline(true);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
+ }
+ });
+ }
+
+ /** Set the BubbleController on the view, must be called before doing anything else. */
+ public void initialize(BubbleController controller) {
+ mController = controller;
+ mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController,
+ /* listener= */ this,
+ /* viewParent= */ this);
+ mTaskView = mBubbleTaskViewHelper.getTaskView();
+ addView(mTaskView);
+ mTaskView.setEnableSurfaceClipping(true);
+ mTaskView.setCornerRadius(mCornerRadius);
+ }
+
+ // TODO (b/275087636): call this when theme/config changes
+ void applyThemeAttrs() {
+ boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
+ mContext.getResources());
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.dialogCornerRadius,
+ android.R.attr.colorBackgroundFloating});
+ mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
+ mCornerRadius = mCornerRadius / 2f;
+ mBackgroundColor = ta.getColor(1, Color.WHITE);
+
+ ta.recycle();
+
+ mMenuView.setCornerRadius(mCornerRadius);
+ mMenuHeight = getResources().getDimensionPixelSize(
+ R.dimen.bubblebar_expanded_view_menu_size);
+
+ if (mTaskView != null) {
+ mTaskView.setCornerRadius(mCornerRadius);
+ mTaskView.setElevation(150);
+ updateMenuColor();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Add corner radius here so that the menu extends behind the rounded corners of TaskView.
+ int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
+ measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+
+ if (mTaskView != null) {
+ int taskViewHeight = height - menuViewHeight;
+ measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Drag handle above
+ final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
+ mMenuView.layout(l, t, r, dragHandleBottom);
+ if (mTaskView != null) {
+ // Subtract radius so that the menu extends behind the rounded corners of TaskView.
+ mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
+ dragHandleBottom + mTaskView.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public void onTaskCreated() {
+ setContentVisibility(true);
+ updateMenuColor();
+ }
+
+ @Override
+ public void onContentVisibilityChanged(boolean visible) {
+ setContentVisibility(visible);
+ }
+
+ @Override
+ public void onBackPressed() {
+ mController.collapseStack();
+ }
+
+ /** Cleans up task view, should be called when the bubble is no longer active. */
+ public void cleanUpExpandedState() {
+ if (mBubbleTaskViewHelper != null) {
+ if (mTaskView != null) {
+ removeView(mTaskView);
+ }
+ mBubbleTaskViewHelper.cleanUpTaskView();
+ }
+ }
+
+ /** Updates the bubble shown in this task view. */
+ public void update(Bubble bubble) {
+ mBubbleTaskViewHelper.update(bubble);
+ }
+
+ /** The task id of the activity shown in the task view, if it exists. */
+ public int getTaskId() {
+ return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
+ }
+
+ /**
+ * Call when the location or size of the view has changed to update TaskView.
+ */
+ public void updateLocation() {
+ if (mTaskView == null) return;
+ mTaskView.onLocationChanged();
+ }
+
+ /** Sets the alpha of the task view. */
+ public void setContentVisibility(boolean visible) {
+ mIsContentVisible = visible;
+
+ if (mTaskView == null) return;
+
+ if (!mIsAnimating) {
+ mTaskView.setAlpha(visible ? 1f : 0f);
+ }
+ }
+
+ /** Updates the menu bar to be the status bar color specified by the app. */
+ private void updateMenuColor() {
+ if (mTaskView == null) return;
+ ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
+ final int taskBgColor = info.taskDescription.getStatusBarColor();
+ final int color = Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
+ if (color != -1) {
+ mMenuView.setBackgroundColor(color);
+ } else {
+ mMenuView.setBackgroundColor(mBackgroundColor);
+ }
+ }
+
+ /**
+ * Sets the alpha of both this view and the task view.
+ */
+ public void setTaskViewAlpha(float alpha) {
+ if (mTaskView != null) {
+ mTaskView.setAlpha(alpha);
+ }
+ setAlpha(alpha);
+ }
+
+ /**
+ * Sets whether the surface displaying app content should sit on top. This is useful for
+ * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
+ * being dragged out, the manage menu) this is set to false, otherwise it should be true.
+ */
+ public void setSurfaceZOrderedOnTop(boolean onTop) {
+ if (mTaskView == null) {
+ return;
+ }
+ mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
+ }
+
+ /**
+ * Sets whether the view is animating, in this case we won't change the content visibility
+ * until the animation is done.
+ */
+ public void setAnimating(boolean animating) {
+ mIsAnimating = animating;
+ // If we're done animating, apply the correct visibility.
+ if (!animating) {
+ setContentVisibility(mIsContentVisible);
+ }
+ }
+}
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
new file mode 100644
index 000000000000..b1a725b6e5c4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -0,0 +1,212 @@
+/*
+ * 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 static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
+import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.ColorDrawable;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+
+import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.BubbleViewProvider;
+
+/**
+ * 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
+ * launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around
+ * on screen and instead shows & animates the expanded bubble for the bubble bar.
+ */
+public class BubbleBarLayerView extends FrameLayout
+ implements ViewTreeObserver.OnComputeInternalInsetsListener {
+
+ private static final String TAG = BubbleBarLayerView.class.getSimpleName();
+
+ private static final float SCRIM_ALPHA = 0.2f;
+
+ private final BubbleController mBubbleController;
+ private final BubblePositioner mPositioner;
+ private final BubbleBarAnimationHelper mAnimationHelper;
+ private final View mScrimView;
+
+ @Nullable
+ private BubbleViewProvider mExpandedBubble;
+ private BubbleBarExpandedView mExpandedView;
+
+ // TODO(b/273310265) - currently the view is always on the right, need to update for RTL.
+ /** Whether the expanded view is displaying on the left of the screen or not. */
+ private boolean mOnLeft = false;
+
+ /** Whether a bubble is expanded. */
+ private boolean mIsExpanded = false;
+
+ private final Region mTouchableRegion = new Region();
+ private final Rect mTempRect = new Rect();
+
+ public BubbleBarLayerView(Context context, BubbleController controller) {
+ super(context);
+ mBubbleController = controller;
+ mPositioner = mBubbleController.getPositioner();
+
+ mAnimationHelper = new BubbleBarAnimationHelper(context,
+ this, mPositioner);
+
+ mScrimView = new View(getContext());
+ mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mScrimView.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
+ addView(mScrimView);
+ mScrimView.setAlpha(0f);
+ mScrimView.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
+
+ setOnClickListener(view -> {
+ mBubbleController.collapseStack();
+ });
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPositioner.update();
+ getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+
+ if (mExpandedView != null) {
+ removeView(mExpandedView);
+ mExpandedView = null;
+ }
+ }
+
+ @Override
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ mTouchableRegion.setEmpty();
+ getTouchableRegion(mTouchableRegion);
+ inoutInfo.touchableRegion.set(mTouchableRegion);
+ }
+
+ /** Updates the sizes of any displaying expanded view. */
+ public void onDisplaySizeChanged() {
+ if (mIsExpanded && mExpandedView != null) {
+ updateExpandedView();
+ }
+ }
+
+ /** Whether the stack of bubbles is expanded or not. */
+ public boolean isExpanded() {
+ return mIsExpanded;
+ }
+
+ // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done.
+ /** Whether the expanded view is positioned on the left or right side of the screen. */
+ public boolean isOnLeft() {
+ return mOnLeft;
+ }
+
+ /** Shows the expanded view of the provided bubble. */
+ public void showExpandedView(BubbleViewProvider b) {
+ BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
+ if (expandedView == null) {
+ return;
+ }
+ if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
+ removeView(mExpandedView);
+ mExpandedView = null;
+ }
+ if (mExpandedView == null) {
+ mExpandedBubble = b;
+ mExpandedView = expandedView;
+ final int width = mPositioner.getExpandedViewWidthForBubbleBar();
+ final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+ mExpandedView.setVisibility(GONE);
+ addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
+ }
+
+ mIsExpanded = true;
+ mBubbleController.getSysuiProxy().onStackExpandChanged(true);
+ mAnimationHelper.animateExpansion(mExpandedBubble);
+ showScrim(true);
+ }
+
+ /** Collapses any showing expanded view */
+ public void collapse() {
+ mIsExpanded = false;
+ final BubbleBarExpandedView viewToRemove = mExpandedView;
+ mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
+ mBubbleController.getSysuiProxy().onStackExpandChanged(false);
+ mExpandedView = null;
+ showScrim(false);
+ }
+
+ /** Updates the expanded view size and position. */
+ private void updateExpandedView() {
+ if (mExpandedView == null) return;
+ final int padding = mPositioner.getBubbleBarExpandedViewPadding();
+ final int width = mPositioner.getExpandedViewWidthForBubbleBar();
+ final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+ FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams();
+ lp.width = width;
+ lp.height = height;
+ mExpandedView.setLayoutParams(lp);
+ if (mOnLeft) {
+ mExpandedView.setX(mPositioner.getInsets().left + padding);
+ } else {
+ mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding);
+ }
+ mExpandedView.setY(mPositioner.getInsets().top + padding);
+ mExpandedView.updateLocation();
+ }
+
+ private void showScrim(boolean show) {
+ if (show) {
+ mScrimView.animate()
+ .setInterpolator(ALPHA_IN)
+ .alpha(SCRIM_ALPHA)
+ .start();
+ } else {
+ mScrimView.animate()
+ .alpha(0f)
+ .setInterpolator(ALPHA_OUT)
+ .start();
+ }
+ }
+
+ /**
+ * Fills in the touchable region for expanded view. This is used by window manager to
+ * decide which touch events go to the expanded view.
+ */
+ private void getTouchableRegion(Region outRegion) {
+ mTempRect.setEmpty();
+ if (mIsExpanded) {
+ getBoundsOnScreen(mTempRect);
+ outRegion.op(mTempRect, Region.Op.UNION);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
new file mode 100644
index 000000000000..9ee8a9d98aa1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
@@ -0,0 +1,42 @@
+/*
+ * 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.content.Context;
+import android.view.Gravity;
+import android.widget.LinearLayout;
+
+/**
+ * Handle / menu view to show at the top of a bubble bar expanded view.
+ */
+public class HandleView extends LinearLayout {
+
+ // TODO(b/273307221): implement the manage menu in this view.
+ public HandleView(Context context) {
+ super(context);
+ setOrientation(LinearLayout.HORIZONTAL);
+ setGravity(Gravity.CENTER);
+ }
+
+ /**
+ * The menu extends past the top of the TaskView because of the rounded corners. This means
+ * to center content in the menu we must subtract the radius (i.e. the amount of space covered
+ * by TaskView).
+ */
+ public void setCornerRadius(float radius) {
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
index ae1f43320c8b..72702e7c2b88 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
@@ -79,7 +79,7 @@ public class DisplayChangeController {
}
/** Query all listeners for changes that should happen on display change. */
- public void dispatchOnDisplayChange(WindowContainerTransaction outWct, int displayId,
+ void dispatchOnDisplayChange(WindowContainerTransaction outWct, int displayId,
int fromRotation, int toRotation, DisplayAreaInfo newDisplayAreaInfo) {
for (OnDisplayChangingListener c : mDisplayChangeListener) {
c.onDisplayChange(displayId, fromRotation, toRotation, newDisplayAreaInfo, outWct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index f07ea751b044..8353900be0ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -29,6 +29,7 @@ import android.view.Display;
import android.view.IDisplayWindowListener;
import android.view.IWindowManager;
import android.view.InsetsState;
+import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
@@ -85,11 +86,6 @@ public class DisplayController {
}
}
- /** Get the DisplayChangeController. */
- public DisplayChangeController getChangeController() {
- return mChangeController;
- }
-
/**
* Gets a display by id from DisplayManager.
*/
@@ -195,6 +191,26 @@ public class DisplayController {
}
}
+
+ /** Called when a display rotate requested. */
+ public void onDisplayRotateRequested(WindowContainerTransaction wct, int displayId,
+ int fromRotation, int toRotation) {
+ synchronized (mDisplays) {
+ final DisplayRecord dr = mDisplays.get(displayId);
+ if (dr == null) {
+ Slog.w(TAG, "Skipping Display rotate on non-added display.");
+ return;
+ }
+
+ if (dr.mDisplayLayout != null) {
+ dr.mDisplayLayout.rotateTo(dr.mContext.getResources(), toRotation);
+ }
+
+ mChangeController.dispatchOnDisplayChange(
+ wct, displayId, fromRotation, toRotation, null /* newDisplayAreaInfo */);
+ }
+ }
+
private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
synchronized (mDisplays) {
final DisplayRecord dr = mDisplays.get(displayId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 7f7af935ff2b..2ea43162d225 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -16,17 +16,23 @@
package com.android.wm.shell.common;
+import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_CANCEL;
+import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_END;
+import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_START;
+import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY;
+import static android.view.inputmethod.ImeTracker.TOKEN_NONE;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.content.ComponentName;
-import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.RemoteException;
-import android.os.ServiceManager;
+import android.util.EventLog;
import android.util.Slog;
import android.util.SparseArray;
import android.view.IDisplayWindowInsetsController;
@@ -34,19 +40,21 @@ import android.view.IWindowManager;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
-import android.view.InsetsVisibilities;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodManagerGlobal;
import androidx.annotation.VisibleForTesting;
-import com.android.internal.view.IInputMethodManager;
import com.android.wm.shell.sysui.ShellInit;
import java.util.ArrayList;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -114,7 +122,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
if (mDisplayController.getDisplayLayout(displayId).rotation()
!= pd.mRotation && isImeShowing(displayId)) {
- pd.startAnimation(true, false /* forceRestart */);
+ pd.startAnimation(true, false /* forceRestart */, null /* statsToken */);
}
}
@@ -133,7 +141,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
if (pd == null) {
return false;
}
- final InsetsSource imeSource = pd.mInsetsState.getSource(InsetsState.ITYPE_IME);
+ final InsetsSource imeSource = pd.mInsetsState.peekSource(InsetsSource.ID_IME);
return imeSource != null && pd.mImeSourceControl != null && imeSource.isVisible();
}
@@ -209,7 +217,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener {
final int mDisplayId;
final InsetsState mInsetsState = new InsetsState();
- final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+ @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
InsetsSourceControl mImeSourceControl = null;
int mAnimationDirection = DIRECTION_NONE;
ValueAnimator mAnimation = null;
@@ -237,16 +245,19 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
return;
}
- updateImeVisibility(insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME));
+ updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME,
+ WindowInsets.Type.ime()));
- final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME);
- final Rect newFrame = newSource.getFrame();
- final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame();
+ final InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME);
+ final Rect newFrame = newSource != null ? newSource.getFrame() : null;
+ final boolean newSourceVisible = newSource != null && newSource.isVisible();
+ final InsetsSource oldSource = mInsetsState.peekSource(InsetsSource.ID_IME);
+ final Rect oldFrame = oldSource != null ? oldSource.getFrame() : null;
mInsetsState.set(insetsState, true /* copySources */);
- if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) {
+ if (mImeShowing && !Objects.equals(oldFrame, newFrame) && newSourceVisible) {
if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation");
- startAnimation(mImeShowing, true /* forceRestart */);
+ startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
}
}
@@ -261,7 +272,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
if (activeControl == null) {
continue;
}
- if (activeControl.getType() == InsetsState.ITYPE_IME) {
+ if (activeControl.getType() == WindowInsets.Type.ime()) {
imeSourceControl = activeControl;
}
}
@@ -280,7 +291,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
final boolean positionChanged =
!imeSourceControl.getSurfacePosition().equals(lastSurfacePosition);
if (positionChanged) {
- startAnimation(mImeShowing, true /* forceRestart */);
+ startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
}
} else {
if (!haveSameLeash(mImeSourceControl, imeSourceControl)) {
@@ -315,26 +326,27 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
@Override
- public void showInsets(int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
if (DEBUG) Slog.d(TAG, "Got showInsets for ime");
- startAnimation(true /* show */, false /* forceRestart */);
+ startAnimation(true /* show */, false /* forceRestart */, statsToken);
}
@Override
- public void hideInsets(int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
if (DEBUG) Slog.d(TAG, "Got hideInsets for ime");
- startAnimation(false /* show */, false /* forceRestart */);
+ startAnimation(false /* show */, false /* forceRestart */, statsToken);
}
@Override
- public void topFocusedWindowChanged(ComponentName component,
- InsetsVisibilities requestedVisibilities) {
+ public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) {
// Do nothing
}
@@ -342,11 +354,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
* Sends the local visibility state back to window manager. Needed for legacy adjustForIme.
*/
private void setVisibleDirectly(boolean visible) {
- mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible);
- mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible);
+ mInsetsState.setSourceVisible(InsetsSource.ID_IME, visible);
+ mRequestedVisibleTypes = visible
+ ? mRequestedVisibleTypes | WindowInsets.Type.ime()
+ : mRequestedVisibleTypes & ~WindowInsets.Type.ime();
try {
- mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId,
- mRequestedVisibilities);
+ mWmService.updateDisplayWindowRequestedVisibleTypes(mDisplayId,
+ mRequestedVisibleTypes);
} catch (RemoteException e) {
}
}
@@ -369,9 +383,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
.navBarFrameHeight();
}
- private void startAnimation(final boolean show, final boolean forceRestart) {
- final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME);
+ private void startAnimation(final boolean show, final boolean forceRestart,
+ @Nullable ImeTracker.Token statsToken) {
+ final InsetsSource imeSource = mInsetsState.peekSource(InsetsSource.ID_IME);
if (imeSource == null || mImeSourceControl == null) {
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
return;
}
final Rect newFrame = imeSource.getFrame();
@@ -392,8 +408,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
+ (mAnimationDirection == DIRECTION_SHOW ? "SHOW"
: (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE")));
}
- if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show)
+ if ((!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show))
|| (mAnimationDirection == DIRECTION_HIDE && !show)) {
+ ImeTracker.forLogging().onCancelled(
+ statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
return;
}
boolean seek = false;
@@ -437,8 +455,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
mTransactionPool.release(t);
});
mAnimation.setInterpolator(INTERPOLATOR);
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
mAnimation.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled = false;
+ @Nullable
+ private final ImeTracker.Token mStatsToken = statsToken;
@Override
public void onAnimationStart(Animator animation) {
@@ -457,8 +478,19 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
: 1.f;
t.setAlpha(mImeSourceControl.getLeash(), alpha);
if (mAnimationDirection == DIRECTION_SHOW) {
+ ImeTracker.forLogging().onProgress(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
t.show(mImeSourceControl.getLeash());
}
+ if (DEBUG_IME_VISIBILITY) {
+ EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START,
+ statsToken != null ? statsToken.getTag() : TOKEN_NONE,
+ mDisplayId, mAnimationDirection, alpha, startY , endY,
+ Objects.toString(mImeSourceControl.getLeash()),
+ Objects.toString(mImeSourceControl.getInsetsHint()),
+ Objects.toString(mImeSourceControl.getSurfacePosition()),
+ Objects.toString(mImeFrame));
+ }
t.apply();
mTransactionPool.release(t);
}
@@ -466,6 +498,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
@Override
public void onAnimationCancel(Animator animation) {
mCancelled = true;
+ if (DEBUG_IME_VISIBILITY) {
+ EventLog.writeEvent(IMF_IME_REMOTE_ANIM_CANCEL,
+ statsToken != null ? statsToken.getTag() : TOKEN_NONE, mDisplayId,
+ Objects.toString(mImeSourceControl.getInsetsHint()));
+ }
}
@Override
@@ -478,8 +515,25 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
dispatchEndPositioning(mDisplayId, mCancelled, t);
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
+ ImeTracker.forLogging().onProgress(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
t.hide(mImeSourceControl.getLeash());
removeImeSurface();
+ ImeTracker.forLogging().onHidden(mStatsToken);
+ } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
+ ImeTracker.forLogging().onShown(mStatsToken);
+ } else if (mCancelled) {
+ ImeTracker.forLogging().onCancelled(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
+ }
+ if (DEBUG_IME_VISIBILITY) {
+ EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END,
+ statsToken != null ? statsToken.getTag() : TOKEN_NONE,
+ mDisplayId, mAnimationDirection, endY,
+ Objects.toString(mImeSourceControl.getLeash()),
+ Objects.toString(mImeSourceControl.getInsetsHint()),
+ Objects.toString(mImeSourceControl.getSurfacePosition()),
+ Objects.toString(mImeFrame));
}
t.apply();
mTransactionPool.release(t);
@@ -515,16 +569,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
void removeImeSurface() {
- final IInputMethodManager imms = getImms();
- if (imms != null) {
- try {
- // Remove the IME surface to make the insets invisible for
- // non-client controlled insets.
- imms.removeImeSurface();
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to remove IME surface.", e);
- }
- }
+ // Remove the IME surface to make the insets invisible for
+ // non-client controlled insets.
+ InputMethodManagerGlobal.removeImeSurface(
+ e -> Slog.e(TAG, "Failed to remove IME surface.", e));
}
/**
@@ -598,11 +646,6 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
}
- public IInputMethodManager getImms() {
- return IInputMethodManager.Stub.asInterface(
- ServiceManager.getService(Context.INPUT_METHOD_SERVICE));
- }
-
private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) {
if (a == b) {
return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index 90a01f8c5295..9bdda14cf00b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.common;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.Slog;
@@ -24,7 +25,8 @@ import android.view.IDisplayWindowInsetsController;
import android.view.IWindowManager;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
import androidx.annotation.BinderThread;
@@ -156,34 +158,44 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
}
}
- private void showInsets(int types, boolean fromIme) {
+ private void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
+ ImeTracker.forLogging().onFailed(
+ statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
return;
}
+ ImeTracker.forLogging().onProgress(
+ statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
for (OnInsetsChangedListener listener : listeners) {
- listener.showInsets(types, fromIme);
+ listener.showInsets(types, fromIme, statsToken);
}
}
- private void hideInsets(int types, boolean fromIme) {
+ private void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
+ ImeTracker.forLogging().onFailed(
+ statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
return;
}
+ ImeTracker.forLogging().onProgress(
+ statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
for (OnInsetsChangedListener listener : listeners) {
- listener.hideInsets(types, fromIme);
+ listener.hideInsets(types, fromIme, statsToken);
}
}
private void topFocusedWindowChanged(ComponentName component,
- InsetsVisibilities requestedVisibilities) {
+ @InsetsType int requestedVisibleTypes) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
return;
}
for (OnInsetsChangedListener listener : listeners) {
- listener.topFocusedWindowChanged(component, requestedVisibilities);
+ listener.topFocusedWindowChanged(component, requestedVisibleTypes);
}
}
@@ -192,9 +204,9 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
extends IDisplayWindowInsetsController.Stub {
@Override
public void topFocusedWindowChanged(ComponentName component,
- InsetsVisibilities requestedVisibilities) throws RemoteException {
+ @InsetsType int requestedVisibleTypes) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.topFocusedWindowChanged(component, requestedVisibilities);
+ PerDisplay.this.topFocusedWindowChanged(component, requestedVisibleTypes);
});
}
@@ -214,16 +226,18 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
}
@Override
- public void showInsets(int types, boolean fromIme) throws RemoteException {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.showInsets(types, fromIme);
+ PerDisplay.this.showInsets(types, fromIme, statsToken);
});
}
@Override
- public void hideInsets(int types, boolean fromIme) throws RemoteException {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.hideInsets(types, fromIme);
+ PerDisplay.this.hideInsets(types, fromIme, statsToken);
});
}
}
@@ -239,11 +253,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
/**
* Called when top focused window changes to determine whether or not to take over insets
* control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false.
+ *
* @param component The application component that is open in the top focussed window.
- * @param requestedVisibilities The insets visibilities requested by the focussed window.
+ * @param requestedVisibleTypes The {@link InsetsType} requested visible by the focused
+ * window.
*/
default void topFocusedWindowChanged(ComponentName component,
- InsetsVisibilities requestedVisibilities) {}
+ @InsetsType int requestedVisibleTypes) {}
/**
* Called when the window insets configuration has changed.
@@ -259,17 +275,23 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
/**
* Called when a set of insets source window should be shown by policy.
*
- * @param types internal insets types (WindowInsets.Type.InsetsType) to show
+ * @param types {@link InsetsType} to show
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME show request
+ * or {@code null} otherwise.
*/
- default void showInsets(int types, boolean fromIme) {}
+ default void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {}
/**
* Called when a set of insets source window should be hidden by policy.
*
- * @param types internal insets types (WindowInsets.Type.InsetsType) to hide
+ * @param types {@link InsetsType} to hide
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME hide request
+ * or {@code null} otherwise.
*/
- default void hideInsets(int types, boolean fromIme) {}
+ default void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {}
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index 84840139d1a3..1959eb03a6b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -25,7 +25,6 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON
import static android.util.RotationUtils.rotateBounds;
import static android.util.RotationUtils.rotateInsets;
import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
@@ -46,9 +45,9 @@ import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
-import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.Surface;
+import android.view.WindowInsets;
import androidx.annotation.VisibleForTesting;
@@ -301,9 +300,12 @@ public class DisplayLayout {
return mAllowSeamlessRotationDespiteNavBarMoving;
}
- /** @return whether the navigation bar will change sides during rotation. */
+ /**
+ * Returns {@code true} if the navigation bar will change sides during rotation and the display
+ * is not square.
+ */
public boolean navigationBarCanMove() {
- return mNavigationBarCanMove;
+ return mNavigationBarCanMove && mWidth != mHeight;
}
/** @return the rotation that would make the physical display "upside down". */
@@ -372,23 +374,19 @@ public class DisplayLayout {
// Only navigation bar
if (hasNavigationBar) {
- final InsetsSource extraNavBar = insetsState.getSource(ITYPE_EXTRA_NAVIGATION_BAR);
- final boolean hasExtraNav = extraNavBar != null && extraNavBar.isVisible();
+ final Insets insets = insetsState.calculateInsets(
+ insetsState.getDisplayFrame(),
+ WindowInsets.Type.navigationBars(),
+ false /* ignoreVisibility */);
int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
int navBarSize =
getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode);
if (position == NAV_BAR_BOTTOM) {
- outInsets.bottom = hasExtraNav
- ? Math.max(navBarSize, extraNavBar.getFrame().height())
- : navBarSize;
+ outInsets.bottom = Math.max(insets.bottom , navBarSize);
} else if (position == NAV_BAR_RIGHT) {
- outInsets.right = hasExtraNav
- ? Math.max(navBarSize, extraNavBar.getFrame().width())
- : navBarSize;
+ outInsets.right = Math.max(insets.right , navBarSize);
} else if (position == NAV_BAR_LEFT) {
- outInsets.left = hasExtraNav
- ? Math.max(navBarSize, extraNavBar.getFrame().width())
- : navBarSize;
+ outInsets.left = Math.max(insets.left , navBarSize);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
index 2a1bf0ee42ba..fad3dee1f927 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.common;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.view.SurfaceControl;
+import android.window.ScreenCapture;
import java.util.function.Consumer;
@@ -35,9 +36,9 @@ public class ScreenshotUtils {
* @param consumer Consumer for the captured buffer
*/
public static void captureLayer(SurfaceControl sc, Rect crop,
- Consumer<SurfaceControl.ScreenshotHardwareBuffer> consumer) {
- consumer.accept(SurfaceControl.captureLayers(
- new SurfaceControl.LayerCaptureArgs.Builder(sc)
+ Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) {
+ consumer.accept(ScreenCapture.captureLayers(
+ new ScreenCapture.LayerCaptureArgs.Builder(sc)
.setSourceCrop(crop)
.setCaptureSecureLayers(true)
.setAllowProtected(true)
@@ -45,7 +46,7 @@ public class ScreenshotUtils {
}
private static class BufferConsumer implements
- Consumer<SurfaceControl.ScreenshotHardwareBuffer> {
+ Consumer<ScreenCapture.ScreenshotHardwareBuffer> {
SurfaceControl mScreenshot = null;
SurfaceControl.Transaction mTransaction;
SurfaceControl mSurfaceControl;
@@ -61,7 +62,7 @@ public class ScreenshotUtils {
}
@Override
- public void accept(SurfaceControl.ScreenshotHardwareBuffer buffer) {
+ public void accept(ScreenCapture.ScreenshotHardwareBuffer buffer) {
if (buffer == null || buffer.getHardwareBuffer() == null) {
return;
}
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 e270edb800bd..5e42782431fd 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
@@ -19,6 +19,7 @@ package com.android.wm.shell.common;
import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Region;
@@ -46,6 +47,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -221,7 +223,7 @@ public class SystemWindows {
}
final Display display = mDisplayController.getDisplay(mDisplayId);
SurfaceControlViewHost viewRoot =
- new SurfaceControlViewHost(view.getContext(), display, wwm);
+ new SurfaceControlViewHost(view.getContext(), display, wwm, "SystemWindows");
attrs.flags |= FLAG_HARDWARE_ACCELERATED;
viewRoot.setView(view, attrs);
mViewRoots.put(view, viewRoot);
@@ -304,7 +306,9 @@ public class SystemWindows {
}
}
- protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+ @Override
+ protected SurfaceControl getParentSurface(IWindow window,
+ WindowManager.LayoutParams attrs) {
SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
.setName("SystemWindowLeash")
@@ -314,7 +318,7 @@ public class SystemWindows {
synchronized (this) {
mLeashForWindow.put(window.asBinder(), leash);
}
- b.setParent(leash);
+ return leash;
}
@Override
@@ -344,17 +348,17 @@ public class SystemWindows {
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration newMergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
- int resizeMode) {}
+ boolean dragResizing) {}
@Override
public void insetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {}
@Override
- public void showInsets(int types, boolean fromIme) {}
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
@Override
- public void hideInsets(int types, boolean fromIme) {}
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
@Override
public void moved(int newX, int newY) {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
index ac6e4c2a6521..53683c67d825 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
@@ -54,14 +54,6 @@ public class TabletopModeController implements
DevicePostureController.OnDevicePostureChangedListener,
DisplayController.OnDisplaysChangedListener {
/**
- * When {@code true}, floating windows like PiP would auto move to the position
- * specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode.
- */
- private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP =
- SystemProperties.getBoolean(
- "persist.wm.debug.enable_move_floating_window_in_tabletop", true);
-
- /**
* Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled,
* {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise.
* See also {@link #getPreferredHalfInTabletopMode()}.
@@ -162,14 +154,6 @@ public class TabletopModeController implements
}
}
- /**
- * @return {@code true} if floating windows like PiP would auto move to the position
- * specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode.
- */
- public boolean enableMoveFloatingWindowInTabletop() {
- return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP;
- }
-
/** @return Preferred half for floating windows like PiP when in tabletop mode. */
@PreferredTabletopHalf
public int getPreferredHalfInTabletopMode() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
index 0f9260c9deaa..9abf0f678179 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
@@ -38,7 +38,7 @@ public interface TaskStackListenerCallback {
default void onTaskStackChanged() { }
- default void onTaskProfileLocked(RunningTaskInfo taskInfo) { }
+ default void onTaskProfileLocked(RunningTaskInfo taskInfo, int userId) { }
default void onTaskDisplayChanged(int taskId, int newDisplayId) { }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
index e2106e478bb3..d8859bac471f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
@@ -150,8 +150,8 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler.
}
@Override
- public void onTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo) {
- mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskInfo).sendToTarget();
+ public void onTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo, int userId) {
+ mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, userId, 0, taskInfo).sendToTarget();
}
@Override
@@ -348,8 +348,9 @@ public class TaskStackListenerImpl extends TaskStackListener implements Handler.
case ON_TASK_PROFILE_LOCKED: {
final ActivityManager.RunningTaskInfo
info = (ActivityManager.RunningTaskInfo) msg.obj;
+ final int userId = msg.arg1;
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
- mTaskStackListeners.get(i).onTaskProfileLocked(info);
+ mTaskStackListeners.get(i).onTaskProfileLocked(info, userId);
}
break;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java
index a09aab666a31..931cf0cee28c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-package com.android.wm.shell.pip.tv;
+package com.android.wm.shell.common;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,36 +30,34 @@ import android.widget.RelativeLayout;
import com.android.wm.shell.R;
/**
- * A View that represents Pip Menu action button, such as "Fullscreen" and "Close" as well custom
- * (provided by the application in Pip) and media buttons.
+ * A common action button for TV window menu layouts.
*/
-public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener {
+public class TvWindowMenuActionButton extends RelativeLayout {
private final ImageView mIconImageView;
private final View mButtonBackgroundView;
- private final View mButtonView;
- private OnClickListener mOnClickListener;
- public TvPipMenuActionButton(Context context) {
+ private Icon mCurrentIcon;
+
+ public TvWindowMenuActionButton(Context context) {
this(context, null, 0, 0);
}
- public TvPipMenuActionButton(Context context, AttributeSet attrs) {
+ public TvWindowMenuActionButton(Context context, AttributeSet attrs) {
this(context, attrs, 0, 0);
}
- public TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ public TvWindowMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
- public TvPipMenuActionButton(
+ public TvWindowMenuActionButton(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- inflater.inflate(R.layout.tv_pip_menu_action_button, this);
+ inflater.inflate(R.layout.tv_window_menu_action_button, this);
mIconImageView = findViewById(R.id.icon);
- mButtonView = findViewById(R.id.button);
mButtonBackgroundView = findViewById(R.id.background);
final int[] values = new int[]{android.R.attr.src, android.R.attr.text};
@@ -70,23 +70,8 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic
setTextAndDescription(textResId);
}
typedArray.recycle();
- }
- @Override
- public void setOnClickListener(OnClickListener listener) {
- // We do not want to set an OnClickListener to the TvPipMenuActionButton itself, but only to
- // the ImageView. So let's "cash" the listener we've been passed here and set a "proxy"
- // listener to the ImageView.
- mOnClickListener = listener;
- mButtonView.setOnClickListener(listener != null ? this : null);
- }
-
- @Override
- public void onClick(View v) {
- if (mOnClickListener != null) {
- // Pass the correct view - this.
- mOnClickListener.onClick(this);
- }
+ setIsCustomCloseAction(false);
}
/**
@@ -105,11 +90,24 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic
}
}
+ public void setImageIconAsync(Icon icon, Handler handler) {
+ mCurrentIcon = icon;
+ // Remove old image while waiting for the new one to load.
+ mIconImageView.setImageDrawable(null);
+ icon.loadDrawableAsync(mContext, d -> {
+ // The image hasn't been set any other way and the drawable belongs to the most
+ // recently set Icon.
+ if (mIconImageView.getDrawable() == null && mCurrentIcon == icon) {
+ mIconImageView.setImageDrawable(d);
+ }
+ }, handler);
+ }
+
/**
* Sets the text for description the with the given string.
*/
public void setTextAndDescription(CharSequence text) {
- mButtonView.setContentDescription(text);
+ setContentDescription(text);
}
/**
@@ -119,32 +117,29 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic
setTextAndDescription(getContext().getString(resId));
}
- @Override
- public void setEnabled(boolean enabled) {
- mButtonView.setEnabled(enabled);
- }
-
- @Override
- public boolean isEnabled() {
- return mButtonView.isEnabled();
- }
-
- void setIsCustomCloseAction(boolean isCustomCloseAction) {
+ /**
+ * Marks this button as a custom close action button.
+ * This changes the style of the action button to highlight that this action finishes the
+ * Picture-in-Picture activity.
+ *
+ * @param isCustomCloseAction sets or unsets this button as a custom close action button.
+ */
+ public void setIsCustomCloseAction(boolean isCustomCloseAction) {
mIconImageView.setImageTintList(
getResources().getColorStateList(
- isCustomCloseAction ? R.color.tv_pip_menu_close_icon
- : R.color.tv_pip_menu_icon));
+ isCustomCloseAction ? R.color.tv_window_menu_close_icon
+ : R.color.tv_window_menu_icon));
mButtonBackgroundView.setBackgroundTintList(getResources()
- .getColorStateList(isCustomCloseAction ? R.color.tv_pip_menu_close_icon_bg
- : R.color.tv_pip_menu_icon_bg));
+ .getColorStateList(isCustomCloseAction ? R.color.tv_window_menu_close_icon_bg
+ : R.color.tv_window_menu_icon_bg));
}
@Override
public String toString() {
- if (mButtonView.getContentDescription() == null) {
- return TvPipMenuActionButton.class.getSimpleName();
+ if (getContentDescription() == null) {
+ return TvWindowMenuActionButton.class.getSimpleName();
}
- return mButtonView.getContentDescription().toString();
+ return getContentDescription().toString();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
new file mode 100644
index 000000000000..81423473171d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
@@ -0,0 +1,137 @@
+/*
+ * 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.common.bubbles;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an update to bubbles state. This is passed through
+ * {@link com.android.wm.shell.bubbles.IBubblesListener} to launcher so that taskbar may render
+ * bubbles. This should be kept this as minimal as possible in terms of data.
+ */
+public class BubbleBarUpdate implements Parcelable {
+
+ public static final String BUNDLE_KEY = "update";
+
+ public boolean expandedChanged;
+ public boolean expanded;
+ @Nullable
+ public String selectedBubbleKey;
+ @Nullable
+ public BubbleInfo addedBubble;
+ @Nullable
+ public BubbleInfo updatedBubble;
+ @Nullable
+ public String suppressedBubbleKey;
+ @Nullable
+ public String unsupressedBubbleKey;
+
+ // This is only populated if bubbles have been removed.
+ public List<RemovedBubble> removedBubbles = new ArrayList<>();
+
+ // This is only populated if the order of the bubbles has changed.
+ public List<String> bubbleKeysInOrder = new ArrayList<>();
+
+ // This is only populated the first time a listener is connected so it gets the current state.
+ public List<BubbleInfo> currentBubbleList = new ArrayList<>();
+
+ public BubbleBarUpdate() {
+ }
+
+ public BubbleBarUpdate(Parcel parcel) {
+ expandedChanged = parcel.readBoolean();
+ expanded = parcel.readBoolean();
+ selectedBubbleKey = parcel.readString();
+ addedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(),
+ BubbleInfo.class);
+ updatedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(),
+ BubbleInfo.class);
+ suppressedBubbleKey = parcel.readString();
+ unsupressedBubbleKey = parcel.readString();
+ removedBubbles = parcel.readParcelableList(new ArrayList<>(),
+ RemovedBubble.class.getClassLoader());
+ parcel.readStringList(bubbleKeysInOrder);
+ currentBubbleList = parcel.readParcelableList(new ArrayList<>(),
+ BubbleInfo.class.getClassLoader());
+ }
+
+ /**
+ * Returns whether anything has changed in this update.
+ */
+ public boolean anythingChanged() {
+ return expandedChanged
+ || selectedBubbleKey != null
+ || addedBubble != null
+ || updatedBubble != null
+ || !removedBubbles.isEmpty()
+ || !bubbleKeysInOrder.isEmpty()
+ || suppressedBubbleKey != null
+ || unsupressedBubbleKey != null
+ || !currentBubbleList.isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return "BubbleBarUpdate{ expandedChanged=" + expandedChanged
+ + " expanded=" + expanded
+ + " selectedBubbleKey=" + selectedBubbleKey
+ + " addedBubble=" + addedBubble
+ + " updatedBubble=" + updatedBubble
+ + " suppressedBubbleKey=" + suppressedBubbleKey
+ + " unsuppressedBubbleKey=" + unsupressedBubbleKey
+ + " removedBubbles=" + removedBubbles
+ + " bubbles=" + bubbleKeysInOrder
+ + " currentBubbleList=" + currentBubbleList
+ + " }";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeBoolean(expandedChanged);
+ parcel.writeBoolean(expanded);
+ parcel.writeString(selectedBubbleKey);
+ parcel.writeParcelable(addedBubble, flags);
+ parcel.writeParcelable(updatedBubble, flags);
+ parcel.writeString(suppressedBubbleKey);
+ parcel.writeString(unsupressedBubbleKey);
+ parcel.writeParcelableList(removedBubbles, flags);
+ parcel.writeStringList(bubbleKeysInOrder);
+ parcel.writeParcelableList(currentBubbleList, flags);
+ }
+
+ @NonNull
+ public static final Creator<BubbleBarUpdate> CREATOR =
+ new Creator<BubbleBarUpdate>() {
+ public BubbleBarUpdate createFromParcel(Parcel source) {
+ return new BubbleBarUpdate(source);
+ }
+ public BubbleBarUpdate[] newArray(int size) {
+ return new BubbleBarUpdate[size];
+ }
+ };
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
new file mode 100644
index 000000000000..21355a3efa2e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
@@ -0,0 +1,173 @@
+/*
+ * 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.common.bubbles;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Contains information necessary to present a bubble.
+ */
+public class BubbleInfo implements Parcelable {
+
+ private String mKey; // Same key as the Notification
+ private int mFlags; // Flags from BubbleMetadata
+ @Nullable
+ private String mShortcutId;
+ private int mUserId;
+ private String mPackageName;
+ /**
+ * All notification bubbles require a shortcut to be set on the notification, however, the
+ * app could still specify an Icon and PendingIntent to use for the bubble. In that case
+ * this icon will be populated. If the bubble is entirely shortcut based, this will be null.
+ */
+ @Nullable
+ private Icon mIcon;
+ @Nullable
+ private String mTitle;
+ private boolean mIsImportantConversation;
+
+ public BubbleInfo(String key, int flags, @Nullable String shortcutId, @Nullable Icon icon,
+ int userId, String packageName, @Nullable String title,
+ boolean isImportantConversation) {
+ mKey = key;
+ mFlags = flags;
+ mShortcutId = shortcutId;
+ mIcon = icon;
+ mUserId = userId;
+ mPackageName = packageName;
+ mTitle = title;
+ mIsImportantConversation = isImportantConversation;
+ }
+
+ private BubbleInfo(Parcel source) {
+ mKey = source.readString();
+ mFlags = source.readInt();
+ mShortcutId = source.readString();
+ mIcon = source.readTypedObject(Icon.CREATOR);
+ mUserId = source.readInt();
+ mPackageName = source.readString();
+ mTitle = source.readString();
+ mIsImportantConversation = source.readBoolean();
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ @Nullable
+ public String getShortcutId() {
+ return mShortcutId;
+ }
+
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ public int getFlags() {
+ return mFlags;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @Nullable
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public boolean isImportantConversation() {
+ return mIsImportantConversation;
+ }
+
+ /**
+ * Whether this bubble is currently being hidden from the stack.
+ */
+ public boolean isBubbleSuppressed() {
+ return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0;
+ }
+
+ /**
+ * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API
+ * to
+ * hide the bubble when in the same content).
+ */
+ public boolean isBubbleSuppressable() {
+ return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0;
+ }
+
+ /**
+ * Whether the notification for this bubble is hidden from the shade.
+ */
+ public boolean isNotificationSuppressed() {
+ return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION) != 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BubbleInfo)) return false;
+ BubbleInfo bubble = (BubbleInfo) o;
+ return Objects.equals(mKey, bubble.mKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return mKey.hashCode();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mKey);
+ parcel.writeInt(mFlags);
+ parcel.writeString(mShortcutId);
+ parcel.writeTypedObject(mIcon, flags);
+ parcel.writeInt(mUserId);
+ parcel.writeString(mPackageName);
+ parcel.writeString(mTitle);
+ parcel.writeBoolean(mIsImportantConversation);
+ }
+
+ @NonNull
+ public static final Creator<BubbleInfo> CREATOR =
+ new Creator<>() {
+ public BubbleInfo createFromParcel(Parcel source) {
+ return new BubbleInfo(source);
+ }
+
+ public BubbleInfo[] newArray(int size) {
+ return new BubbleInfo[size];
+ }
+ };
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java
new file mode 100644
index 000000000000..f90591b84b7e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java
@@ -0,0 +1,70 @@
+/*
+ * 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.common.bubbles;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a removed bubble, defining the key and reason the bubble was removed.
+ */
+public class RemovedBubble implements Parcelable {
+
+ private final String mKey;
+ private final int mRemovalReason;
+
+ public RemovedBubble(String key, int removalReason) {
+ mKey = key;
+ mRemovalReason = removalReason;
+ }
+
+ public RemovedBubble(Parcel parcel) {
+ mKey = parcel.readString();
+ mRemovalReason = parcel.readInt();
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public int getRemovalReason() {
+ return mRemovalReason;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mKey);
+ dest.writeInt(mRemovalReason);
+ }
+
+ @NonNull
+ public static final Creator<RemovedBubble> CREATOR =
+ new Creator<RemovedBubble>() {
+ public RemovedBubble createFromParcel(Parcel source) {
+ return new RemovedBubble(source);
+ }
+ public RemovedBubble[] newArray(int size) {
+ return new RemovedBubble[size];
+ }
+ };
+}
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 c63419851f7f..69f0bad4fb45 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
@@ -37,6 +37,7 @@ import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
@@ -58,9 +59,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
public static final long TOUCH_ANIMATION_DURATION = 150;
public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;
- /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */
- private float mExpandedTaskBarHeight;
-
private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
private SplitLayout mSplitLayout;
@@ -215,16 +213,19 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
void onInsetsChanged(InsetsState insetsState, boolean animate) {
mSplitLayout.getDividerBounds(mTempRect);
- final InsetsSource taskBarInsetsSource =
- insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
// Only insets the divider bar with task bar when it's expanded so that the rounded corners
// will be drawn against task bar.
// But there is no need to do it when IME showing because there are no rounded corners at
// the bottom. This also avoids the problem of task bar height not changing when IME
// floating.
- if (!insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME)
- && taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
- mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect));
+ if (!insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime())) {
+ for (int i = insetsState.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = insetsState.sourceAt(i);
+ if (source.getType() == WindowInsets.Type.navigationBars()
+ && source.insetsRoundedCornerFrame()) {
+ mTempRect.inset(source.calculateVisibleInsets(mTempRect));
+ }
+ }
}
if (!mTempRect.equals(mDividerBounds)) {
@@ -249,8 +250,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mDividerBar = findViewById(R.id.divider_bar);
mHandle = findViewById(R.id.docked_divider_handle);
mBackground = findViewById(R.id.docked_divider_background);
- mExpandedTaskBarHeight = getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.taskbar_frame_height);
mTouchElevation = getResources().getDimensionPixelSize(
R.dimen.docked_stack_divider_lift_elevation);
mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener());
@@ -372,7 +371,14 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mViewHost.relayout(lp);
}
- void setInteractive(boolean interactive, String from) {
+ /**
+ * Set divider should interactive to user or not.
+ *
+ * @param interactive divider interactive.
+ * @param hideHandle divider handle hidden or not, only work when interactive is false.
+ * @param from caller from where.
+ */
+ void setInteractive(boolean interactive, boolean hideHandle, String from) {
if (interactive == mInteractive) return;
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
"Set divider bar %s from %s", interactive ? "interactive" : "non-interactive",
@@ -388,7 +394,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mMoving = false;
}
releaseTouching();
- mHandle.setVisibility(mInteractive ? View.VISIBLE : View.INVISIBLE);
+ mHandle.setVisibility(!mInteractive && hideHandle ? View.INVISIBLE : View.VISIBLE);
}
private boolean isLandscape() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index bdf0ac2ed30c..a9ccdf6a156f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -22,6 +22,8 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
@@ -51,6 +53,8 @@ import com.android.wm.shell.R;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.SurfaceUtils;
+import java.util.function.Consumer;
+
/**
* Handles split decor like showing resizing hint for a specific split.
*/
@@ -58,7 +62,6 @@ public class SplitDecorManager extends WindowlessWindowManager {
private static final String TAG = SplitDecorManager.class.getSimpleName();
private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground";
private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground";
- private static final long FADE_DURATION = 133;
private final IconProvider mIconProvider;
private final SurfaceSession mSurfaceSession;
@@ -93,7 +96,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
}
@Override
- protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+ protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
@@ -102,7 +105,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
.setParent(mHostLeash)
.setCallsite("SplitDecorManager#attachToParentSurface");
mIconLeash = builder.build();
- b.setParent(mIconLeash);
+ return mIconLeash;
}
/** Inflates split decor surface on the root surface. */
@@ -114,7 +117,8 @@ public class SplitDecorManager extends WindowlessWindowManager {
context = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
null /* options */);
mHostLeash = rootLeash;
- mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this);
+ mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this,
+ "SplitDecorManager");
mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size);
final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context)
@@ -164,6 +168,10 @@ public class SplitDecorManager extends WindowlessWindowManager {
t.remove(mGapBackgroundLeash);
mGapBackgroundLeash = null;
}
+ if (mScreenshot != null) {
+ t.remove(mScreenshot);
+ mScreenshot = null;
+ }
mHostLeash = null;
mIcon = null;
mResizingIconView = null;
@@ -246,7 +254,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
}
/** Stops showing resizing hint. */
- public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) {
+ public void onResized(SurfaceControl.Transaction t, Consumer<Boolean> animFinishedCallback) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
}
@@ -256,6 +264,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
mScreenshotAnimator = ValueAnimator.ofFloat(1, 0);
+ mScreenshotAnimator.setDuration(FADE_DURATION);
mScreenshotAnimator.addUpdateListener(valueAnimator -> {
final float progress = (float) valueAnimator.getAnimatedValue();
animT.setAlpha(mScreenshot, progress);
@@ -276,7 +285,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
mScreenshot = null;
if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
- animFinishedCallback.run();
+ animFinishedCallback.accept(true);
}
}
});
@@ -284,6 +293,9 @@ public class SplitDecorManager extends WindowlessWindowManager {
}
if (mResizingIconView == null) {
+ if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+ animFinishedCallback.accept(false);
+ }
return;
}
@@ -302,18 +314,21 @@ public class SplitDecorManager extends WindowlessWindowManager {
releaseDecor(finishT);
finishT.apply();
finishT.close();
+ if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+ animFinishedCallback.accept(true);
+ }
}
});
return;
}
}
if (mShown) {
- fadeOutDecor(animFinishedCallback);
+ fadeOutDecor(()-> animFinishedCallback.accept(true));
} else {
// Decor surface is hidden so release it directly.
releaseDecor(t);
if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
- animFinishedCallback.run();
+ animFinishedCallback.accept(false);
}
}
}
@@ -323,6 +338,8 @@ public class SplitDecorManager extends WindowlessWindowManager {
if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
+ } else if (mScreenshot != null) {
+ t.remove(mScreenshot);
}
mTempRect.set(mOldBounds);
@@ -339,6 +356,8 @@ public class SplitDecorManager extends WindowlessWindowManager {
if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
+ } else if (mScreenshot != null) {
+ t.remove(mScreenshot);
}
mScreenshot = screenshot;
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 b4acd6046182..f70d3aec9ec8 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
@@ -63,8 +63,10 @@ import com.android.internal.policy.DockedDividerUtils;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.InteractionJankMonitorUtils;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
@@ -104,6 +106,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
private final Rect mWinBounds2 = new Rect();
private final SplitLayoutHandler mSplitLayoutHandler;
private final SplitWindowManager mSplitWindowManager;
+ private final DisplayController mDisplayController;
private final DisplayImeController mDisplayImeController;
private final ImePositionProcessor mImePositionProcessor;
private final ResizingEffectPolicy mSurfaceEffectPolicy;
@@ -128,13 +131,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public SplitLayout(String windowName, Context context, Configuration configuration,
SplitLayoutHandler splitLayoutHandler,
SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
- DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer,
- int parallaxType) {
+ DisplayController displayController, DisplayImeController displayImeController,
+ ShellTaskOrganizer taskOrganizer, int parallaxType) {
mContext = context.createConfigurationContext(configuration);
mOrientation = configuration.orientation;
mRotation = configuration.windowConfiguration.getRotation();
mDensity = configuration.densityDpi;
mSplitLayoutHandler = splitLayoutHandler;
+ mDisplayController = displayController;
mDisplayImeController = displayImeController;
mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration,
parentContainerCallbacks);
@@ -145,7 +149,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
updateDividerConfig(mContext);
mRootBounds.set(configuration.windowConfiguration.getBounds());
- mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
+ mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
resetDividerPosition();
mDimNonImeSide = mContext.getResources().getBoolean(R.bool.config_dimNonImeAttachedSide);
@@ -314,7 +318,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
mRotation = rotation;
mDensity = density;
mUiMode = uiMode;
- mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
+ mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
updateDividerConfig(mContext);
initDividerPosition(mTempRect);
updateInvisibleRect();
@@ -324,7 +328,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
/** Rotate the layout to specific rotation and calculate new bounds. The stable insets value
* should be calculated by display layout. */
- public void rotateTo(int newRotation, Rect stableInsets) {
+ public void rotateTo(int newRotation) {
final int rotationDelta = (newRotation - mRotation + 4) % 4;
final boolean changeOrient = (rotationDelta % 2) != 0;
@@ -337,7 +341,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
// We only need new bounds here, other configuration should be update later.
mTempRect.set(mRootBounds);
mRootBounds.set(tmpRect);
- mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, stableInsets);
+ mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
initDividerPosition(mTempRect);
}
@@ -412,7 +416,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
/** Releases and re-inflates {@link DividerView} on the root surface. */
public void update(SurfaceControl.Transaction t) {
- if (!mInitialized) return;
+ if (!mInitialized) {
+ init();
+ return;
+ }
mSplitWindowManager.release(t);
mImePositionProcessor.reset();
mSplitWindowManager.init(this, mInsetsState);
@@ -490,6 +497,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
/**
+ * Set divider should interactive to user or not.
+ *
+ * @param interactive divider interactive.
+ * @param hideHandle divider handle hidden or not, only work when interactive is false.
+ * @param from caller from where.
+ */
+ public void setDividerInteractive(boolean interactive, boolean hideHandle, String from) {
+ mSplitWindowManager.setInteractive(interactive, hideHandle, from);
+ }
+
+ /**
* Sets new divide position and updates bounds correspondingly. Notifies listener if the new
* target indicates dismissing split.
*/
@@ -534,10 +552,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss);
}
- private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds,
- @Nullable Rect stableInsets) {
+ private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) {
final boolean isLandscape = isLandscape(rootBounds);
- final Rect insets = stableInsets != null ? stableInsets : getDisplayInsets(context);
+ 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
@@ -620,7 +637,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
SurfaceControl leash2, Consumer<Rect> finishCallback) {
final boolean isLandscape = isLandscape();
- final Rect insets = getDisplayInsets(mContext);
+ final Rect insets = getDisplayStableInsets(mContext);
insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top,
isLandscape ? insets.right : 0, isLandscape ? 0 : insets.bottom);
@@ -691,18 +708,27 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
return animator;
}
- private static Rect getDisplayInsets(Context context) {
- return context.getSystemService(WindowManager.class)
- .getMaximumWindowMetrics()
- .getWindowInsets()
- .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout())
- .toRect();
+ private Rect getDisplayStableInsets(Context context) {
+ final DisplayLayout displayLayout =
+ mDisplayController.getDisplayLayout(context.getDisplayId());
+ return displayLayout != null
+ ? displayLayout.stableInsets()
+ : context.getSystemService(WindowManager.class)
+ .getMaximumWindowMetrics()
+ .getWindowInsets()
+ .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()
+ | WindowInsets.Type.displayCutout())
+ .toRect();
}
private static boolean isLandscape(Rect bounds) {
return bounds.width() > bounds.height();
}
+ public boolean isDensityChanged(int densityDpi) {
+ return mDensity != densityDpi;
+ }
+
/**
* Return if this layout is landscape.
*/
@@ -727,10 +753,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
getRefBounds2(mTempRect);
t.setPosition(leash2, mTempRect.left, mTempRect.top)
.setWindowCrop(leash2, mTempRect.width(), mTempRect.height());
- // Make right or bottom side surface always higher than left or top side to avoid weird
- // animation when dismiss split. e.g. App surface fling above on decor surface.
- t.setLayer(leash1, 1);
- t.setLayer(leash2, 2);
if (mImePositionProcessor.adjustSurfaceLayoutForIme(
t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) {
@@ -743,26 +765,33 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
}
- /** Apply recorded task layout to the {@link WindowContainerTransaction}. */
- public void applyTaskChanges(WindowContainerTransaction wct,
+ /** Apply recorded task layout to the {@link WindowContainerTransaction}.
+ *
+ * @return true if stage bounds actually update.
+ */
+ public boolean applyTaskChanges(WindowContainerTransaction wct,
ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
+ boolean boundsChanged = false;
if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
wct.setBounds(task1.token, mBounds1);
wct.setSmallestScreenWidthDp(task1.token, getSmallestWidthDp(mBounds1));
mWinBounds1.set(mBounds1);
mWinToken1 = task1.token;
+ boundsChanged = true;
}
if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
wct.setBounds(task2.token, mBounds2);
wct.setSmallestScreenWidthDp(task2.token, getSmallestWidthDp(mBounds2));
mWinBounds2.set(mBounds2);
mWinToken2 = task2.token;
+ boundsChanged = true;
}
+ return boundsChanged;
}
private int getSmallestWidthDp(Rect bounds) {
mTempRect.set(bounds);
- mTempRect.inset(getDisplayInsets(mContext));
+ mTempRect.inset(getDisplayStableInsets(mContext));
final int minWidth = Math.min(mTempRect.width(), mTempRect.height());
final float density = mContext.getResources().getDisplayMetrics().density;
return (int) (minWidth / density);
@@ -1099,10 +1128,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
// ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
// because DividerView won't receive onImeVisibilityChanged callback after it being
// re-inflated.
- mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus,
+ setDividerInteractive(!mImeShown || !mHasImeFocus || isFloating, true,
"onImeStartPositioning");
- return needOffset ? IME_ANIMATION_NO_ALPHA : 0;
+ return mTargetYOffset != mLastYOffset ? IME_ANIMATION_NO_ALPHA : 0;
}
@Override
@@ -1126,7 +1155,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
// Restore the split layout when wm-shell is not controlling IME insets anymore.
if (!controlling && mImeShown) {
reset();
- mSplitWindowManager.setInteractive(true, "onImeControlTargetChanged");
+ setDividerInteractive(true, true, "onImeControlTargetChanged");
mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
}
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 b8204d013105..ef93a336305f 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
@@ -43,6 +43,11 @@ public class SplitScreenConstants {
*/
public static final int SPLIT_POSITION_BOTTOM_OR_RIGHT = 1;
+ /**
+ * Duration used for every split fade-in or fade-out.
+ */
+ public static final int FADE_DURATION = 133;
+
@IntDef(prefix = {"SPLIT_POSITION_"}, value = {
SPLIT_POSITION_UNDEFINED,
SPLIT_POSITION_TOP_OR_LEFT,
@@ -59,4 +64,17 @@ public class SplitScreenConstants {
/** Flag applied to a transition change to identify it as a divider bar for animation. */
public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+
+ public static final String splitPositionToString(@SplitPosition int pos) {
+ switch (pos) {
+ case SPLIT_POSITION_UNDEFINED:
+ return "SPLIT_POSITION_UNDEFINED";
+ case SPLIT_POSITION_TOP_OR_LEFT:
+ return "SPLIT_POSITION_TOP_OR_LEFT";
+ case SPLIT_POSITION_BOTTOM_OR_RIGHT:
+ return "SPLIT_POSITION_BOTTOM_OR_RIGHT";
+ default:
+ return "UNKNOWN";
+ }
+ }
}
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 042721c97053..0289da916937 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
@@ -78,8 +78,15 @@ public class SplitScreenUtils {
return taskInfo != null ? getPackageName(taskInfo.baseIntent) : null;
}
- /** Returns true if they are the same package. */
- public static boolean samePackage(String packageName1, String packageName2) {
- return packageName1 != null && packageName1.equals(packageName2);
+ /** Retrieve user id from a taskId */
+ public static int getUserId(int taskId, ShellTaskOrganizer taskOrganizer) {
+ final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId);
+ return taskInfo != null ? taskInfo.userId : -1;
+ }
+
+ /** Returns true if package names and user ids match. */
+ public static boolean samePackage(String packageName1, String packageName2,
+ int userId1, int userId2) {
+ return (packageName1 != null && packageName1.equals(packageName2)) && (userId1 == userId2);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 060ac56cae96..00361d9dd9cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -93,7 +93,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
}
@Override
- protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+ protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
@@ -103,7 +103,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
mParentContainerCallbacks.attachToParentSurface(builder);
mLeash = builder.build();
mParentContainerCallbacks.onLeashReady(mLeash);
- b.setParent(mLeash);
+ return mLeash;
}
/** Inflates {@link DividerView} on to the root surface. */
@@ -113,7 +113,8 @@ public final class SplitWindowManager extends WindowlessWindowManager {
"Try to inflate divider view again without release first");
}
- mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+ mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this,
+ "SplitWindowManager");
mDividerView = (DividerView) LayoutInflater.from(mContext)
.inflate(R.layout.split_divider, null /* root */);
@@ -126,6 +127,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
lp.token = new Binder();
lp.setTitle(mWindowName);
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
+ lp.accessibilityTitle = mContext.getResources().getString(R.string.accessibility_divider);
mViewHost.setView(mDividerView, lp);
mDividerView.setup(splitLayout, this, mViewHost, insetsState);
}
@@ -166,9 +168,16 @@ public final class SplitWindowManager extends WindowlessWindowManager {
}
}
- void setInteractive(boolean interactive, String from) {
+ /**
+ * Set divider should interactive to user or not.
+ *
+ * @param interactive divider interactive.
+ * @param hideHandle divider handle hidden or not, only work when interactive is false.
+ * @param from caller from where.
+ */
+ void setInteractive(boolean interactive, boolean hideHandle, String from) {
if (mDividerView == null) return;
- mDividerView.setInteractive(interactive, from);
+ mDividerView.setInteractive(interactive, hideHandle, from);
}
View getDividerView() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 838e37a905db..62b0799618ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -47,6 +47,8 @@ import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import dagger.Lazy;
+
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
@@ -55,8 +57,6 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
-import dagger.Lazy;
-
/**
* Controller to show/update compat UI components on Tasks based on whether the foreground
* activities are in compatibility mode.
@@ -284,13 +284,18 @@ public class CompatUIController implements OnDisplaysChangedListener,
ShellTaskOrganizer.TaskListener taskListener) {
CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId);
if (layout != null) {
- // UI already exists, update the UI layout.
- if (!layout.updateCompatInfo(taskInfo, taskListener,
- showOnDisplay(layout.getDisplayId()))) {
- // The layout is no longer eligible to be shown, remove from active layouts.
+ if (layout.needsToBeRecreated(taskInfo, taskListener)) {
mActiveCompatLayouts.remove(taskInfo.taskId);
+ layout.release();
+ } else {
+ // UI already exists, update the UI layout.
+ if (!layout.updateCompatInfo(taskInfo, taskListener,
+ showOnDisplay(layout.getDisplayId()))) {
+ // The layout is no longer eligible to be shown, remove from active layouts.
+ mActiveCompatLayouts.remove(taskInfo.taskId);
+ }
+ return;
}
- return;
}
// Create a new UI layout.
@@ -329,17 +334,19 @@ public class CompatUIController implements OnDisplaysChangedListener,
private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
- if (mActiveLetterboxEduLayout != null
- && mActiveLetterboxEduLayout.getTaskId() == taskInfo.taskId) {
- // UI already exists, update the UI layout.
- if (!mActiveLetterboxEduLayout.updateCompatInfo(taskInfo, taskListener,
- showOnDisplay(mActiveLetterboxEduLayout.getDisplayId()))) {
- // The layout is no longer eligible to be shown, clear active layout.
+ if (mActiveLetterboxEduLayout != null) {
+ if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
+ mActiveLetterboxEduLayout.release();
mActiveLetterboxEduLayout = null;
+ } else {
+ if (!mActiveLetterboxEduLayout.updateCompatInfo(taskInfo, taskListener,
+ showOnDisplay(mActiveLetterboxEduLayout.getDisplayId()))) {
+ // The layout is no longer eligible to be shown, clear active layout.
+ mActiveLetterboxEduLayout = null;
+ }
+ return;
}
- return;
}
-
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
if (context == null) {
@@ -433,13 +440,18 @@ public class CompatUIController implements OnDisplaysChangedListener,
private void createOrUpdateReachabilityEduLayout(TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
if (mActiveReachabilityEduLayout != null) {
- // UI already exists, update the UI layout.
- if (!mActiveReachabilityEduLayout.updateCompatInfo(taskInfo, taskListener,
- showOnDisplay(mActiveReachabilityEduLayout.getDisplayId()))) {
- // The layout is no longer eligible to be shown, remove from active layouts.
+ if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
+ mActiveReachabilityEduLayout.release();
mActiveReachabilityEduLayout = null;
+ } else {
+ // UI already exists, update the UI layout.
+ if (!mActiveReachabilityEduLayout.updateCompatInfo(taskInfo, taskListener,
+ showOnDisplay(mActiveReachabilityEduLayout.getDisplayId()))) {
+ // The layout is no longer eligible to be shown, remove from active layouts.
+ mActiveReachabilityEduLayout = null;
+ }
+ return;
}
- return;
}
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
index f65c26ada04d..d44b4d8f63b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
@@ -21,7 +21,6 @@ import android.app.TaskInfo;
import android.app.TaskInfo.CameraCompatControlState;
import android.content.Context;
import android.util.AttributeSet;
-import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@@ -113,14 +112,6 @@ class CompatUILayout extends LinearLayout {
}
@Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mWindowManager.relayout();
- }
- return super.onInterceptTouchEvent(ev);
- }
-
- @Override
protected void onFinishInflate() {
super.onFinishInflate();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 170c0ee91b40..065806df3dc8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -20,8 +20,8 @@ import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.app.TaskInfo.CameraCompatControlState;
@@ -46,20 +46,12 @@ import java.util.function.Consumer;
*/
class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
- /**
- * The Compat UI should be below the Letterbox Education.
- */
- private static final int Z_ORDER = LetterboxEduWindowManager.Z_ORDER - 1;
-
private final CompatUICallback mCallback;
private final CompatUIConfiguration mCompatUIConfiguration;
private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
- @NonNull
- private TaskInfo mTaskInfo;
-
// Remember the last reported states in case visibility changes due to keyguard or IME updates.
@VisibleForTesting
boolean mHasSizeCompat;
@@ -81,7 +73,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration,
Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
- mTaskInfo = taskInfo;
mCallback = callback;
mHasSizeCompat = taskInfo.topActivityInSizeCompat;
mCameraCompatControlState = taskInfo.cameraCompatControlState;
@@ -92,7 +83,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
@Override
protected int getZOrder() {
- return Z_ORDER;
+ return TASK_CHILD_LAYER_COMPAT_UI + 1;
}
@Override
@@ -133,7 +124,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
@Override
public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
boolean canShow) {
- mTaskInfo = taskInfo;
final boolean prevHasSizeCompat = mHasSizeCompat;
final int prevCameraCompatControlState = mCameraCompatControlState;
mHasSizeCompat = taskInfo.topActivityInSizeCompat;
@@ -153,7 +143,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
/** Called when the restart button is clicked. */
void onRestartButtonClicked() {
- mOnRestartButtonClicked.accept(Pair.create(mTaskInfo, getTaskListener()));
+ mOnRestartButtonClicked.accept(Pair.create(getLastTaskInfo(), getTaskListener()));
}
/** Called when the camera treatment button is clicked. */
@@ -214,14 +204,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
: taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth();
final int positionY = taskStableBounds.bottom - taskBounds.top
- mLayout.getMeasuredHeight();
- // To secure a proper visualisation, we hide the layout while updating the position of
- // the {@link SurfaceControl} it belongs.
- final int oldVisibility = mLayout.getVisibility();
- if (oldVisibility == View.VISIBLE) {
- mLayout.setVisibility(View.GONE);
- }
updateSurfacePosition(positionX, positionY);
- mLayout.setVisibility(oldVisibility);
}
private void updateVisibilityOfViews() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index 346cd940e678..180498c50c78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -26,6 +26,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.content.Context;
@@ -65,6 +66,9 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
private DisplayLayout mDisplayLayout;
private final Rect mStableBounds;
+ @NonNull
+ private TaskInfo mTaskInfo;
+
/**
* Utility class for adding and releasing a View hierarchy for this {@link
* WindowlessWindowManager} to {@code mLeash}.
@@ -83,6 +87,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout) {
super(taskInfo.configuration, null /* rootSurface */, null /* hostInputToken */);
+ mTaskInfo = taskInfo;
mContext = context;
mSyncQueue = syncQueue;
mTaskConfig = taskInfo.configuration;
@@ -95,6 +100,17 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
}
/**
+ * @return {@code true} if the instance of the specific {@link CompatUIWindowManagerAbstract}
+ * for the current task id needs to be recreated loading the related resources. This happens
+ * if the user switches between Light/Dark mode, if the device is docked/undocked or if the
+ * user switches between multi-window mode to fullscreen where the
+ * {@link ShellTaskOrganizer.TaskListener} implementation is different.
+ */
+ boolean needsToBeRecreated(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
+ return hasUiModeChanged(mTaskInfo, taskInfo) || hasTaskListenerChanged(taskListener);
+ }
+
+ /**
* Returns the z-order of this window which will be passed to the {@link SurfaceControl} once
* {@link #attachToParentSurface} is called.
*
@@ -155,7 +171,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
}
@Override
- protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+ protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
String className = getClass().getSimpleName();
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setContainerLayer()
@@ -164,9 +180,8 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
.setCallsite(className + "#attachToParentSurface");
attachToParentSurface(builder);
mLeash = builder.build();
- b.setParent(mLeash);
-
initSurface(mLeash);
+ return mLeash;
}
protected ShellTaskOrganizer.TaskListener getTaskListener() {
@@ -196,6 +211,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
@VisibleForTesting(visibility = PROTECTED)
public boolean updateCompatInfo(TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener, boolean canShow) {
+ mTaskInfo = taskInfo;
final Configuration prevTaskConfig = mTaskConfig;
final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener;
mTaskConfig = taskInfo.configuration;
@@ -316,6 +332,11 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
updateSurfacePosition();
}
+ @Nullable
+ protected TaskInfo getLastTaskInfo() {
+ return mTaskInfo;
+ }
+
/**
* Called following a change in the task bounds, display layout stable bounds, or the layout
* direction.
@@ -364,7 +385,8 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
/** Creates a {@link SurfaceControlViewHost} for this window manager. */
@VisibleForTesting(visibility = PRIVATE)
public SurfaceControlViewHost createSurfaceViewHost() {
- return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+ return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this,
+ getClass().getSimpleName());
}
/** Gets the layout params. */
@@ -402,4 +424,12 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
protected final String getTag() {
return getClass().getSimpleName();
}
+
+ protected boolean hasTaskListenerChanged(ShellTaskOrganizer.TaskListener newTaskListener) {
+ return !mTaskListener.equals(newTaskListener);
+ }
+
+ protected static boolean hasUiModeChanged(TaskInfo currentTaskInfo, TaskInfo newTaskInfo) {
+ return currentTaskInfo.configuration.uiMode != newTaskInfo.configuration.uiMode;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java
index 0c21c8ccd686..fce1a39399d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java
@@ -17,8 +17,8 @@
package com.android.wm.shell.compatui;
import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.content.Context;
@@ -46,12 +46,6 @@ import java.util.function.Consumer;
*/
class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
- /**
- * The Letterbox Education should be the topmost child of the Task in case there can be more
- * than one child.
- */
- public static final int Z_ORDER = Integer.MAX_VALUE;
-
private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
private final Transitions mTransitions;
@@ -74,9 +68,6 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
@VisibleForTesting
LetterboxEduDialogLayout mLayout;
- @NonNull
- private TaskInfo mTaskInfo;
-
/**
* The vertical margin between the dialog container and the task stable bounds (excluding
* insets).
@@ -104,7 +95,6 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
DialogAnimationController<LetterboxEduDialogLayout> animationController,
DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
- mTaskInfo = taskInfo;
mTransitions = transitions;
mOnDismissCallback = onDismissCallback;
mAnimationController = animationController;
@@ -118,7 +108,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
@Override
protected int getZOrder() {
- return Z_ORDER;
+ return TASK_CHILD_LAYER_COMPAT_UI + 2;
}
@Override
@@ -151,7 +141,6 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
// startEnterAnimation will be called immediately if shell-transitions are disabled.
mTransitions.runOnIdle(this::startEnterAnimation);
-
return mLayout;
}
@@ -202,7 +191,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
mLayout.setDismissOnClickListener(null);
mAnimationController.startExitAnimation(mLayout, () -> {
release();
- mOnDismissCallback.accept(Pair.create(mTaskInfo, getTaskListener()));
+ mOnDismissCallback.accept(Pair.create(getLastTaskInfo(), getTaskListener()));
});
}
@@ -215,13 +204,18 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
@Override
public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
boolean canShow) {
- mTaskInfo = taskInfo;
mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation;
return super.updateCompatInfo(taskInfo, taskListener, canShow);
}
@Override
+ boolean needsToBeRecreated(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
+ return super.needsToBeRecreated(taskInfo, taskListener)
+ && !mCompatUIConfiguration.getHasSeenLetterboxEducation(mUserId);
+ }
+
+ @Override
protected void onParentBoundsChanged() {
if (mLayout == null) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
index b6e396d12512..95bb1fe1c986 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
@@ -18,8 +18,8 @@ package com.android.wm.shell.compatui;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.content.Context;
@@ -41,11 +41,6 @@ import com.android.wm.shell.common.SyncTransactionQueue;
*/
class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
- /**
- * The Compat UI should be below the Letterbox Education.
- */
- private static final int Z_ORDER = LetterboxEduWindowManager.Z_ORDER - 1;
-
// The time to wait before hiding the education
private static final long DISAPPEAR_DELAY_MS = 4000L;
@@ -56,9 +51,6 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
private final ShellExecutor mMainExecutor;
- @NonNull
- private TaskInfo mTaskInfo;
-
private boolean mIsActivityLetterboxed;
private int mLetterboxVerticalPosition;
@@ -90,7 +82,6 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout,
CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
- mTaskInfo = taskInfo;
mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled;
mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition;
mLetterboxHorizontalPosition = taskInfo.topActivityLetterboxHorizontalPosition;
@@ -102,7 +93,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
@Override
protected int getZOrder() {
- return Z_ORDER;
+ return TASK_CHILD_LAYER_COMPAT_UI + 1;
}
@Override
@@ -140,7 +131,6 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
@Override
public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
boolean canShow) {
- mTaskInfo = taskInfo;
final boolean prevIsActivityLetterboxed = mIsActivityLetterboxed;
final int prevLetterboxVerticalPosition = mLetterboxVerticalPosition;
final int prevLetterboxHorizontalPosition = mLetterboxHorizontalPosition;
@@ -226,14 +216,14 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
if (mLayout == null) {
return;
}
-
+ final TaskInfo lastTaskInfo = getLastTaskInfo();
final boolean eligibleForDisplayHorizontalEducation = mForceUpdate
- || !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(mTaskInfo)
+ || !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo)
|| (mHasUserDoubleTapped
&& (mLetterboxHorizontalPosition == REACHABILITY_LEFT_OR_UP_POSITION
|| mLetterboxHorizontalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION));
final boolean eligibleForDisplayVerticalEducation = mForceUpdate
- || !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(mTaskInfo)
+ || !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo)
|| (mHasUserDoubleTapped
&& (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION
|| mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION));
@@ -245,7 +235,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
mLayout.handleVisibility(eligibleForDisplayHorizontalEducation,
eligibleForDisplayVerticalEducation,
mLetterboxVerticalPosition, mLetterboxHorizontalPosition, availableWidth,
- availableHeight, mCompatUIConfiguration, mTaskInfo);
+ availableHeight, mCompatUIConfiguration, lastTaskInfo);
if (!mHasLetterboxSizeChanged) {
updateHideTime();
mMainExecutor.executeDelayed(this::hideReachability, DISAPPEAR_DELAY_MS);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java
index c53e6389331a..05fd5f1172e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java
@@ -95,6 +95,9 @@ public class RestartDialogLayout extends ConstraintLayout implements DialogConta
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ final View checkboxContainer = findViewById(
+ R.id.letterbox_restart_dialog_checkbox_container);
+ final CheckBox checkbox = findViewById(R.id.letterbox_restart_dialog_checkbox);
mDialogContainer = findViewById(R.id.letterbox_restart_dialog_container);
mDialogTitle = findViewById(R.id.letterbox_restart_dialog_title);
mBackgroundDim = getBackground().mutate();
@@ -103,5 +106,6 @@ public class RestartDialogLayout extends ConstraintLayout implements DialogConta
// We add a no-op on-click listener to the dialog container so that clicks on it won't
// propagate to the listener of the layout (which represents the background dim).
mDialogContainer.setOnClickListener(view -> {});
+ checkboxContainer.setOnClickListener(view -> checkbox.performClick());
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
index aab123a843ea..a770da28fbd1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
@@ -17,8 +17,8 @@
package com.android.wm.shell.compatui;
import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.content.Context;
@@ -47,12 +47,6 @@ import java.util.function.Consumer;
*/
class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
- /**
- * The restart dialog should be the topmost child of the Task in case there can be more
- * than one child.
- */
- private static final int Z_ORDER = Integer.MAX_VALUE;
-
private final DialogAnimationController<RestartDialogLayout> mAnimationController;
private final Transitions mTransitions;
@@ -72,9 +66,6 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
*/
private final int mDialogVerticalMargin;
- @NonNull
- private TaskInfo mTaskInfo;
-
@Nullable
@VisibleForTesting
RestartDialogLayout mLayout;
@@ -100,7 +91,6 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
DialogAnimationController<RestartDialogLayout> animationController,
CompatUIConfiguration compatUIConfiguration) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
- mTaskInfo = taskInfo;
mTransitions = transitions;
mOnDismissCallback = onDismissCallback;
mOnRestartCallback = onRestartCallback;
@@ -112,7 +102,7 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
@Override
protected int getZOrder() {
- return Z_ORDER;
+ return TASK_CHILD_LAYER_COMPAT_UI + 2;
}
@Override
@@ -130,7 +120,7 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
protected boolean eligibleToShowLayout() {
// We don't show this dialog if the user has explicitly selected so clicking on a checkbox.
return mRequestRestartDialog && !isTaskbarEduShowing() && (mLayout != null
- || mCompatUIConfiguration.shouldShowRestartDialogAgain(mTaskInfo));
+ || mCompatUIConfiguration.shouldShowRestartDialogAgain(getLastTaskInfo()));
}
@Override
@@ -148,18 +138,6 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
mRequestRestartDialog = enabled;
}
- @Override
- public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
- boolean canShow) {
- mTaskInfo = taskInfo;
- return super.updateCompatInfo(taskInfo, taskListener, canShow);
- }
-
- boolean needsToBeRecreated(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
- return taskInfo.configuration.uiMode != mTaskInfo.configuration.uiMode
- || !getTaskListener().equals(taskListener);
- }
-
private void updateDialogMargins() {
if (mLayout == null) {
return;
@@ -170,10 +148,10 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
final Rect taskBounds = getTaskBounds();
final Rect taskStableBounds = getTaskStableBounds();
-
- marginParams.topMargin = taskStableBounds.top - taskBounds.top + mDialogVerticalMargin;
- marginParams.bottomMargin =
- taskBounds.bottom - taskStableBounds.bottom + mDialogVerticalMargin;
+ // only update margins based on taskbar insets
+ marginParams.topMargin = mDialogVerticalMargin;
+ marginParams.bottomMargin = taskBounds.bottom - taskStableBounds.bottom
+ + mDialogVerticalMargin;
dialogContainer.setLayoutParams(marginParams);
}
@@ -196,6 +174,7 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
// Dialog has already been released.
return;
}
+ final TaskInfo lastTaskInfo = getLastTaskInfo();
mLayout.setDismissOnClickListener(this::onDismiss);
mLayout.setRestartOnClickListener(dontShowAgain -> {
if (mLayout != null) {
@@ -205,9 +184,9 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
});
}
if (dontShowAgain) {
- mCompatUIConfiguration.setDontShowRestartDialogAgain(mTaskInfo);
+ mCompatUIConfiguration.setDontShowRestartDialogAgain(lastTaskInfo);
}
- mOnRestartCallback.accept(Pair.create(mTaskInfo, getTaskListener()));
+ mOnRestartCallback.accept(Pair.create(lastTaskInfo, getTaskListener()));
});
// Focus on the dialog title for accessibility.
mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
@@ -221,7 +200,7 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
mLayout.setDismissOnClickListener(null);
mAnimationController.startExitAnimation(mLayout, () -> {
release();
- mOnDismissCallback.accept(Pair.create(mTaskInfo, getTaskListener()));
+ mOnDismissCallback.accept(Pair.create(getLastTaskInfo(), getTaskListener()));
});
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index b0756a0afe5d..14daae03e6b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -31,6 +31,7 @@ import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -53,11 +54,11 @@ import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
-import java.util.Optional;
-
import dagger.Module;
import dagger.Provides;
+import java.util.Optional;
+
/**
* Provides TV specific dependencies for Pip.
*/
@@ -70,7 +71,7 @@ public abstract class TvPipModule {
ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
- PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
@@ -83,6 +84,7 @@ public abstract class TvPipModule {
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper windowManagerShellWrapper,
+ @ShellMainThread Handler mainHandler, // needed for registerReceiverForAllUsers()
@ShellMainThread ShellExecutor mainExecutor) {
return Optional.of(
TvPipController.create(
@@ -90,7 +92,7 @@ public abstract class TvPipModule {
shellInit,
shellController,
tvPipBoundsState,
- pipSizeSpecHandler,
+ pipDisplayLayoutState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
pipAppOpsListener,
@@ -103,6 +105,7 @@ public abstract class TvPipModule {
pipParamsChangedForwarder,
displayController,
windowManagerShellWrapper,
+ mainHandler,
mainExecutor));
}
@@ -139,28 +142,36 @@ public abstract class TvPipModule {
@WMSingleton
@Provides
static TvPipBoundsState provideTvPipBoundsState(Context context,
- PipSizeSpecHandler pipSizeSpecHandler) {
- return new TvPipBoundsState(context, pipSizeSpecHandler);
+ PipSizeSpecHandler pipSizeSpecHandler, PipDisplayLayoutState pipDisplayLayoutState) {
+ return new TvPipBoundsState(context, pipSizeSpecHandler, pipDisplayLayoutState);
}
@WMSingleton
@Provides
- static PipSizeSpecHandler providePipSizeSpecHelper(Context context) {
- return new PipSizeSpecHandler(context);
+ static PipSizeSpecHandler providePipSizeSpecHelper(Context context,
+ PipDisplayLayoutState pipDisplayLayoutState) {
+ return new PipSizeSpecHandler(context, pipDisplayLayoutState);
}
// Handler needed for loadDrawableAsync() in PipControlsViewController
@WMSingleton
@Provides
static PipTransitionController provideTvPipTransition(
+ Context context,
ShellInit shellInit,
ShellTaskOrganizer shellTaskOrganizer,
Transitions transitions,
- PipAnimationController pipAnimationController,
+ TvPipBoundsState tvPipBoundsState,
+ PipDisplayLayoutState pipDisplayLayoutState,
+ PipTransitionState pipTransitionState,
+ TvPipMenuController pipMenuController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
- TvPipBoundsState tvPipBoundsState, TvPipMenuController pipMenuController) {
- return new TvPipTransition(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState,
- pipMenuController, tvPipBoundsAlgorithm, pipAnimationController);
+ PipAnimationController pipAnimationController,
+ PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
+ return new TvPipTransition(context, shellInit, shellTaskOrganizer, transitions,
+ tvPipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController,
+ tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
+ Optional.empty());
}
@WMSingleton
@@ -169,22 +180,17 @@ public abstract class TvPipModule {
Context context,
TvPipBoundsState tvPipBoundsState,
SystemWindows systemWindows,
- PipMediaController pipMediaController,
@ShellMainThread Handler mainHandler) {
- return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController,
- mainHandler);
+ return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler);
}
- // Handler needed for registerReceiverForAllUsers()
@WMSingleton
@Provides
static TvPipNotificationController provideTvPipNotificationController(Context context,
PipMediaController pipMediaController,
- PipParamsChangedForwarder pipParamsChangedForwarder,
- TvPipBoundsState tvPipBoundsState,
- @ShellMainThread Handler mainHandler) {
+ PipParamsChangedForwarder pipParamsChangedForwarder) {
return new TvPipNotificationController(context, pipMediaController,
- pipParamsChangedForwarder, tvPipBoundsState, mainHandler);
+ pipParamsChangedForwarder);
}
@WMSingleton
@@ -206,7 +212,7 @@ public abstract class TvPipModule {
TvPipMenuController tvPipMenuController,
SyncTransactionQueue syncTransactionQueue,
TvPipBoundsState tvPipBoundsState,
- PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
PipTransitionState pipTransitionState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipAnimationController pipAnimationController,
@@ -218,7 +224,7 @@ public abstract class TvPipModule {
PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
@ShellMainThread ShellExecutor mainExecutor) {
return new TvPipTaskOrganizer(context,
- syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipSizeSpecHandler,
+ syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState,
tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController,
pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
splitScreenControllerOptional, displayController, pipUiEventLogger,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
index 15bfeb297b41..12d51f54a09c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
@@ -16,20 +16,36 @@
package com.android.wm.shell.dagger;
-import android.view.IWindowManager;
+import android.content.Context;
+import android.os.Handler;
+import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.splitscreen.tv.TvSplitScreenController;
import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
import com.android.wm.shell.startingsurface.tv.TvStartingWindowTypeAlgorithm;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
import dagger.Module;
import dagger.Provides;
+import java.util.Optional;
+
/**
* Provides dependencies from {@link com.android.wm.shell}, these dependencies are only
* accessible from components within the WM subcomponent (can be explicitly exposed to the
@@ -50,5 +66,33 @@ public class TvWMShellModule {
@DynamicOverride
static StartingWindowTypeAlgorithm provideStartingWindowTypeAlgorithm() {
return new TvStartingWindowTypeAlgorithm();
- };
+ }
+
+ @WMSingleton
+ @Provides
+ @DynamicOverride
+ static SplitScreenController provideSplitScreenController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ ShellTaskOrganizer shellTaskOrganizer,
+ SyncTransactionQueue syncQueue,
+ RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+ DisplayController displayController,
+ DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController,
+ Optional<DragAndDropController> dragAndDropController,
+ Transitions transitions,
+ TransactionPool transactionPool,
+ IconProvider iconProvider,
+ Optional<RecentTasksController> recentTasks,
+ @ShellMainThread ShellExecutor mainExecutor,
+ Handler mainHandler,
+ SystemWindows systemWindows) {
+ return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController,
+ shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController,
+ displayImeController, displayInsetsController, dragAndDropController, transitions,
+ transactionPool, iconProvider, recentTasks, mainExecutor, mainHandler,
+ systemWindows);
+ }
}
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 ef21c7e9ec0c..c491fed5d896 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
@@ -32,12 +32,10 @@ import com.android.wm.shell.R;
import com.android.wm.shell.RootDisplayAreaOrganizer;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.TaskViewFactory;
-import com.android.wm.shell.TaskViewFactoryController;
-import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
import com.android.wm.shell.back.BackAnimation;
+import com.android.wm.shell.back.BackAnimationBackground;
import com.android.wm.shell.back.BackAnimationController;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.Bubbles;
@@ -72,6 +70,8 @@ import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.keyguard.KeyguardTransitions;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
@@ -81,6 +81,7 @@ import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.startingsurface.StartingSurface;
@@ -91,6 +92,9 @@ import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.sysui.ShellInterface;
+import com.android.wm.shell.taskview.TaskViewFactory;
+import com.android.wm.shell.taskview.TaskViewFactoryController;
+import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.ShellTransitions;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
@@ -184,15 +188,16 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static DragAndDropController provideDragAndDropController(Context context,
+ static Optional<DragAndDropController> provideDragAndDropController(Context context,
ShellInit shellInit,
ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
DisplayController displayController,
UiEventLogger uiEventLogger,
IconProvider iconProvider,
@ShellMainThread ShellExecutor mainExecutor) {
- return new DragAndDropController(context, shellInit, shellController, displayController,
- uiEventLogger, iconProvider, mainExecutor);
+ return Optional.ofNullable(DragAndDropController.create(context, shellInit, shellController,
+ shellCommandHandler, displayController, uiEventLogger, iconProvider, mainExecutor));
}
@WMSingleton
@@ -283,21 +288,30 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
+ static BackAnimationBackground provideBackAnimationBackground(
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ return new BackAnimationBackground(rootTaskDisplayAreaOrganizer);
+ }
+
+ @WMSingleton
+ @Provides
static Optional<BackAnimationController> provideBackAnimationController(
Context context,
ShellInit shellInit,
ShellController shellController,
@ShellMainThread ShellExecutor shellExecutor,
- @ShellBackgroundThread Handler backgroundHandler
+ @ShellBackgroundThread Handler backgroundHandler,
+ BackAnimationBackground backAnimationBackground
) {
if (BackAnimationController.IS_ENABLED) {
return Optional.of(
new BackAnimationController(shellInit, shellController, shellExecutor,
- backgroundHandler, context));
+ backgroundHandler, context, backAnimationBackground));
}
return Optional.empty();
}
+
//
// Bubbles (optional feature)
//
@@ -510,6 +524,9 @@ public abstract class WMShellBaseModule {
desktopModeTaskRepository, mainExecutor));
}
+ @BindsOptionalOf
+ abstract RecentsTransitionHandler optionalRecentsTransitionHandler();
+
//
// Shell transitions
//
@@ -530,13 +547,16 @@ public abstract class WMShellBaseModule {
DisplayController displayController,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
- @ShellAnimationThread ShellExecutor animExecutor) {
+ @ShellAnimationThread ShellExecutor animExecutor,
+ ShellCommandHandler shellCommandHandler,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
// TODO(b/238217847): Force override shell init if registration is disabled
shellInit = new ShellInit(mainExecutor);
}
return new Transitions(context, shellInit, shellController, organizer, pool,
- displayController, mainExecutor, mainHandler, animExecutor);
+ displayController, mainExecutor, mainHandler, animExecutor, shellCommandHandler,
+ rootTaskDisplayAreaOrganizer);
}
@WMSingleton
@@ -546,6 +566,28 @@ public abstract class WMShellBaseModule {
}
//
+ // Keyguard transitions (optional feature)
+ //
+
+ @WMSingleton
+ @Provides
+ static KeyguardTransitionHandler provideKeyguardTransitionHandler(
+ ShellInit shellInit,
+ Transitions transitions,
+ @ShellMainThread Handler mainHandler,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new KeyguardTransitionHandler(
+ shellInit, transitions, mainHandler, mainExecutor);
+ }
+
+ @WMSingleton
+ @Provides
+ static KeyguardTransitions provideKeyguardTransitions(
+ KeyguardTransitionHandler handler) {
+ return handler.asKeyguardTransitions();
+ }
+
+ //
// Display areas
//
@@ -692,10 +734,11 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static ShellController provideShellController(ShellInit shellInit,
+ static ShellController provideShellController(Context context,
+ ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
@ShellMainThread ShellExecutor mainExecutor) {
- return new ShellController(shellInit, shellCommandHandler, mainExecutor);
+ return new ShellController(context, shellInit, shellCommandHandler, mainExecutor);
}
//
@@ -782,7 +825,7 @@ public abstract class WMShellBaseModule {
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- DragAndDropController dragAndDropController,
+ Optional<DragAndDropController> dragAndDropControllerOptional,
ShellTaskOrganizer shellTaskOrganizer,
Optional<BubbleController> bubblesOptional,
Optional<SplitScreenController> splitScreenOptional,
@@ -793,6 +836,7 @@ public abstract class WMShellBaseModule {
Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
Optional<FreeformComponents> freeformComponents,
Optional<RecentTasksController> recentTasksOptional,
+ Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional,
Optional<OneHandedController> oneHandedControllerOptional,
Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
Optional<ActivityEmbeddingController> activityEmbeddingOptional,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 8f1e074ec183..cff317259f1e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -21,6 +21,7 @@ import android.content.pm.LauncherApps;
import android.os.Handler;
import android.os.UserManager;
import android.view.Choreographer;
+import android.view.IWindowManager;
import android.view.WindowManager;
import com.android.internal.jank.InteractionJankMonitor;
@@ -29,7 +30,6 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.TaskViewTransitions;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleData;
@@ -53,18 +53,21 @@ import com.android.wm.shell.desktopmode.DesktopModeController;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -81,10 +84,12 @@ import com.android.wm.shell.pip.phone.PipMotionHelper;
import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
@@ -100,15 +105,15 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
/**
* Provides dependencies from {@link com.android.wm.shell}, these dependencies are only
* accessible from components within the WM subcomponent (can be explicitly exposed to the
@@ -166,19 +171,20 @@ public abstract class WMShellModule {
BubblePositioner positioner,
DisplayController displayController,
@DynamicOverride Optional<OneHandedController> oneHandedOptional,
- DragAndDropController dragAndDropController,
+ Optional<DragAndDropController> dragAndDropController,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellBackgroundThread ShellExecutor bgExecutor,
TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue) {
+ SyncTransactionQueue syncQueue,
+ IWindowManager wmService) {
return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
null /* synchronizer */, floatingContentCoordinator,
new BubbleDataRepository(context, launcherApps, mainExecutor),
statusBarService, windowManager, windowManagerShellWrapper, userManager,
launcherApps, logger, taskStackListener, organizer, positioner, displayController,
oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
- taskViewTransitions, syncQueue);
+ taskViewTransitions, syncQueue, wmService);
}
//
@@ -194,6 +200,7 @@ public abstract class WMShellModule {
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue,
+ Transitions transitions,
Optional<DesktopModeController> desktopModeController,
Optional<DesktopTasksController> desktopTasksController,
Optional<SplitScreenController> splitScreenController) {
@@ -205,6 +212,7 @@ public abstract class WMShellModule {
taskOrganizer,
displayController,
syncQueue,
+ transitions,
desktopModeController,
desktopTasksController,
splitScreenController);
@@ -314,7 +322,7 @@ public abstract class WMShellModule {
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- DragAndDropController dragAndDropController,
+ Optional<DragAndDropController> dragAndDropController,
Transitions transitions,
TransactionPool transactionPool,
IconProvider iconProvider,
@@ -343,6 +351,7 @@ public abstract class WMShellModule {
PhonePipKeepClearAlgorithm pipKeepClearAlgorithm,
PipBoundsState pipBoundsState,
PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
@@ -360,18 +369,18 @@ public abstract class WMShellModule {
return Optional.ofNullable(PipController.create(
context, shellInit, shellCommandHandler, shellController,
displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm,
- pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, pipMotionHelper,
- pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
- pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
- taskStackListener, pipParamsChangedForwarder, displayInsetsController,
- pipTabletopController, oneHandedController, mainExecutor));
+ pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, pipDisplayLayoutState,
+ pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
+ pipTransitionState, pipTouchHandler, pipTransitionController,
+ windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
+ displayInsetsController, pipTabletopController, oneHandedController, mainExecutor));
}
@WMSingleton
@Provides
static PipBoundsState providePipBoundsState(Context context,
- PipSizeSpecHandler pipSizeSpecHandler) {
- return new PipBoundsState(context, pipSizeSpecHandler);
+ PipSizeSpecHandler pipSizeSpecHandler, PipDisplayLayoutState pipDisplayLayoutState) {
+ return new PipBoundsState(context, pipSizeSpecHandler, pipDisplayLayoutState);
}
@WMSingleton
@@ -388,8 +397,9 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
- static PipSizeSpecHandler providePipSizeSpecHelper(Context context) {
- return new PipSizeSpecHandler(context);
+ static PipSizeSpecHandler providePipSizeSpecHelper(Context context,
+ PipDisplayLayoutState pipDisplayLayoutState) {
+ return new PipSizeSpecHandler(context, pipDisplayLayoutState);
}
@WMSingleton
@@ -446,7 +456,7 @@ public abstract class WMShellModule {
SyncTransactionQueue syncTransactionQueue,
PipTransitionState pipTransitionState,
PipBoundsState pipBoundsState,
- PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
PipBoundsAlgorithm pipBoundsAlgorithm,
PhonePipMenuController menuPhoneController,
PipAnimationController pipAnimationController,
@@ -458,7 +468,7 @@ public abstract class WMShellModule {
PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
@ShellMainThread ShellExecutor mainExecutor) {
return new PipTaskOrganizer(context,
- syncTransactionQueue, pipTransitionState, pipBoundsState, pipSizeSpecHandler,
+ syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState,
pipBoundsAlgorithm, menuPhoneController, pipAnimationController,
pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
splitScreenControllerOptional, displayController, pipUiEventLogger,
@@ -477,12 +487,12 @@ public abstract class WMShellModule {
static PipTransitionController providePipTransitionController(Context context,
ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Transitions transitions,
PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm,
- PipBoundsState pipBoundsState, PipSizeSpecHandler pipSizeSpecHandler,
+ PipBoundsState pipBoundsState, PipDisplayLayoutState pipDisplayLayoutState,
PipTransitionState pipTransitionState, PhonePipMenuController pipMenuController,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
Optional<SplitScreenController> splitScreenOptional) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
- pipBoundsState, pipSizeSpecHandler, pipTransitionState, pipMenuController,
+ pipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController,
pipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
splitScreenOptional);
}
@@ -523,9 +533,23 @@ public abstract class WMShellModule {
ShellInit shellInit,
Optional<SplitScreenController> splitScreenOptional,
Optional<PipTouchHandler> pipTouchHandlerOptional,
+ Optional<RecentsTransitionHandler> recentsTransitionHandler,
+ KeyguardTransitionHandler keyguardTransitionHandler,
+ Optional<UnfoldTransitionHandler> unfoldHandler,
Transitions transitions) {
return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional,
- pipTouchHandlerOptional);
+ pipTouchHandlerOptional, recentsTransitionHandler, keyguardTransitionHandler,
+ unfoldHandler);
+ }
+
+ @WMSingleton
+ @Provides
+ static RecentsTransitionHandler provideRecentsTransitionHandler(
+ ShellInit shellInit,
+ Transitions transitions,
+ Optional<RecentTasksController> recentTasksController) {
+ return new RecentsTransitionHandler(shellInit, transitions,
+ recentTasksController.orElse(null));
}
//
@@ -615,14 +639,8 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
- static UnfoldBackgroundController provideUnfoldBackgroundController(
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- Context context
- ) {
- return new UnfoldBackgroundController(
- context,
- rootTaskDisplayAreaOrganizer
- );
+ static UnfoldBackgroundController provideUnfoldBackgroundController(Context context) {
+ return new UnfoldBackgroundController(context);
}
//
@@ -654,42 +672,43 @@ public abstract class WMShellModule {
Context context,
ShellInit shellInit,
ShellController shellController,
+ DisplayController displayController,
ShellTaskOrganizer shellTaskOrganizer,
+ SyncTransactionQueue syncQueue,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
Transitions transitions,
+ EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler,
+ ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler,
@DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
@ShellMainThread ShellExecutor mainExecutor
) {
- return new DesktopTasksController(context, shellInit, shellController, shellTaskOrganizer,
- transitions, desktopModeTaskRepository, mainExecutor);
+ return new DesktopTasksController(context, shellInit, shellController, displayController,
+ shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions,
+ enterDesktopTransitionHandler, exitDesktopTransitionHandler,
+ desktopModeTaskRepository, mainExecutor);
}
@WMSingleton
@Provides
- @DynamicOverride
- static DesktopModeTaskRepository provideDesktopModeTaskRepository() {
- return new DesktopModeTaskRepository();
+ static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
+ Transitions transitions) {
+ return new EnterDesktopTaskTransitionHandler(transitions);
}
- //
- // Kids mode
- //
@WMSingleton
@Provides
- static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
- Context context,
- ShellInit shellInit,
- ShellCommandHandler shellCommandHandler,
- SyncTransactionQueue syncTransactionQueue,
- DisplayController displayController,
- DisplayInsetsController displayInsetsController,
- Optional<UnfoldAnimationController> unfoldAnimationController,
- Optional<RecentTasksController> recentTasksOptional,
- @ShellMainThread ShellExecutor mainExecutor,
- @ShellMainThread Handler mainHandler
+ static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler(
+ Transitions transitions,
+ Context context
) {
- return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
- syncTransactionQueue, displayController, displayInsetsController,
- unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
+ return new ExitDesktopTaskTransitionHandler(transitions, context);
+ }
+
+ @WMSingleton
+ @Provides
+ @DynamicOverride
+ static DesktopModeTaskRepository provideDesktopModeTaskRepository() {
+ return new DesktopModeTaskRepository();
}
//
@@ -703,7 +722,6 @@ public abstract class WMShellModule {
@Provides
static Object provideIndependentShellComponentsToCreate(
DefaultMixedHandler defaultMixedHandler,
- KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<DesktopModeController> desktopModeController) {
return new Object();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index cbd544cc4b86..e732a0354806 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -16,9 +16,12 @@
package com.android.wm.shell.desktopmode;
+import android.graphics.Region;
+
import com.android.wm.shell.common.annotations.ExternalThread;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Interface to interact with desktop mode feature in shell.
@@ -32,7 +35,16 @@ public interface DesktopMode {
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
- void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+ void addVisibleTasksListener(DesktopModeTaskRepository.VisibleTasksListener listener,
Executor callbackExecutor);
+ /**
+ * Adds a consumer to listen for Desktop task corner changes. This is used for gesture
+ * exclusion. The SparseArray contains a list of four corner resize handles mapped to each
+ * desktop task's taskId. The resize handle Rects are stored in the following order:
+ * left-top, left-bottom, right-top, right-bottom.
+ */
+ default void addDesktopGestureExclusionRegionListener(Consumer<Region> listener,
+ Executor callbackExecutor) { }
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index bc8171058776..b9d2be280efb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -34,6 +34,7 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.app.WindowConfiguration;
import android.content.Context;
import android.database.ContentObserver;
+import android.graphics.Region;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
@@ -69,6 +70,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Handles windowing changes when desktop mode system setting changes
@@ -149,11 +151,21 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
- public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+ public void addVisibleTasksListener(DesktopModeTaskRepository.VisibleTasksListener listener,
Executor callbackExecutor) {
mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor);
}
+ /**
+ * Adds a listener to track changes to corners of desktop mode tasks.
+ * @param listener the listener to add.
+ * @param callbackExecutor the executor to call the listener on.
+ */
+ public void addTaskCornerListener(Consumer<Region> listener,
+ Executor callbackExecutor) {
+ mDesktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor);
+ }
+
@VisibleForTesting
void updateDesktopModeActive(boolean active) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
@@ -252,10 +264,10 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
/**
* Show apps on desktop
*/
- void showDesktopApps() {
+ void showDesktopApps(int displayId) {
// Bring apps to front, ignoring their visibility status to always ensure they are on top.
WindowContainerTransaction wct = new WindowContainerTransaction();
- bringDesktopAppsToFront(wct);
+ bringDesktopAppsToFront(displayId, wct);
if (!wct.isEmpty()) {
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -268,12 +280,12 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
}
/** Get number of tasks that are marked as visible */
- int getVisibleTaskCount() {
- return mDesktopModeTaskRepository.getVisibleTaskCount();
+ int getVisibleTaskCount(int displayId) {
+ return mDesktopModeTaskRepository.getVisibleTaskCount(displayId);
}
- private void bringDesktopAppsToFront(WindowContainerTransaction wct) {
- final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
+ private void bringDesktopAppsToFront(int displayId, WindowContainerTransaction wct) {
+ final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks(displayId);
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
final List<RunningTaskInfo> taskInfos = new ArrayList<>();
@@ -312,6 +324,37 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
}
/**
+ * Update corner rects stored for a specific task
+ * @param taskId task to update
+ * @param taskCorners task's new corner handles
+ */
+ public void onTaskCornersChanged(int taskId, Region taskCorners) {
+ mDesktopModeTaskRepository.updateTaskCorners(taskId, taskCorners);
+ }
+
+ /**
+ * Remove corners saved for a task. Likely used due to task closure.
+ * @param taskId task to remove
+ */
+ public void removeCornersForTask(int taskId) {
+ mDesktopModeTaskRepository.removeTaskCorners(taskId);
+ }
+
+ /**
+ * Moves a specifc task to the front.
+ * @param taskInfo the task to show in front.
+ */
+ public void moveTaskToFront(RunningTaskInfo taskInfo) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(taskInfo.token, true /* onTop */);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null);
+ } else {
+ mShellTaskOrganizer.applyTransaction(wct);
+ }
+ }
+
+ /**
* Turn desktop mode on or off
* @param active the desired state for desktop mode setting
*/
@@ -343,6 +386,7 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
+ RunningTaskInfo triggerTask = request.getTriggerTask();
// Only do anything if we are in desktop mode and opening/moving-to-front a task/app in
// freeform
if (!DesktopModeStatus.isActive(mContext)) {
@@ -356,19 +400,15 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
WindowManager.transitTypeToString(request.getType()));
return null;
}
- if (request.getTriggerTask() == null
- || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+ if (triggerTask == null || triggerTask.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task");
return null;
}
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request);
- WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this);
- if (wct == null) {
- wct = new WindowContainerTransaction();
- }
- bringDesktopAppsToFront(wct);
- wct.reorder(request.getTriggerTask().token, true /* onTop */);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ bringDesktopAppsToFront(triggerTask.displayId, wct);
+ wct.reorder(triggerTask.token, true /* onTop */);
return wct;
}
@@ -415,10 +455,19 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
private final class DesktopModeImpl implements DesktopMode {
@Override
- public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+ public void addVisibleTasksListener(
+ DesktopModeTaskRepository.VisibleTasksListener listener,
+ Executor callbackExecutor) {
+ mMainExecutor.execute(() -> {
+ DesktopModeController.this.addVisibleTasksListener(listener, callbackExecutor);
+ });
+ }
+
+ @Override
+ public void addDesktopGestureExclusionRegionListener(Consumer<Region> listener,
Executor callbackExecutor) {
mMainExecutor.execute(() -> {
- DesktopModeController.this.addListener(listener, callbackExecutor);
+ DesktopModeController.this.addTaskCornerListener(listener, callbackExecutor);
});
}
}
@@ -444,16 +493,17 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
mController = null;
}
- public void showDesktopApps() {
+ @Override
+ public void showDesktopApps(int displayId) {
executeRemoteCallWithTaskPermission(mController, "showDesktopApps",
- DesktopModeController::showDesktopApps);
+ controller -> controller.showDesktopApps(displayId));
}
@Override
- public int getVisibleTaskCount() throws RemoteException {
+ public int getVisibleTaskCount(int displayId) throws RemoteException {
int[] result = new int[1];
executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount",
- controller -> result[0] = controller.getVisibleTaskCount(),
+ controller -> result[0] = controller.getVisibleTaskCount(displayId),
true /* blocking */
);
return result[0];
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 055949fd8c89..76ca68bbfa75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -43,6 +43,18 @@ public class DesktopModeStatus {
"persist.wm.debug.desktop_mode_2", false);
/**
+ * Flag to indicate whether task resizing is veiled.
+ */
+ private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_veiled_resizing", true);
+
+ /**
+ * Flag to indicate is moving task to another display is enabled.
+ */
+ public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_change_display", false);
+
+ /**
* Return {@code true} if desktop mode support is enabled
*/
public static boolean isProto1Enabled() {
@@ -65,6 +77,13 @@ public class DesktopModeStatus {
}
/**
+ * Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
+ */
+ public static boolean isVeiledResizeEnabled() {
+ return IS_VEILED_RESIZE_ENABLED;
+ }
+
+ /**
* Check if desktop mode is active
*
* @return {@code true} if active
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 935a588475af..a7a1991c94d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -16,32 +16,61 @@
package com.android.wm.shell.desktopmode
+import android.graphics.Region
import android.util.ArrayMap
import android.util.ArraySet
+import android.util.SparseArray
+import androidx.core.util.forEach
+import androidx.core.util.keyIterator
+import androidx.core.util.valueIterator
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.util.KtProtoLog
import java.util.concurrent.Executor
+import java.util.function.Consumer
/**
* Keeps track of task data related to desktop mode.
*/
class DesktopModeTaskRepository {
- /**
- * Set of task ids that are marked as active in desktop mode.
- * Active tasks in desktop mode are freeform tasks that are visible or have been visible after
- * desktop mode was activated.
- * Task gets removed from this list when it vanishes. Or when desktop mode is turned off.
- */
- private val activeTasks = ArraySet<Int>()
- private val visibleTasks = ArraySet<Int>()
+ /** Task data that is tracked per display */
+ private data class DisplayData(
+ /**
+ * Set of task ids that are marked as active in desktop mode. Active tasks in desktop mode
+ * are freeform tasks that are visible or have been visible after desktop mode was
+ * activated. Task gets removed from this list when it vanishes. Or when desktop mode is
+ * turned off.
+ */
+ val activeTasks: ArraySet<Int> = ArraySet(),
+ val visibleTasks: ArraySet<Int> = ArraySet(),
+ )
+
// Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
private val freeformTasksInZOrder = mutableListOf<Int>()
private val activeTasksListeners = ArraySet<ActiveTasksListener>()
// Track visible tasks separately because a task may be part of the desktop but not visible.
private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
+ // Track corners of desktop tasks, used to determine gesture exclusion
+ private val desktopCorners = SparseArray<Region>()
+ private var desktopGestureExclusionListener: Consumer<Region>? = null
+ private var desktopGestureExclusionExecutor: Executor? = null
- /**
- * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository.
- */
+ private val displayData =
+ object : SparseArray<DisplayData>() {
+ /**
+ * Get the [DisplayData] associated with this [displayId]
+ *
+ * Creates a new instance if one does not exist
+ */
+ fun getOrCreate(displayId: Int): DisplayData {
+ if (!contains(displayId)) {
+ put(displayId, DisplayData())
+ }
+ return get(displayId)
+ }
+ }
+
+ /** Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository. */
fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) {
activeTasksListeners.add(activeTasksListener)
}
@@ -49,10 +78,39 @@ class DesktopModeTaskRepository {
/**
* Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not.
*/
- fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
- visibleTasksListeners.put(visibleTasksListener, executor)
- executor.execute(
- Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) })
+ fun addVisibleTasksListener(
+ visibleTasksListener: VisibleTasksListener,
+ executor: Executor
+ ) {
+ visibleTasksListeners[visibleTasksListener] = executor
+ displayData.keyIterator().forEach { displayId ->
+ val visibleTasks = getVisibleTaskCount(displayId)
+ executor.execute {
+ visibleTasksListener.onVisibilityChanged(displayId, visibleTasks > 0)
+ }
+ }
+ }
+
+ /**
+ * Add a Consumer which will inform other classes of changes to corners for all Desktop tasks.
+ */
+ fun setTaskCornerListener(cornersListener: Consumer<Region>, executor: Executor) {
+ desktopGestureExclusionListener = cornersListener
+ desktopGestureExclusionExecutor = executor
+ executor.execute {
+ desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
+ }
+ }
+
+ /**
+ * Create a new merged region representative of all corners in all desktop tasks.
+ */
+ private fun calculateDesktopExclusionRegion(): Region {
+ val desktopCornersRegion = Region()
+ desktopCorners.valueIterator().forEach { taskCorners ->
+ desktopCornersRegion.op(taskCorners, Region.Op.UNION)
+ }
+ return desktopCornersRegion
}
/**
@@ -70,14 +128,27 @@ class DesktopModeTaskRepository {
}
/**
- * Mark a task with given [taskId] as active.
+ * Mark a task with given [taskId] as active on given [displayId]
*
- * @return `true` if the task was not active
+ * @return `true` if the task was not active on given [displayId]
*/
- fun addActiveTask(taskId: Int): Boolean {
- val added = activeTasks.add(taskId)
+ fun addActiveTask(displayId: Int, taskId: Int): Boolean {
+ // Check if task is active on another display, if so, remove it
+ displayData.forEach { id, data ->
+ if (id != displayId && data.activeTasks.remove(taskId)) {
+ activeTasksListeners.onEach { it.onActiveTasksChanged(id) }
+ }
+ }
+
+ val added = displayData.getOrCreate(displayId).activeTasks.add(taskId)
if (added) {
- activeTasksListeners.onEach { it.onActiveTasksChanged() }
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: add active task=%d displayId=%d",
+ taskId,
+ displayId
+ )
+ activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) }
}
return added
}
@@ -88,71 +159,118 @@ class DesktopModeTaskRepository {
* @return `true` if the task was active
*/
fun removeActiveTask(taskId: Int): Boolean {
- val removed = activeTasks.remove(taskId)
- if (removed) {
- activeTasksListeners.onEach { it.onActiveTasksChanged() }
+ var result = false
+ displayData.forEach { displayId, data ->
+ if (data.activeTasks.remove(taskId)) {
+ activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) }
+ result = true
+ }
}
- return removed
+ if (result) {
+ KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove active task=%d", taskId)
+ }
+ return result
}
/**
* Check if a task with the given [taskId] was marked as an active task
*/
fun isActiveTask(taskId: Int): Boolean {
- return activeTasks.contains(taskId)
+ return displayData.valueIterator().asSequence().any { data ->
+ data.activeTasks.contains(taskId)
+ }
}
/**
* Whether a task is visible.
*/
fun isVisibleTask(taskId: Int): Boolean {
- return visibleTasks.contains(taskId)
+ return displayData.valueIterator().asSequence().any { data ->
+ data.visibleTasks.contains(taskId)
+ }
}
/**
- * Get a set of the active tasks
+ * Get a set of the active tasks for given [displayId]
*/
- fun getActiveTasks(): ArraySet<Int> {
- return ArraySet(activeTasks)
+ fun getActiveTasks(displayId: Int): ArraySet<Int> {
+ return ArraySet(displayData[displayId]?.activeTasks)
}
/**
* Get a list of freeform tasks, ordered from top-bottom (top at index 0).
*/
+ // TODO(b/278084491): pass in display id
fun getFreeformTasksInZOrder(): List<Int> {
return freeformTasksInZOrder
}
/**
* Updates whether a freeform task with this id is visible or not and notifies listeners.
+ *
+ * If the task was visible on a different display with a different displayId, it is removed from
+ * the set of visible tasks on that display. Listeners will be notified.
*/
- fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) {
- val prevCount: Int = visibleTasks.size
+ fun updateVisibleFreeformTasks(displayId: Int, taskId: Int, visible: Boolean) {
if (visible) {
- visibleTasks.add(taskId)
+ // Task is visible. Check if we need to remove it from any other display.
+ val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId }
+ for (otherDisplayId in otherDisplays) {
+ if (displayData[otherDisplayId].visibleTasks.remove(taskId)) {
+ // Task removed from other display, check if we should notify listeners
+ if (displayData[otherDisplayId].visibleTasks.isEmpty()) {
+ notifyVisibleTaskListeners(otherDisplayId, hasVisibleFreeformTasks = false)
+ }
+ }
+ }
+ }
+
+ val prevCount = getVisibleTaskCount(displayId)
+ if (visible) {
+ displayData.getOrCreate(displayId).visibleTasks.add(taskId)
} else {
- visibleTasks.remove(taskId)
+ displayData[displayId]?.visibleTasks?.remove(taskId)
}
- if (prevCount == 0 && visibleTasks.size == 1 ||
- prevCount > 0 && visibleTasks.size == 0) {
- for ((listener, executor) in visibleTasksListeners) {
- executor.execute(
- Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) })
- }
+ val newCount = getVisibleTaskCount(displayId)
+
+ if (prevCount != newCount) {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: update task visibility taskId=%d visible=%b displayId=%d",
+ taskId,
+ visible,
+ displayId
+ )
+ }
+
+ // Check if count changed and if there was no tasks or this is the first task
+ if (prevCount != newCount && (prevCount == 0 || newCount == 0)) {
+ notifyVisibleTaskListeners(displayId, newCount > 0)
+ }
+ }
+
+ private fun notifyVisibleTaskListeners(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+ visibleTasksListeners.forEach { (listener, executor) ->
+ executor.execute { listener.onVisibilityChanged(displayId, hasVisibleFreeformTasks) }
}
}
/**
- * Get number of tasks that are marked as visible
+ * Get number of tasks that are marked as visible on given [displayId]
*/
- fun getVisibleTaskCount(): Int {
- return visibleTasks.size
+ fun getVisibleTaskCount(displayId: Int): Int {
+ return displayData[displayId]?.visibleTasks?.size ?: 0
}
/**
* Add (or move if it already exists) the task to the top of the ordered list.
*/
fun addOrMoveFreeformTaskToTop(taskId: Int) {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: add or move task to top taskId=%d",
+ taskId
+ )
if (freeformTasksInZOrder.contains(taskId)) {
freeformTasksInZOrder.remove(taskId)
}
@@ -163,17 +281,44 @@ class DesktopModeTaskRepository {
* Remove the task from the ordered list.
*/
fun removeFreeformTask(taskId: Int) {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: remove freeform task from ordered list taskId=%d",
+ taskId
+ )
freeformTasksInZOrder.remove(taskId)
}
/**
+ * Updates the active desktop corners; if desktopCorners has been accepted by
+ * desktopCornersListener, it will be updated in the appropriate classes.
+ */
+ fun updateTaskCorners(taskId: Int, taskCorners: Region) {
+ desktopCorners.put(taskId, taskCorners)
+ desktopGestureExclusionExecutor?.execute {
+ desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
+ }
+ }
+
+ /**
+ * Removes the active desktop corners for the specified task; if desktopCorners has been
+ * accepted by desktopCornersListener, it will be updated in the appropriate classes.
+ */
+ fun removeTaskCorners(taskId: Int) {
+ desktopCorners.delete(taskId)
+ desktopGestureExclusionExecutor?.execute {
+ desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
+ }
+ }
+
+ /**
* Defines interface for classes that can listen to changes for active tasks in desktop mode.
*/
interface ActiveTasksListener {
/**
* Called when the active tasks change in desktop mode.
*/
- fun onActiveTasksChanged() {}
+ fun onActiveTasksChanged(displayId: Int) {}
}
/**
@@ -183,6 +328,6 @@ class DesktopModeTaskRepository {
/**
* Called when the desktop starts or stops showing freeform tasks.
*/
- fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {}
+ fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
new file mode 100644
index 000000000000..0f0d572f8eae
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -0,0 +1,330 @@
+/*
+ * 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.desktopmode;
+
+import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Animated visual indicator for Desktop Mode windowing transitions.
+ */
+public class DesktopModeVisualIndicator {
+
+ private final Context mContext;
+ private final DisplayController mDisplayController;
+ private final ShellTaskOrganizer mTaskOrganizer;
+ private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer;
+ private final ActivityManager.RunningTaskInfo mTaskInfo;
+ private final SurfaceControl mTaskSurface;
+ private SurfaceControl mLeash;
+
+ private final SyncTransactionQueue mSyncQueue;
+ private SurfaceControlViewHost mViewHost;
+
+ private View mView;
+ private boolean mIsFullscreen;
+
+ public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue,
+ ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController,
+ Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer,
+ RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer) {
+ mSyncQueue = syncQueue;
+ mTaskInfo = taskInfo;
+ mDisplayController = displayController;
+ mContext = context;
+ mTaskSurface = taskSurface;
+ mTaskOrganizer = taskOrganizer;
+ mRootTdaOrganizer = taskDisplayAreaOrganizer;
+ createView();
+ }
+
+ /**
+ * Create a fullscreen indicator with no animation
+ */
+ private void createView() {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ final Resources resources = mContext.getResources();
+ final DisplayMetrics metrics = resources.getDisplayMetrics();
+ final int screenWidth = metrics.widthPixels;
+ final int screenHeight = metrics.heightPixels;
+ mView = new View(mContext);
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder();
+ mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder);
+ mLeash = builder
+ .setName("Fullscreen Indicator")
+ .setContainerLayer()
+ .build();
+ t.show(mLeash);
+ final WindowManager.LayoutParams lp =
+ new WindowManager.LayoutParams(screenWidth, screenHeight,
+ WindowManager.LayoutParams.TYPE_APPLICATION,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
+ lp.setTitle("Fullscreen indicator for Task=" + mTaskInfo.taskId);
+ lp.setTrustedOverlay();
+ final WindowlessWindowManager windowManager = new WindowlessWindowManager(
+ mTaskInfo.configuration, mLeash,
+ null /* hostInputToken */);
+ mViewHost = new SurfaceControlViewHost(mContext,
+ mDisplayController.getDisplay(mTaskInfo.displayId), windowManager,
+ "FullscreenVisualIndicator");
+ mViewHost.setView(mView, lp);
+ // We want this indicator to be behind the dragged task, but in front of all others.
+ t.setRelativeLayer(mLeash, mTaskSurface, -1);
+
+ mSyncQueue.runInSync(transaction -> {
+ transaction.merge(t);
+ t.close();
+ });
+ }
+
+ /**
+ * Create fullscreen indicator and fades it in.
+ */
+ public void createFullscreenIndicator() {
+ mIsFullscreen = true;
+ mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background);
+ final VisualIndicatorAnimator animator = VisualIndicatorAnimator.toFullscreenAnimator(
+ mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId));
+ animator.start();
+ }
+
+ /**
+ * Create a fullscreen indicator. Animator fades it in while expanding the bounds outwards.
+ */
+ public void createFullscreenIndicatorWithAnimatedBounds() {
+ mIsFullscreen = true;
+ mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background);
+ final VisualIndicatorAnimator animator = VisualIndicatorAnimator
+ .toFullscreenAnimatorWithAnimatedBounds(mView,
+ mDisplayController.getDisplayLayout(mTaskInfo.displayId));
+ animator.start();
+ }
+
+ /**
+ * Takes existing fullscreen indicator and animates it to freeform bounds
+ */
+ public void transitionFullscreenIndicatorToFreeform() {
+ mIsFullscreen = false;
+ final VisualIndicatorAnimator animator = VisualIndicatorAnimator.toFreeformAnimator(
+ mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId));
+ animator.start();
+ }
+
+ /**
+ * Takes the existing freeform indicator and animates it to fullscreen
+ */
+ public void transitionFreeformIndicatorToFullscreen() {
+ mIsFullscreen = true;
+ final VisualIndicatorAnimator animator =
+ VisualIndicatorAnimator.toFullscreenAnimatorWithAnimatedBounds(
+ mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId));
+ animator.start();
+ }
+
+ /**
+ * Release the indicator and its components when it is no longer needed.
+ */
+ public void releaseVisualIndicator(SurfaceControl.Transaction t) {
+ if (mViewHost == null) return;
+ if (mViewHost != null) {
+ mViewHost.release();
+ mViewHost = null;
+ }
+
+ if (mLeash != null) {
+ t.remove(mLeash);
+ mLeash = null;
+ }
+ }
+
+ /**
+ * Returns true if visual indicator is fullscreen
+ */
+ public boolean isFullscreen() {
+ return mIsFullscreen;
+ }
+
+ /**
+ * Animator for Desktop Mode transitions which supports bounds and alpha animation.
+ */
+ private static class VisualIndicatorAnimator extends ValueAnimator {
+ private static final int FULLSCREEN_INDICATOR_DURATION = 200;
+ private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f;
+ private static final float INDICATOR_FINAL_OPACITY = 0.7f;
+
+ private final View mView;
+ private final Rect mStartBounds;
+ private final Rect mEndBounds;
+ private final RectEvaluator mRectEvaluator;
+
+ private VisualIndicatorAnimator(View view, Rect startBounds,
+ Rect endBounds) {
+ mView = view;
+ mStartBounds = new Rect(startBounds);
+ mEndBounds = endBounds;
+ setFloatValues(0, 1);
+ mRectEvaluator = new RectEvaluator(new Rect());
+ }
+
+ /**
+ * Create animator for visual indicator of fullscreen transition
+ *
+ * @param view the view for this indicator
+ * @param displayLayout information about the display the transitioning task is currently on
+ */
+ public static VisualIndicatorAnimator toFullscreenAnimator(@NonNull View view,
+ @NonNull DisplayLayout displayLayout) {
+ final Rect bounds = getMaxBounds(displayLayout);
+ final VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
+ view, bounds, bounds);
+ animator.setInterpolator(new DecelerateInterpolator());
+ setupIndicatorAnimation(animator);
+ return animator;
+ }
+
+
+ /**
+ * Create animator for visual indicator of fullscreen transition
+ *
+ * @param view the view for this indicator
+ * @param displayLayout information about the display the transitioning task is currently on
+ */
+ public static VisualIndicatorAnimator toFullscreenAnimatorWithAnimatedBounds(
+ @NonNull View view, @NonNull DisplayLayout displayLayout) {
+ final int padding = displayLayout.stableInsets().top;
+ Rect startBounds = new Rect(padding, padding,
+ displayLayout.width() - padding, displayLayout.height() - padding);
+ view.getBackground().setBounds(startBounds);
+
+ final VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
+ view, startBounds, getMaxBounds(displayLayout));
+ animator.setInterpolator(new DecelerateInterpolator());
+ setupIndicatorAnimation(animator);
+ return animator;
+ }
+
+ /**
+ * Create animator for visual indicator of freeform transition
+ *
+ * @param view the view for this indicator
+ * @param displayLayout information about the display the transitioning task is currently on
+ */
+ public static VisualIndicatorAnimator toFreeformAnimator(@NonNull View view,
+ @NonNull DisplayLayout displayLayout) {
+ final float adjustmentPercentage = 1f - FINAL_FREEFORM_SCALE;
+ final int width = displayLayout.width();
+ final int height = displayLayout.height();
+ Rect endBounds = new Rect((int) (adjustmentPercentage * width / 2),
+ (int) (adjustmentPercentage * height / 2),
+ (int) (displayLayout.width() - (adjustmentPercentage * width / 2)),
+ (int) (displayLayout.height() - (adjustmentPercentage * height / 2)));
+ final VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
+ view, getMaxBounds(displayLayout), endBounds);
+ animator.setInterpolator(new DecelerateInterpolator());
+ setupIndicatorAnimation(animator);
+ return animator;
+ }
+
+ /**
+ * Add necessary listener for animation of indicator
+ */
+ private static void setupIndicatorAnimation(@NonNull VisualIndicatorAnimator animator) {
+ animator.addUpdateListener(a -> {
+ if (animator.mView != null) {
+ animator.updateBounds(a.getAnimatedFraction(), animator.mView);
+ animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView);
+ } else {
+ animator.cancel();
+ }
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ animator.mView.getBackground().setBounds(animator.mEndBounds);
+ }
+ });
+ animator.setDuration(FULLSCREEN_INDICATOR_DURATION);
+ }
+
+ /**
+ * Update bounds of view based on current animation fraction.
+ * Use of delta is to animate bounds independently, in case we need to
+ * run multiple animations simultaneously.
+ *
+ * @param fraction fraction to use, compared against previous fraction
+ * @param view the view to update
+ */
+ private void updateBounds(float fraction, View view) {
+ if (mStartBounds.equals(mEndBounds)) {
+ return;
+ }
+ Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds);
+ view.getBackground().setBounds(currentBounds);
+ }
+
+ /**
+ * Fade in the fullscreen indicator
+ *
+ * @param fraction current animation fraction
+ */
+ private void updateIndicatorAlpha(float fraction, View view) {
+ view.setAlpha(fraction * INDICATOR_FINAL_OPACITY);
+ }
+
+ /**
+ * Return the max bounds of a fullscreen indicator
+ */
+ private static Rect getMaxBounds(@NonNull DisplayLayout displayLayout) {
+ final int padding = displayLayout.stableInsets().top;
+ final int width = displayLayout.width() - 2 * padding;
+ final int height = displayLayout.height() - 2 * padding;
+ Rect endBounds = new Rect((int) (padding
+ - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * width)),
+ (int) (padding
+ - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * height)),
+ (int) (displayLayout.width() - padding
+ + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * width)),
+ (int) (displayLayout.height() - padding
+ + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * height)));
+ return endBounds;
+ }
+ }
+}
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 31c5e33f21e3..91bb155d9d01 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
@@ -16,7 +16,7 @@
package com.android.wm.shell.desktopmode
-import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -24,6 +24,9 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.Region
import android.os.IBinder
import android.os.SystemProperties
import android.view.SurfaceControl
@@ -36,12 +39,14 @@ import android.window.TransitionRequestInfo
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
-import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.ExecutorUtils
import com.android.wm.shell.common.ExternalInterfaceBinder
import com.android.wm.shell.common.RemoteCallable
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.common.annotations.ExternalThread
import com.android.wm.shell.common.annotations.ShellMainThread
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
@@ -50,21 +55,33 @@ import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.ShellSharedConstants
import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.KtProtoLog
import java.util.concurrent.Executor
import java.util.function.Consumer
/** Handles moving tasks in and out of desktop */
class DesktopTasksController(
- private val context: Context,
- shellInit: ShellInit,
- private val shellController: ShellController,
- private val shellTaskOrganizer: ShellTaskOrganizer,
- private val transitions: Transitions,
- private val desktopModeTaskRepository: DesktopModeTaskRepository,
- @ShellMainThread private val mainExecutor: ShellExecutor
+ private val context: Context,
+ shellInit: ShellInit,
+ private val shellController: ShellController,
+ private val displayController: DisplayController,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val syncQueue: SyncTransactionQueue,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val transitions: Transitions,
+ private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
+ private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
+ private val desktopModeTaskRepository: DesktopModeTaskRepository,
+ @ShellMainThread private val mainExecutor: ShellExecutor
) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
private val desktopMode: DesktopModeImpl
+ private var visualIndicator: DesktopModeVisualIndicator? = null
+ private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> {
+ t: SurfaceControl.Transaction ->
+ visualIndicator?.releaseVisualIndicator(t)
+ visualIndicator = null
+ }
init {
desktopMode = DesktopModeImpl()
@@ -74,7 +91,7 @@ class DesktopTasksController(
}
private fun onInit() {
- ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
+ KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
shellController.addExternalInterface(
ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE,
{ createExternalInterface() },
@@ -84,10 +101,11 @@ class DesktopTasksController(
}
/** Show all tasks, that are part of the desktop, on top of launcher */
- fun showDesktopApps() {
- ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps")
+ fun showDesktopApps(displayId: Int) {
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps")
val wct = WindowContainerTransaction()
- bringDesktopAppsToFront(wct)
+ // TODO(b/278084491): pass in display id
+ bringDesktopAppsToFront(displayId, wct)
// Execute transaction if there are pending operations
if (!wct.isEmpty) {
@@ -101,8 +119,8 @@ class DesktopTasksController(
}
/** Get number of tasks that are marked as visible */
- fun getVisibleTaskCount(): Int {
- return desktopModeTaskRepository.getVisibleTaskCount()
+ fun getVisibleTaskCount(displayId: Int): Int {
+ return desktopModeTaskRepository.getVisibleTaskCount(displayId)
}
/** Move a task with given `taskId` to desktop */
@@ -111,12 +129,15 @@ class DesktopTasksController(
}
/** Move a task to desktop */
- fun moveToDesktop(task: ActivityManager.RunningTaskInfo) {
- ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDesktop: %d", task.taskId)
-
+ fun moveToDesktop(task: RunningTaskInfo) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveToDesktop taskId=%d",
+ task.taskId
+ )
val wct = WindowContainerTransaction()
// Bring other apps to front first
- bringDesktopAppsToFront(wct)
+ bringDesktopAppsToFront(task.displayId, wct)
addMoveToDesktopChanges(wct, task.token)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
@@ -125,14 +146,62 @@ class DesktopTasksController(
}
}
+ /**
+ * Moves a single task to freeform and sets the taskBounds to the passed in bounds,
+ * startBounds
+ */
+ fun moveToFreeform(taskInfo: RunningTaskInfo, startBounds: Rect) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveToFreeform with bounds taskId=%d",
+ taskInfo.taskId
+ )
+ val wct = WindowContainerTransaction()
+ moveHomeTaskToFront(wct)
+ addMoveToDesktopChanges(wct, taskInfo.getToken())
+ wct.setBounds(taskInfo.token, startBounds)
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ enterDesktopTaskTransitionHandler.startTransition(
+ Transitions.TRANSIT_ENTER_FREEFORM, wct, mOnAnimationFinishedCallback)
+ } else {
+ shellTaskOrganizer.applyTransaction(wct)
+ }
+ }
+
+ /** Brings apps to front and sets freeform task bounds */
+ private fun moveToDesktopWithAnimation(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveToDesktop with animation taskId=%d",
+ taskInfo.taskId
+ )
+ val wct = WindowContainerTransaction()
+ bringDesktopAppsToFront(taskInfo.displayId, wct)
+ addMoveToDesktopChanges(wct, taskInfo.getToken())
+ wct.setBounds(taskInfo.token, freeformBounds)
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ enterDesktopTaskTransitionHandler.startTransition(
+ Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct, mOnAnimationFinishedCallback)
+ } else {
+ shellTaskOrganizer.applyTransaction(wct)
+ releaseVisualIndicator()
+ }
+ }
+
/** 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: ActivityManager.RunningTaskInfo) {
- ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId)
+ fun moveToFullscreen(task: RunningTaskInfo) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveToFullscreen taskId=%d",
+ task.taskId
+ )
val wct = WindowContainerTransaction()
addMoveToFullscreenChanges(wct, task.token)
@@ -144,6 +213,125 @@ class DesktopTasksController(
}
/**
+ * Move a task to fullscreen after being dragged from fullscreen and released back into
+ * status bar area
+ */
+ fun cancelMoveToFreeform(task: RunningTaskInfo, position: Point) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: cancelMoveToFreeform taskId=%d",
+ task.taskId
+ )
+ val wct = WindowContainerTransaction()
+ addMoveToFullscreenChanges(wct, task.token)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(wct, position,
+ mOnAnimationFinishedCallback)
+ } else {
+ shellTaskOrganizer.applyTransaction(wct)
+ releaseVisualIndicator()
+ }
+ }
+
+ private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveToFullscreen with animation taskId=%d",
+ task.taskId
+ )
+ val wct = WindowContainerTransaction()
+ addMoveToFullscreenChanges(wct, task.token)
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ exitDesktopTaskTransitionHandler.startTransition(
+ Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct, position, mOnAnimationFinishedCallback)
+ } else {
+ shellTaskOrganizer.applyTransaction(wct)
+ releaseVisualIndicator()
+ }
+ }
+
+ /** Move a task to the front */
+ fun moveTaskToFront(taskInfo: RunningTaskInfo) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveTaskToFront taskId=%d",
+ taskInfo.taskId
+ )
+
+ val wct = WindowContainerTransaction()
+ wct.reorder(taskInfo.token, true)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
+ } else {
+ shellTaskOrganizer.applyTransaction(wct)
+ }
+ }
+
+ /**
+ * Move task to the next display.
+ *
+ * Queries all current known display ids and sorts them in ascending order. Then iterates
+ * through the list and looks for the display id that is larger than the display id for
+ * the passed in task. If a display with a higher id is not found, iterates through the list and
+ * finds the first display id that is not the display id for the passed in task.
+ *
+ * If a display matching the above criteria is found, re-parents the task to that display.
+ * No-op if no such display is found.
+ */
+ fun moveToNextDisplay(taskId: Int) {
+ val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
+ if (task == null) {
+ KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId)
+ return
+ }
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d",
+ taskId, task.displayId)
+
+ val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted()
+ // Get the first display id that is higher than current task display id
+ var newDisplayId = displayIds.firstOrNull { displayId -> displayId > task.displayId }
+ if (newDisplayId == null) {
+ // No display with a higher id, get the first display id that is not the task display id
+ newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId }
+ }
+ if (newDisplayId == null) {
+ KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found")
+ return
+ }
+ moveToDisplay(task, newDisplayId)
+ }
+
+ /**
+ * Move [task] to display with [displayId].
+ *
+ * No-op if task is already on that display per [RunningTaskInfo.displayId].
+ */
+ private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) {
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d",
+ task.taskId, displayId)
+
+ if (task.displayId == displayId) {
+ KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display")
+ return
+ }
+
+ val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
+ if (displayAreaInfo == null) {
+ KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found")
+ return
+ }
+
+ val wct = WindowContainerTransaction()
+ wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+ } else {
+ shellTaskOrganizer.applyTransaction(wct)
+ }
+ }
+
+ /**
* Get windowing move for a given `taskId`
*
* @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found
@@ -154,9 +342,9 @@ class DesktopTasksController(
?: WINDOWING_MODE_UNDEFINED
}
- private fun bringDesktopAppsToFront(wct: WindowContainerTransaction) {
- ProtoLog.v(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront")
- val activeTasks = desktopModeTaskRepository.getActiveTasks()
+ private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) {
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront")
+ val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId)
// First move home to front and then other tasks on top of it
moveHomeTaskToFront(wct)
@@ -176,6 +364,16 @@ class DesktopTasksController(
?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) }
}
+ private fun releaseVisualIndicator() {
+ val t = SurfaceControl.Transaction()
+ visualIndicator?.releaseVisualIndicator(t)
+ visualIndicator = null
+ syncQueue.runInSync { transaction ->
+ transaction.merge(t)
+ t.close()
+ }
+ }
+
override fun getContext(): Context {
return context
}
@@ -200,18 +398,17 @@ class DesktopTasksController(
request: TransitionRequestInfo
): WindowContainerTransaction? {
// Check if we should skip handling this transition
- val task: ActivityManager.RunningTaskInfo? = request.triggerTask
val shouldHandleRequest =
when {
// Only handle open or to front transitions
request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> false
// Only handle when it is a task transition
- task == null -> false
+ request.triggerTask == null -> false
// Only handle standard type tasks
- task.activityType != ACTIVITY_TYPE_STANDARD -> false
+ request.triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> false
// Only handle fullscreen or freeform tasks
- task.windowingMode != WINDOWING_MODE_FULLSCREEN &&
- task.windowingMode != WINDOWING_MODE_FREEFORM -> false
+ request.triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN &&
+ request.triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> false
// Otherwise process it
else -> true
}
@@ -220,15 +417,16 @@ class DesktopTasksController(
return null
}
- val activeTasks = desktopModeTaskRepository.getActiveTasks()
+ val task: RunningTaskInfo = request.triggerTask
+ val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
// Check if we should switch a fullscreen task to freeform
- if (task?.windowingMode == WINDOWING_MODE_FULLSCREEN) {
+ if (task.windowingMode == WINDOWING_MODE_FULLSCREEN) {
// If there are any visible desktop tasks, switch the task to freeform
if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
- ProtoLog.d(
+ KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController#handleRequest: switch fullscreen task to freeform," +
+ "DesktopTasksController: switch fullscreen task to freeform on transition" +
" taskId=%d",
task.taskId
)
@@ -239,13 +437,13 @@ class DesktopTasksController(
}
// CHeck if we should switch a freeform task to fullscreen
- if (task?.windowingMode == WINDOWING_MODE_FREEFORM) {
+ if (task.windowingMode == WINDOWING_MODE_FREEFORM) {
// If no visible desktop tasks, switch this task to freeform as the transition came
// outside of this controller
if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
- ProtoLog.d(
+ KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController#handleRequest: switch freeform task to fullscreen," +
+ "DesktopTasksController: switch freeform task to fullscreen oon transition" +
" taskId=%d",
task.taskId
)
@@ -298,21 +496,160 @@ class DesktopTasksController(
}
/**
+ * Perform checks required on drag move. Create/release fullscreen indicator as needed.
+ *
+ * @param taskInfo the task being dragged.
+ * @param taskSurface SurfaceControl of dragged task.
+ * @param y coordinate of dragged task. Used for checks against status bar height.
+ */
+ fun onDragPositioningMove(
+ taskInfo: RunningTaskInfo,
+ taskSurface: SurfaceControl,
+ y: Float
+ ) {
+ if (taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
+ val statusBarHeight = getStatusBarHeight(taskInfo)
+ if (y <= statusBarHeight && visualIndicator == null) {
+ visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo,
+ displayController, context, taskSurface, shellTaskOrganizer,
+ rootTaskDisplayAreaOrganizer)
+ visualIndicator?.createFullscreenIndicatorWithAnimatedBounds()
+ } else if (y > statusBarHeight && visualIndicator != null) {
+ releaseVisualIndicator()
+ }
+ }
+ }
+
+ /**
+ * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area.
+ *
+ * @param taskInfo the task being dragged.
+ * @param position position of surface when drag ends
+ */
+ fun onDragPositioningEnd(
+ taskInfo: RunningTaskInfo,
+ position: Point
+ ) {
+ val statusBarHeight = getStatusBarHeight(taskInfo)
+ if (position.y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
+ moveToFullscreenWithAnimation(taskInfo, position)
+ }
+ }
+
+ /**
+ * Perform checks required on drag move. Create/release fullscreen indicator and transitions
+ * indicator to freeform or fullscreen dimensions as needed.
+ *
+ * @param taskInfo the task being dragged.
+ * @param taskSurface SurfaceControl of dragged task.
+ * @param y coordinate of dragged task. Used for checks against status bar height.
+ */
+ fun onDragPositioningMoveThroughStatusBar(
+ taskInfo: RunningTaskInfo,
+ taskSurface: SurfaceControl,
+ y: Float
+ ) {
+ // If the motion event is above the status bar, return since we do not need to show the
+ // visual indicator at this point.
+ if (y < getStatusBarHeight(taskInfo)) {
+ return
+ }
+ if (visualIndicator == null) {
+ visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo,
+ displayController, context, taskSurface, shellTaskOrganizer,
+ rootTaskDisplayAreaOrganizer)
+ visualIndicator?.createFullscreenIndicator()
+ }
+ val indicator = visualIndicator ?: return
+ if (y >= getFreeformTransitionStatusBarDragThreshold(taskInfo)) {
+ if (indicator.isFullscreen) {
+ indicator.transitionFullscreenIndicatorToFreeform()
+ }
+ } else if (!indicator.isFullscreen) {
+ indicator.transitionFreeformIndicatorToFullscreen()
+ }
+ }
+
+ /**
+ * Perform checks required when drag ends under status bar area.
+ *
+ * @param taskInfo the task being dragged.
+ * @param y height of drag, to be checked against status bar height.
+ */
+ fun onDragPositioningEndThroughStatusBar(
+ taskInfo: RunningTaskInfo,
+ freeformBounds: Rect
+ ) {
+ moveToDesktopWithAnimation(taskInfo, freeformBounds)
+ }
+
+ private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int {
+ return displayController.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0
+ }
+
+ /**
+ * Returns the threshold at which we transition a task into freeform when dragging a
+ * fullscreen task down from the status bar
+ */
+ private fun getFreeformTransitionStatusBarDragThreshold(taskInfo: RunningTaskInfo): Int {
+ return 2 * getStatusBarHeight(taskInfo)
+ }
+
+ /**
+ * Update the corner region for a specified task
+ */
+ fun onTaskCornersChanged(taskId: Int, corner: Region) {
+ desktopModeTaskRepository.updateTaskCorners(taskId, corner)
+ }
+
+ /**
+ * Remove a previously tracked corner region for a specified task.
+ */
+ fun removeCornersForTask(taskId: Int) {
+ desktopModeTaskRepository.removeTaskCorners(taskId)
+ }
+
+ /**
* Adds a listener to find out about changes in the visibility of freeform tasks.
*
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
- fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
+ fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor)
}
+ /**
+ * Adds a listener to track changes to desktop task corners
+ *
+ * @param listener the listener to add.
+ * @param callbackExecutor the executor to call the listener on.
+ */
+ fun setTaskCornerListener(
+ listener: Consumer<Region>,
+ callbackExecutor: Executor
+ ) {
+ desktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor)
+ }
+
/** The interface for calls from outside the shell, within the host process. */
@ExternalThread
private inner class DesktopModeImpl : DesktopMode {
- override fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
+ override fun addVisibleTasksListener(
+ listener: VisibleTasksListener,
+ callbackExecutor: Executor
+ ) {
+ mainExecutor.execute {
+ this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor)
+ }
+ }
+
+ override fun addDesktopGestureExclusionRegionListener(
+ listener: Consumer<Region>,
+ callbackExecutor: Executor
+ ) {
mainExecutor.execute {
- this@DesktopTasksController.addListener(listener, callbackExecutor)
+ this@DesktopTasksController.setTaskCornerListener(listener, callbackExecutor)
}
}
}
@@ -326,20 +663,19 @@ class DesktopTasksController(
controller = null
}
- override fun showDesktopApps() {
+ override fun showDesktopApps(displayId: Int) {
ExecutorUtils.executeRemoteCallWithTaskPermission(
controller,
- "showDesktopApps",
- Consumer(DesktopTasksController::showDesktopApps)
- )
+ "showDesktopApps"
+ ) { c -> c.showDesktopApps(displayId) }
}
- override fun getVisibleTaskCount(): Int {
+ override fun getVisibleTaskCount(displayId: Int): Int {
val result = IntArray(1)
ExecutorUtils.executeRemoteCallWithTaskPermission(
controller,
"getVisibleTaskCount",
- { controller -> result[0] = controller.getVisibleTaskCount() },
+ { controller -> result[0] = controller.getVisibleTaskCount(displayId) },
true /* blocking */
)
return result[0]
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
new file mode 100644
index 000000000000..3733b919e366
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -0,0 +1,248 @@
+/*
+ * 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.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks
+ * entering and exiting freeform.
+ */
+public class EnterDesktopTaskTransitionHandler implements Transitions.TransitionHandler {
+
+ private final Transitions mTransitions;
+ private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+
+ // The size of the screen during drag relative to the fullscreen size
+ public static final float DRAG_FREEFORM_SCALE = 0.4f;
+ // The size of the screen after drag relative to the fullscreen size
+ public static final float FINAL_FREEFORM_SCALE = 0.6f;
+ public static final int FREEFORM_ANIMATION_DURATION = 336;
+
+ private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
+ private Point mPosition;
+ private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
+
+ public EnterDesktopTaskTransitionHandler(
+ Transitions transitions) {
+ this(transitions, SurfaceControl.Transaction::new);
+ }
+
+ public EnterDesktopTaskTransitionHandler(
+ Transitions transitions,
+ Supplier<SurfaceControl.Transaction> supplier) {
+ mTransitions = transitions;
+ mTransactionSupplier = supplier;
+ }
+
+ /**
+ * Starts Transition of a given type
+ * @param type Transition type
+ * @param wct WindowContainerTransaction for transition
+ * @param onAnimationEndCallback to be called after animation
+ */
+ public void startTransition(@WindowManager.TransitionType int type,
+ @NonNull WindowContainerTransaction wct,
+ Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
+ mOnAnimationFinishedCallback = onAnimationEndCallback;
+ final IBinder token = mTransitions.startTransition(type, wct, this);
+ mPendingTransitionTokens.add(token);
+ }
+
+ /**
+ * Starts Transition of type TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
+ * @param wct WindowContainerTransaction for transition
+ * @param position Position of task when transition is triggered
+ * @param onAnimationEndCallback to be called after animation
+ */
+ public void startCancelMoveToDesktopMode(@NonNull WindowContainerTransaction wct,
+ Point position,
+ Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
+ mPosition = position;
+ startTransition(Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE, wct,
+ onAnimationEndCallback);
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ boolean transitionHandled = false;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
+ continue;
+ }
+
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue;
+ }
+
+ if (change.getMode() == WindowManager.TRANSIT_CHANGE) {
+ transitionHandled |= startChangeTransition(
+ transition, info.getType(), change, startT, finishT, finishCallback);
+ }
+ }
+
+ mPendingTransitionTokens.remove(transition);
+
+ return transitionHandled;
+ }
+
+ private boolean startChangeTransition(
+ @NonNull IBinder transition,
+ @WindowManager.TransitionType int type,
+ @NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (!mPendingTransitionTokens.contains(transition)) {
+ return false;
+ }
+
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (type == Transitions.TRANSIT_ENTER_FREEFORM
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ // Transitioning to freeform but keeping fullscreen bounds, so the crop is set
+ // to null and we don't require an animation
+ final SurfaceControl sc = change.getLeash();
+ startT.setWindowCrop(sc, null);
+ startT.apply();
+ mTransitions.getMainExecutor().execute(
+ () -> finishCallback.onTransitionFinished(null, null));
+ return true;
+ }
+
+ Rect endBounds = change.getEndAbsBounds();
+ if (type == Transitions.TRANSIT_ENTER_DESKTOP_MODE
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
+ && !endBounds.isEmpty()) {
+ // This Transition animates a task to freeform bounds after being dragged into freeform
+ // mode and brings the remaining freeform tasks to front
+ final SurfaceControl sc = change.getLeash();
+ startT.setWindowCrop(sc, endBounds.width(),
+ endBounds.height());
+ startT.apply();
+
+ // We want to find the scale of the current bounds relative to the end bounds. The
+ // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be
+ // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to
+ // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds
+ final ValueAnimator animator =
+ ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f);
+ animator.setDuration(FREEFORM_ANIMATION_DURATION);
+ final SurfaceControl.Transaction t = mTransactionSupplier.get();
+ animator.addUpdateListener(animation -> {
+ final float animationValue = (float) animation.getAnimatedValue();
+ t.setScale(sc, animationValue, animationValue);
+
+ final float animationWidth = endBounds.width() * animationValue;
+ final float animationHeight = endBounds.height() * animationValue;
+ final int animationX = endBounds.centerX() - (int) (animationWidth / 2);
+ final int animationY = endBounds.centerY() - (int) (animationHeight / 2);
+
+ t.setPosition(sc, animationX, animationY);
+ t.apply();
+ });
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mOnAnimationFinishedCallback != null) {
+ mOnAnimationFinishedCallback.accept(finishT);
+ }
+ mTransitions.getMainExecutor().execute(
+ () -> finishCallback.onTransitionFinished(null, null));
+ }
+ });
+
+ animator.start();
+ return true;
+ }
+
+ if (type == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+ && mPosition != null) {
+ // This Transition animates a task to fullscreen after being dragged from the status
+ // bar and then released back into the status bar area
+ final SurfaceControl sc = change.getLeash();
+ // Hide the first (fullscreen) frame because the animation will start from the smaller
+ // scale size.
+ startT.hide(sc)
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .apply();
+
+ final ValueAnimator animator = new ValueAnimator();
+ animator.setFloatValues(DRAG_FREEFORM_SCALE, 1f);
+ animator.setDuration(FREEFORM_ANIMATION_DURATION);
+ final SurfaceControl.Transaction t = mTransactionSupplier.get();
+ animator.addUpdateListener(animation -> {
+ final float scale = (float) animation.getAnimatedValue();
+ t.setPosition(sc, mPosition.x * (1 - scale), mPosition.y * (1 - scale))
+ .setScale(sc, scale, scale)
+ .show(sc)
+ .apply();
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mOnAnimationFinishedCallback != null) {
+ mOnAnimationFinishedCallback.accept(finishT);
+ }
+ mTransitions.getMainExecutor().execute(
+ () -> finishCallback.onTransitionFinished(null, null));
+ }
+ });
+ animator.start();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
new file mode 100644
index 000000000000..3ad5edf0e604
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -0,0 +1,187 @@
+/*
+ * 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.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.DisplayMetrics;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+
+/**
+ * The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks
+ * entering and exiting freeform.
+ */
+public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler {
+ private static final int FULLSCREEN_ANIMATION_DURATION = 336;
+ private final Context mContext;
+ private final Transitions mTransitions;
+ private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
+ private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
+ private Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+ private Point mPosition;
+
+ public ExitDesktopTaskTransitionHandler(
+ Transitions transitions,
+ Context context) {
+ this(transitions, SurfaceControl.Transaction::new, context);
+ }
+
+ private ExitDesktopTaskTransitionHandler(
+ Transitions transitions,
+ Supplier<SurfaceControl.Transaction> supplier,
+ Context context) {
+ mTransitions = transitions;
+ mTransactionSupplier = supplier;
+ mContext = context;
+ }
+
+ /**
+ * Starts Transition of a given type
+ * @param type Transition type
+ * @param wct WindowContainerTransaction for transition
+ * @param position Position of the task when transition is started
+ * @param onAnimationEndCallback to be called after animation
+ */
+ public void startTransition(@WindowManager.TransitionType int type,
+ @NonNull WindowContainerTransaction wct, Point position,
+ Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
+ mPosition = position;
+ mOnAnimationFinishedCallback = onAnimationEndCallback;
+ final IBinder token = mTransitions.startTransition(type, wct, this);
+ mPendingTransitionTokens.add(token);
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ boolean transitionHandled = false;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
+ continue;
+ }
+
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue;
+ }
+
+ if (change.getMode() == WindowManager.TRANSIT_CHANGE) {
+ transitionHandled |= startChangeTransition(
+ transition, info.getType(), change, startT, finishT, finishCallback);
+ }
+ }
+
+ mPendingTransitionTokens.remove(transition);
+
+ return transitionHandled;
+ }
+
+ @VisibleForTesting
+ boolean startChangeTransition(
+ @NonNull IBinder transition,
+ @WindowManager.TransitionType int type,
+ @NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (!mPendingTransitionTokens.contains(transition)) {
+ return false;
+ }
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (type == Transitions.TRANSIT_EXIT_DESKTOP_MODE
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ // This Transition animates a task to fullscreen after being dragged to status bar
+ final Resources resources = mContext.getResources();
+ final DisplayMetrics metrics = resources.getDisplayMetrics();
+ final int screenWidth = metrics.widthPixels;
+ final int screenHeight = metrics.heightPixels;
+ final SurfaceControl sc = change.getLeash();
+ final Rect endBounds = change.getEndAbsBounds();
+ // Hide the first (fullscreen) frame because the animation will start from the freeform
+ // size.
+ startT.hide(sc)
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .apply();
+ final ValueAnimator animator = new ValueAnimator();
+ animator.setFloatValues(0f, 1f);
+ animator.setDuration(FULLSCREEN_ANIMATION_DURATION);
+ // The start bounds contain the correct dimensions of the task but hold the positioning
+ // before being dragged to the status bar to transition into fullscreen
+ final Rect startBounds = change.getStartAbsBounds();
+ final float scaleX = (float) startBounds.width() / screenWidth;
+ final float scaleY = (float) startBounds.height() / screenHeight;
+ final SurfaceControl.Transaction t = mTransactionSupplier.get();
+ animator.addUpdateListener(animation -> {
+ float fraction = animation.getAnimatedFraction();
+ float currentScaleX = scaleX + ((1 - scaleX) * fraction);
+ float currentScaleY = scaleY + ((1 - scaleY) * fraction);
+ t.setPosition(sc, mPosition.x * (1 - fraction), mPosition.y * (1 - fraction))
+ .setScale(sc, currentScaleX, currentScaleY)
+ .show(sc)
+ .apply();
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mOnAnimationFinishedCallback != null) {
+ mOnAnimationFinishedCallback.accept(finishT);
+ }
+ mTransitions.getMainExecutor().execute(
+ () -> finishCallback.onTransitionFinished(null, null));
+ }
+ });
+ animator.start();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index d0739e14675f..899d67267e69 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -21,9 +21,9 @@ package com.android.wm.shell.desktopmode;
*/
interface IDesktopMode {
- /** Show apps on the desktop */
- void showDesktopApps();
+ /** Show apps on the desktop on the given display */
+ void showDesktopApps(int displayId);
- /** Get count of visible desktop tasks */
- int getVisibleTaskCount();
+ /** Get count of visible desktop tasks on the given display */
+ int getVisibleTaskCount(int displayId);
} \ No newline at end of file
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 4cfaae6e51c7..be2489da3628 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
@@ -35,10 +35,14 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
+
import android.content.ClipDescription;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.HardwareRenderer;
import android.graphics.PixelFormat;
import android.util.Slog;
import android.util.SparseArray;
@@ -50,6 +54,8 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
+import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.InstanceId;
@@ -58,25 +64,31 @@ import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
+import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ExternalMainThread;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import java.io.PrintWriter;
import java.util.ArrayList;
/**
* Handles the global drag and drop handling for the Shell.
*/
-public class DragAndDropController implements DisplayController.OnDisplaysChangedListener,
+public class DragAndDropController implements RemoteCallable<DragAndDropController>,
+ DisplayController.OnDisplaysChangedListener,
View.OnDragListener, ComponentCallbacks2 {
private static final String TAG = DragAndDropController.class.getSimpleName();
private final Context mContext;
private final ShellController mShellController;
+ private final ShellCommandHandler mShellCommandHandler;
private final DisplayController mDisplayController;
private final DragAndDropEventLogger mLogger;
private final IconProvider mIconProvider;
@@ -94,15 +106,35 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
void onDragStarted();
}
- public DragAndDropController(Context context,
+ /**
+ * Creates {@link DragAndDropController}. Returns {@code null} if the feature is disabled.
+ */
+ public static DragAndDropController create(Context context,
+ ShellInit shellInit,
+ ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
+ DisplayController displayController,
+ UiEventLogger uiEventLogger,
+ IconProvider iconProvider,
+ ShellExecutor mainExecutor) {
+ if (!context.getResources().getBoolean(R.bool.config_enableShellDragDrop)) {
+ return null;
+ }
+ return new DragAndDropController(context, shellInit, shellController, shellCommandHandler,
+ displayController, uiEventLogger, iconProvider, mainExecutor);
+ }
+
+ DragAndDropController(Context context,
ShellInit shellInit,
ShellController shellController,
+ ShellCommandHandler shellCommandHandler,
DisplayController displayController,
UiEventLogger uiEventLogger,
IconProvider iconProvider,
ShellExecutor mainExecutor) {
mContext = context;
mShellController = shellController;
+ mShellCommandHandler = shellCommandHandler;
mDisplayController = displayController;
mLogger = new DragAndDropEventLogger(uiEventLogger);
mIconProvider = iconProvider;
@@ -120,6 +152,23 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
mMainExecutor.executeDelayed(() -> {
mDisplayController.addDisplayWindowListener(this);
}, 0);
+ mShellController.addExternalInterface(KEY_EXTRA_SHELL_DRAG_AND_DROP,
+ this::createExternalInterface, this);
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ }
+
+ private ExternalInterfaceBinder createExternalInterface() {
+ return new IDragAndDropImpl(this);
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
}
/**
@@ -139,7 +188,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
mListeners.remove(listener);
}
- private void notifyListeners() {
+ private void notifyDragStarted() {
for (int i = 0; i < mListeners.size(); i++) {
mListeners.get(i).onDragStarted();
}
@@ -256,7 +305,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId),
event.getClipData(), loggerSessionId);
setDropTargetWindowVisibility(pd, View.VISIBLE);
- notifyListeners();
+ notifyDragStarted();
break;
case ACTION_DRAG_ENTERED:
pd.dragLayout.show();
@@ -310,13 +359,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
}
private void setDropTargetWindowVisibility(PerDisplay pd, int visibility) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
- "Set drop target window visibility: displayId=%d visibility=%d",
- pd.displayId, visibility);
- pd.rootView.setVisibility(visibility);
- if (visibility == View.VISIBLE) {
- pd.rootView.requestApplyInsets();
- }
+ pd.setWindowVisibility(visibility);
}
private String getMimeTypes(ClipDescription description) {
@@ -330,6 +373,18 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
return mimeTypes;
}
+ /**
+ * Returns if any displays are currently ready to handle a drag/drop.
+ */
+ private boolean isReadyToHandleDrag() {
+ for (int i = 0; i < mDisplayDropTargets.size(); i++) {
+ if (mDisplayDropTargets.valueAt(i).mHasDrawn) {
+ return true;
+ }
+ }
+ return false;
+ }
+
// Note: Component callbacks are always called on the main thread of the process
@ExternalMainThread
@Override
@@ -355,12 +410,53 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
// Do nothing
}
- private static class PerDisplay {
+ /**
+ * Dumps information about this controller.
+ */
+ public void dump(@NonNull PrintWriter pw, String prefix) {
+ pw.println(prefix + TAG);
+ pw.println(prefix + " listeners=" + mListeners.size());
+ }
+
+ /**
+ * The interface for calls from outside the host process.
+ */
+ @BinderThread
+ private static class IDragAndDropImpl extends IDragAndDrop.Stub
+ implements ExternalInterfaceBinder {
+ private DragAndDropController mController;
+
+ public IDragAndDropImpl(DragAndDropController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Invalidates this instance, preventing future calls from updating the controller.
+ */
+ @Override
+ public void invalidate() {
+ mController = null;
+ }
+
+ @Override
+ public boolean isReadyToHandleDrag() {
+ boolean[] result = new boolean[1];
+ executeRemoteCallWithTaskPermission(mController, "isReadyToHandleDrag",
+ controller -> result[0] = controller.isReadyToHandleDrag(),
+ true /* blocking */
+ );
+ return result[0];
+ }
+ }
+
+ private static class PerDisplay implements HardwareRenderer.FrameDrawingCallback {
final int displayId;
final Context context;
final WindowManager wm;
final FrameLayout rootView;
final DragLayout dragLayout;
+ // Tracks whether the window has fully drawn since it was last made visible
+ boolean mHasDrawn;
boolean isHandlingDrag;
// A count of the number of active drags in progress to ensure that we only hide the window
@@ -374,5 +470,25 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange
rootView = rv;
dragLayout = dl;
}
+
+ private void setWindowVisibility(int visibility) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+ "Set drop target window visibility: displayId=%d visibility=%d",
+ displayId, visibility);
+ rootView.setVisibility(visibility);
+ if (visibility == View.VISIBLE) {
+ rootView.requestApplyInsets();
+ if (!mHasDrawn && rootView.getViewRootImpl() != null) {
+ rootView.getViewRootImpl().registerRtFrameCallback(this);
+ }
+ } else {
+ mHasDrawn = false;
+ }
+ }
+
+ @Override
+ public void onFrameDraw(long frame) {
+ mHasDrawn = true;
+ }
}
}
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 df94b414c092..fb08c878837a 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
@@ -266,6 +266,7 @@ public class DragAndDropPolicy {
final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT);
final Bundle opts = intent.hasExtra(EXTRA_ACTIVITY_OPTIONS)
? intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) : new Bundle();
+ final UserHandle user = intent.getParcelableExtra(EXTRA_USER);
if (isTask) {
final int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID);
@@ -273,14 +274,14 @@ public class DragAndDropPolicy {
} else if (isShortcut) {
final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
final String id = intent.getStringExtra(EXTRA_SHORTCUT_ID);
- final UserHandle user = intent.getParcelableExtra(EXTRA_USER);
mStarter.startShortcut(packageName, id, 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, null /* fillIntent */, position, opts);
+ mStarter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */,
+ position, opts);
}
}
@@ -334,8 +335,8 @@ public class DragAndDropPolicy {
void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options);
void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
@Nullable Bundle options, UserHandle user);
- void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
- @Nullable Bundle options);
+ void startIntent(PendingIntent intent, int userId, Intent fillInIntent,
+ @SplitPosition int position, @Nullable Bundle options);
void enterSplitScreen(int taskId, boolean leftOrTop);
/**
@@ -379,8 +380,8 @@ public class DragAndDropPolicy {
}
@Override
- public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, int position,
- @Nullable Bundle options) {
+ public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent,
+ int position, @Nullable Bundle options) {
try {
intent.send(mContext, 0, fillInIntent, null, null, null, options);
} catch (PendingIntent.CanceledException e) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
index 28f59b53b5b6..724a130ef52d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
@@ -104,7 +104,7 @@ public class DropZoneView extends FrameLayout {
setContainerMargin(0, 0, 0, 0); // make sure it's populated
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mMarginColor = getResources().getColor(R.color.taskbar_background);
+ mMarginColor = getResources().getColor(R.color.taskbar_background_dark);
int c = getResources().getColor(android.R.color.system_accent1_500);
mHighlightColor = Color.argb(HIGHLIGHT_ALPHA, Color.red(c), Color.green(c), Color.blue(c));
mSplashScreenColor = Color.argb(SPLASHSCREEN_ALPHA, 0, 0, 0);
@@ -125,7 +125,7 @@ public class DropZoneView extends FrameLayout {
public void onThemeChange() {
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(getContext());
- mMarginColor = getResources().getColor(R.color.taskbar_background);
+ mMarginColor = getResources().getColor(R.color.taskbar_background_dark);
mHighlightColor = getResources().getColor(android.R.color.system_accent1_500);
if (mMarginPercent > 0) {
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/IDragAndDrop.aidl
index 24275e002c7f..aeb0c63fa4c5 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/NonResizeableActivity.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/IDragAndDrop.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker.testapp;
+package com.android.wm.shell.draganddrop;
-import android.app.Activity;
-import android.os.Bundle;
-
-public class NonResizeableActivity extends Activity {
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- setContentView(R.layout.activity_non_resizeable);
- }
+/**
+ * Interface that is exposed to remote callers to manipulate drag and drop.
+ */
+interface IDragAndDrop {
+ /**
+ * Returns whether the shell drop target is showing and will handle a drag/drop.
+ */
+ boolean isReadyToHandleDrag() = 1;
}
+// Last id = 1 \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 48487bc4a3d6..22541bbd892a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -94,11 +94,12 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
mDesktopModeTaskRepository.ifPresent(repository -> {
repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
if (taskInfo.isVisible) {
- if (repository.addActiveTask(taskInfo.taskId)) {
+ if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Adding active freeform task: #%d", taskInfo.taskId);
}
- repository.updateVisibleFreeformTasks(taskInfo.taskId, true);
+ repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId,
+ true);
}
});
}
@@ -117,7 +118,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Removing active freeform task: #%d", taskInfo.taskId);
}
- repository.updateVisibleFreeformTasks(taskInfo.taskId, false);
+ repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false);
});
}
@@ -137,12 +138,13 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
if (DesktopModeStatus.isAnyEnabled()) {
mDesktopModeTaskRepository.ifPresent(repository -> {
if (taskInfo.isVisible) {
- if (repository.addActiveTask(taskInfo.taskId)) {
+ if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Adding active freeform task: #%d", taskInfo.taskId);
}
}
- repository.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible);
+ repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId,
+ taskInfo.isVisible);
});
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 60e5ff27cab9..6b6a7bc42046 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -112,6 +112,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
onChangeTransitionReady(change, startT, finishT);
break;
}
+ mWindowDecorViewModel.onTransitionReady(transition, info, change);
}
mTransitionToTaskInfo.put(transition, taskInfoList);
}
@@ -152,6 +153,8 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
@Override
public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+ mWindowDecorViewModel.onTransitionMerged(merged, playing);
+
final List<ActivityManager.RunningTaskInfo> infoOfMerged =
mTransitionToTaskInfo.get(merged);
if (infoOfMerged == null) {
@@ -175,7 +178,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
final List<ActivityManager.RunningTaskInfo> taskInfo =
mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList());
mTransitionToTaskInfo.remove(transition);
-
+ mWindowDecorViewModel.onTransitionFinished(transition);
for (int i = 0; i < taskInfo.size(); ++i) {
mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index d094c229e0f8..998728d65e6a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -129,6 +129,7 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
final State state = mTasks.get(taskInfo.taskId);
final Point oldPositionInParent = state.mTaskInfo.positionInParent;
+ boolean oldVisible = state.mTaskInfo.isVisible;
if (mWindowDecorViewModelOptional.isPresent()) {
mWindowDecorViewModelOptional.get().onTaskInfoChanged(taskInfo);
@@ -138,12 +139,18 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
updateRecentsForVisibleFullscreenTask(taskInfo);
final Point positionInParent = state.mTaskInfo.positionInParent;
- if (!oldPositionInParent.equals(state.mTaskInfo.positionInParent)) {
+ boolean positionInParentChanged = !oldPositionInParent.equals(positionInParent);
+ boolean becameVisible = !oldVisible && state.mTaskInfo.isVisible;
+
+ if (becameVisible || positionInParentChanged) {
mSyncQueue.runInSync(t -> {
if (!state.mLeash.isValid()) {
// Task vanished before sync completion
return;
}
+ if (becameVisible) {
+ t.show(state.mLeash);
+ }
t.setPosition(state.mLeash, positionInParent.x, positionInParent.y);
});
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java
index f376e1fd6174..32894cdc5aec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java
@@ -117,6 +117,7 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer {
@Override
public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
@NonNull SurfaceControl leash) {
+ leash.setUnreleasedWarningCallSite("HideDisplayCutoutOrganizer.onDisplayAreaAppeared");
if (!addDisplayAreaInfoAndLeashToMap(displayAreaInfo, leash)) {
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
new file mode 100644
index 000000000000..56bd188a0d7d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -0,0 +1,288 @@
+/*
+ * 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.keyguard;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
+import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_OCCLUDING;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
+import static android.view.WindowManager.TRANSIT_SLEEP;
+
+import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ExternalThread;
+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.transition.Transitions.TransitionFinishCallback;
+
+/**
+ * The handler for Keyguard enter/exit and occlude/unocclude animations.
+ *
+ * <p>This takes the highest priority.
+ */
+public class KeyguardTransitionHandler implements Transitions.TransitionHandler {
+ private static final String TAG = "KeyguardTransition";
+
+ private final Transitions mTransitions;
+ private final Handler mMainHandler;
+ private final ShellExecutor mMainExecutor;
+
+ private final ArrayMap<IBinder, StartedTransition> mStartedTransitions = new ArrayMap<>();
+
+ /**
+ * Local IRemoteTransition implementations registered by the keyguard service.
+ * @see KeyguardTransitions
+ */
+ private IRemoteTransition mExitTransition = null;
+ private IRemoteTransition mOccludeTransition = null;
+ private IRemoteTransition mOccludeByDreamTransition = null;
+ private IRemoteTransition mUnoccludeTransition = null;
+
+ private final class StartedTransition {
+ final TransitionInfo mInfo;
+ final SurfaceControl.Transaction mFinishT;
+ final IRemoteTransition mPlayer;
+
+ public StartedTransition(TransitionInfo info,
+ SurfaceControl.Transaction finishT, IRemoteTransition player) {
+ mInfo = info;
+ mFinishT = finishT;
+ mPlayer = player;
+ }
+ }
+ public KeyguardTransitionHandler(
+ @NonNull ShellInit shellInit,
+ @NonNull Transitions transitions,
+ @NonNull Handler mainHandler,
+ @NonNull ShellExecutor mainExecutor) {
+ mTransitions = transitions;
+ mMainHandler = mainHandler;
+ mMainExecutor = mainExecutor;
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mTransitions.addHandler(this);
+ }
+
+ /**
+ * Interface for SystemUI implementations to set custom Keyguard exit/occlude handlers.
+ */
+ @ExternalThread
+ public KeyguardTransitions asKeyguardTransitions() {
+ return new KeyguardTransitionsImpl();
+ }
+
+ public static boolean handles(TransitionInfo info) {
+ return (info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0;
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull TransitionFinishCallback finishCallback) {
+ if (!handles(info)) {
+ return false;
+ }
+
+ // Choose a transition applicable for the changes and keyguard state.
+ if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) {
+ return startAnimation(mExitTransition,
+ "going-away",
+ transition, info, startTransaction, finishTransaction, finishCallback);
+ }
+ if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_OCCLUDING) != 0) {
+ if (hasOpeningDream(info)) {
+ return startAnimation(mOccludeByDreamTransition,
+ "occlude-by-dream",
+ transition, info, startTransaction, finishTransaction, finishCallback);
+ } else {
+ return startAnimation(mOccludeTransition,
+ "occlude",
+ transition, info, startTransaction, finishTransaction, finishCallback);
+ }
+ } else if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
+ return startAnimation(mUnoccludeTransition,
+ "unocclude",
+ transition, info, startTransaction, finishTransaction, finishCallback);
+ } else {
+ Log.i(TAG, "Refused to play keyguard transition: " + info);
+ return false;
+ }
+ }
+
+ private boolean startAnimation(IRemoteTransition remoteHandler, String description,
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "start keyguard %s transition, info = %s", description, info);
+
+ try {
+ mStartedTransitions.put(transition,
+ new StartedTransition(info, finishTransaction, remoteHandler));
+ remoteHandler.startAnimation(transition, info, startTransaction,
+ new IRemoteTransitionFinishedCallback.Stub() {
+ @Override
+ public void onTransitionFinished(
+ WindowContainerTransaction wct, SurfaceControl.Transaction sct) {
+ if (sct != null) {
+ finishTransaction.merge(sct);
+ }
+ // Post our finish callback to let startAnimation finish first.
+ mMainExecutor.executeDelayed(() -> {
+ mStartedTransitions.remove(transition);
+ finishCallback.onTransitionFinished(wct, null);
+ }, 0);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "RemoteException thrown from local IRemoteTransition", e);
+ return false;
+ }
+ startTransaction.clear();
+ return true;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder nextTransition, @NonNull TransitionInfo nextInfo,
+ @NonNull SurfaceControl.Transaction nextT, @NonNull IBinder currentTransition,
+ @NonNull TransitionFinishCallback nextFinishCallback) {
+ final StartedTransition playing = mStartedTransitions.get(currentTransition);
+ if (playing == null) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "unknown keyguard transition %s", currentTransition);
+ return;
+ }
+ if ((nextInfo.getFlags() & WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING) != 0
+ && (playing.mInfo.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) {
+ // Keyguard unlocking has been canceled. Merge the unlock and re-lock transitions to
+ // avoid a flicker where we flash one frame with the screen fully unlocked.
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "canceling keyguard exit transition %s", currentTransition);
+ playing.mFinishT.merge(nextT);
+ try {
+ playing.mPlayer.mergeAnimation(nextTransition, nextInfo, nextT, currentTransition,
+ new FakeFinishCallback());
+ } catch (RemoteException e) {
+ // There is no good reason for this to happen because the player is a local object
+ // implementing an AIDL interface.
+ Log.wtf(TAG, "RemoteException thrown from KeyguardService transition", e);
+ }
+ nextFinishCallback.onTransitionFinished(null, null);
+ } else if (nextInfo.getType() == TRANSIT_SLEEP) {
+ // An empty SLEEP transition comes in as a signal to abort transitions whenever a sleep
+ // token is held. In cases where keyguard is showing, we are running the animation for
+ // the device sleeping/waking, so it's best to ignore this and keep playing anyway.
+ return;
+ } else if (handles(nextInfo)) {
+ // In all other cases, fast-forward to let the next queued transition start playing.
+ finishAnimationImmediately(currentTransition, playing);
+ }
+ }
+
+ @Override
+ public void onTransitionConsumed(IBinder transition, boolean aborted,
+ SurfaceControl.Transaction finishTransaction) {
+ final StartedTransition playing = mStartedTransitions.remove(transition);
+ if (playing != null) {
+ finishAnimationImmediately(transition, playing);
+ }
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ return null;
+ }
+
+ private static boolean hasOpeningDream(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; i--) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (isOpeningType(change.getMode())
+ && change.getTaskInfo() != null
+ && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_DREAM) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void finishAnimationImmediately(IBinder transition, StartedTransition playing) {
+ final IBinder fakeTransition = new Binder();
+ final TransitionInfo fakeInfo = new TransitionInfo(TRANSIT_SLEEP, 0x0);
+ final SurfaceControl.Transaction fakeT = new SurfaceControl.Transaction();
+ final FakeFinishCallback fakeFinishCb = new FakeFinishCallback();
+ try {
+ playing.mPlayer.mergeAnimation(
+ fakeTransition, fakeInfo, fakeT, transition, fakeFinishCb);
+ } catch (RemoteException e) {
+ // There is no good reason for this to happen because the player is a local object
+ // implementing an AIDL interface.
+ Log.wtf(TAG, "RemoteException thrown from KeyguardService transition", e);
+ }
+ }
+
+ private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub {
+ @Override
+ public void onTransitionFinished(
+ WindowContainerTransaction wct, SurfaceControl.Transaction t) {
+ return;
+ }
+ }
+
+ @ExternalThread
+ private final class KeyguardTransitionsImpl implements KeyguardTransitions {
+ @Override
+ public void register(
+ IRemoteTransition exitTransition,
+ IRemoteTransition occludeTransition,
+ IRemoteTransition occludeByDreamTransition,
+ IRemoteTransition unoccludeTransition) {
+ mMainExecutor.execute(() -> {
+ mExitTransition = exitTransition;
+ mOccludeTransition = occludeTransition;
+ mOccludeByDreamTransition = occludeByDreamTransition;
+ mUnoccludeTransition = unoccludeTransition;
+ });
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
new file mode 100644
index 000000000000..b4b327f0eff2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
@@ -0,0 +1,41 @@
+/*
+ * 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.keyguard;
+
+import android.annotation.NonNull;
+import android.window.IRemoteTransition;
+
+import com.android.wm.shell.common.annotations.ExternalThread;
+
+/**
+ * Interface exposed to SystemUI Keyguard to register handlers for running
+ * animations on keyguard visibility changes.
+ *
+ * TODO(b/274954192): Merge the occludeTransition and occludeByDream handlers and just let the
+ * keyguard handler make the decision on which version it wants to play.
+ */
+@ExternalThread
+public interface KeyguardTransitions {
+ /**
+ * Registers a set of remote transitions for Keyguard.
+ */
+ default void register(
+ @NonNull IRemoteTransition unlockTransition,
+ @NonNull IRemoteTransition occludeTransition,
+ @NonNull IRemoteTransition occludeByDreamTransition,
+ @NonNull IRemoteTransition unoccludeTransition) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java
deleted file mode 100644
index 65cb7ac1e5f7..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeSettingsObserver.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.kidsmode;
-
-import android.annotation.NonNull;
-import android.app.ActivityManager;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.provider.Settings;
-
-import java.util.Collection;
-
-/**
- * A ContentObserver for listening kids mode relative setting keys:
- * - {@link Settings.Secure#NAVIGATION_MODE}
- * - {@link Settings.Secure#NAV_BAR_KIDS_MODE}
- *
- * @hide
- */
-public class KidsModeSettingsObserver extends ContentObserver {
- private Context mContext;
- private Runnable mOnChangeRunnable;
-
- public KidsModeSettingsObserver(Handler handler, Context context) {
- super(handler);
- mContext = context;
- }
-
- public void setOnChangeRunnable(Runnable r) {
- mOnChangeRunnable = r;
- }
-
- /**
- * Registers the observer.
- */
- public void register() {
- final ContentResolver r = mContext.getContentResolver();
- r.registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.NAVIGATION_MODE),
- false, this, UserHandle.USER_ALL);
- r.registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE),
- false, this, UserHandle.USER_ALL);
- }
-
- /**
- * Unregisters the observer.
- */
- public void unregister() {
- mContext.getContentResolver().unregisterContentObserver(this);
- }
-
- @Override
- public void onChange(boolean selfChange, @NonNull Collection<Uri> uris, int flags, int userId) {
- if (userId != ActivityManager.getCurrentUser()) {
- return;
- }
-
- if (mOnChangeRunnable != null) {
- mOnChangeRunnable.run();
- }
- }
-
- /**
- * Returns true only when it's in three button nav mode and the kid nav bar mode is enabled.
- * Otherwise, return false.
- */
- public boolean isEnabled() {
- return Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.NAVIGATION_MODE, 0, UserHandle.USER_CURRENT) == 0
- && Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.NAV_BAR_KIDS_MODE, 0, UserHandle.USER_CURRENT) == 1;
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
deleted file mode 100644
index 77fe7a869f8e..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
+++ /dev/null
@@ -1,424 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.kidsmode;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import android.app.ActivityManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.IBinder;
-import android.view.Display;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.SurfaceControl;
-import android.window.ITaskOrganizerController;
-import android.window.TaskAppearedInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import androidx.annotation.NonNull;
-
-import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.recents.RecentTasksController;
-import com.android.wm.shell.sysui.ShellCommandHandler;
-import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.unfold.UnfoldAnimationController;
-
-import java.io.PrintWriter;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * A dedicated task organizer when kids mode is enabled.
- * - Creates a root task with bounds that exclude the navigation bar area
- * - Launch all task into the root task except for Launcher
- */
-public class KidsModeTaskOrganizer extends ShellTaskOrganizer {
- private static final String TAG = "KidsModeTaskOrganizer";
-
- private static final int[] CONTROLLED_ACTIVITY_TYPES =
- {ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_HOME};
- private static final int[] CONTROLLED_WINDOWING_MODES =
- {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
-
- private final Handler mMainHandler;
- private final Context mContext;
- private final ShellCommandHandler mShellCommandHandler;
- private final SyncTransactionQueue mSyncQueue;
- private final DisplayController mDisplayController;
- private final DisplayInsetsController mDisplayInsetsController;
-
- /**
- * The value of the {@link R.bool.config_reverseDefaultRotation} property which defines how
- * {@link Display#getRotation} values are mapped to screen orientations
- */
- private final boolean mReverseDefaultRotationEnabled;
-
- @VisibleForTesting
- ActivityManager.RunningTaskInfo mLaunchRootTask;
- @VisibleForTesting
- SurfaceControl mLaunchRootLeash;
- @VisibleForTesting
- final IBinder mCookie = new Binder();
-
- private final InsetsState mInsetsState = new InsetsState();
- private int mDisplayWidth;
- private int mDisplayHeight;
-
- private KidsModeSettingsObserver mKidsModeSettingsObserver;
- private boolean mEnabled;
-
- private ActivityManager.RunningTaskInfo mHomeTask;
-
- private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateKidsModeState();
- }
- };
-
- DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener =
- new DisplayController.OnDisplaysChangedListener() {
- @Override
- public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
- if (displayId != DEFAULT_DISPLAY) {
- return;
- }
- final DisplayLayout displayLayout =
- mDisplayController.getDisplayLayout(DEFAULT_DISPLAY);
- if (displayLayout == null) {
- return;
- }
- final int displayWidth = displayLayout.width();
- final int displayHeight = displayLayout.height();
- if (displayWidth == mDisplayWidth || displayHeight == mDisplayHeight) {
- return;
- }
- mDisplayWidth = displayWidth;
- mDisplayHeight = displayHeight;
- updateBounds();
- }
- };
-
- DisplayInsetsController.OnInsetsChangedListener mOnInsetsChangedListener =
- new DisplayInsetsController.OnInsetsChangedListener() {
- @Override
- public void insetsChanged(InsetsState insetsState) {
- // Update bounds only when the insets of navigation bar or task bar is changed.
- if (Objects.equals(insetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR),
- mInsetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR))
- && Objects.equals(insetsState.peekSource(
- InsetsState.ITYPE_EXTRA_NAVIGATION_BAR),
- mInsetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR))) {
- return;
- }
- mInsetsState.set(insetsState);
- updateBounds();
- }
- };
-
- @VisibleForTesting
- KidsModeTaskOrganizer(
- Context context,
- ShellInit shellInit,
- ShellCommandHandler shellCommandHandler,
- ITaskOrganizerController taskOrganizerController,
- SyncTransactionQueue syncTransactionQueue,
- DisplayController displayController,
- DisplayInsetsController displayInsetsController,
- Optional<UnfoldAnimationController> unfoldAnimationController,
- Optional<RecentTasksController> recentTasks,
- KidsModeSettingsObserver kidsModeSettingsObserver,
- ShellExecutor mainExecutor,
- Handler mainHandler) {
- // Note: we don't call super with the shell init because we will be initializing manually
- super(/* shellInit= */ null, /* shellCommandHandler= */ null, taskOrganizerController,
- /* compatUI= */ null, unfoldAnimationController, recentTasks, mainExecutor);
- mContext = context;
- mShellCommandHandler = shellCommandHandler;
- mMainHandler = mainHandler;
- mSyncQueue = syncTransactionQueue;
- mDisplayController = displayController;
- mDisplayInsetsController = displayInsetsController;
- mKidsModeSettingsObserver = kidsModeSettingsObserver;
- shellInit.addInitCallback(this::onInit, this);
- mReverseDefaultRotationEnabled = context.getResources().getBoolean(
- R.bool.config_reverseDefaultRotation);
- }
-
- public KidsModeTaskOrganizer(
- Context context,
- ShellInit shellInit,
- ShellCommandHandler shellCommandHandler,
- SyncTransactionQueue syncTransactionQueue,
- DisplayController displayController,
- DisplayInsetsController displayInsetsController,
- Optional<UnfoldAnimationController> unfoldAnimationController,
- Optional<RecentTasksController> recentTasks,
- ShellExecutor mainExecutor,
- Handler mainHandler) {
- // Note: we don't call super with the shell init because we will be initializing manually
- super(/* shellInit= */ null, /* taskOrganizerController= */ null, /* compatUI= */ null,
- unfoldAnimationController, recentTasks, mainExecutor);
- mContext = context;
- mShellCommandHandler = shellCommandHandler;
- mMainHandler = mainHandler;
- mSyncQueue = syncTransactionQueue;
- mDisplayController = displayController;
- mDisplayInsetsController = displayInsetsController;
- shellInit.addInitCallback(this::onInit, this);
- mReverseDefaultRotationEnabled = context.getResources().getBoolean(
- R.bool.config_reverseDefaultRotation);
- }
-
- /**
- * Initializes kids mode status.
- */
- public void onInit() {
- if (mShellCommandHandler != null) {
- mShellCommandHandler.addDumpCallback(this::dump, this);
- }
- if (mKidsModeSettingsObserver == null) {
- mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext);
- }
- mKidsModeSettingsObserver.setOnChangeRunnable(() -> updateKidsModeState());
- updateKidsModeState();
- mKidsModeSettingsObserver.register();
-
- final IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_USER_SWITCHED);
- mContext.registerReceiverForAllUsers(mUserSwitchIntentReceiver, filter, null, mMainHandler);
- }
-
- @Override
- public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- if (mEnabled && mLaunchRootTask == null && taskInfo.launchCookies != null
- && taskInfo.launchCookies.contains(mCookie)) {
- mLaunchRootTask = taskInfo;
- mLaunchRootLeash = leash;
- updateTask();
- }
- super.onTaskAppeared(taskInfo, leash);
-
- // Only allow home to draw under system bars.
- if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
- final WindowContainerTransaction wct = getWindowContainerTransaction();
- wct.setBounds(taskInfo.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight));
- mSyncQueue.queue(wct);
- mHomeTask = taskInfo;
- }
- mSyncQueue.runInSync(t -> {
- // Reset several properties back to fullscreen (PiP, for example, leaves all these
- // properties in a bad state).
- t.setCrop(leash, null);
- t.setPosition(leash, 0, 0);
- t.setAlpha(leash, 1f);
- t.setMatrix(leash, 1, 0, 0, 1);
- t.show(leash);
- });
- }
-
- @Override
- public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- if (mLaunchRootTask != null && mLaunchRootTask.taskId == taskInfo.taskId
- && !taskInfo.equals(mLaunchRootTask)) {
- mLaunchRootTask = taskInfo;
- }
-
- if (mHomeTask != null && mHomeTask.taskId == taskInfo.taskId
- && !taskInfo.equals(mHomeTask)) {
- mHomeTask = taskInfo;
- }
-
- super.onTaskInfoChanged(taskInfo);
- }
-
- @VisibleForTesting
- void updateKidsModeState() {
- final boolean enabled = mKidsModeSettingsObserver.isEnabled();
- if (mEnabled == enabled) {
- return;
- }
- mEnabled = enabled;
- if (mEnabled) {
- enable();
- } else {
- disable();
- }
- }
-
- @VisibleForTesting
- void enable() {
- // Needed since many Kids apps aren't optimised to support both orientations and it will be
- // hard for kids to understand the app compat mode.
- // TODO(229961548): Remove ignoreOrientationRequest exception for Kids Mode once possible.
- if (mReverseDefaultRotationEnabled) {
- setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ true,
- /* fromOrientations */
- new int[]{SCREEN_ORIENTATION_LANDSCAPE, SCREEN_ORIENTATION_REVERSE_LANDSCAPE},
- /* toOrientations */
- new int[]{SCREEN_ORIENTATION_SENSOR_LANDSCAPE,
- SCREEN_ORIENTATION_SENSOR_LANDSCAPE});
- } else {
- setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ true,
- /* fromOrientations */ null, /* toOrientations */ null);
- }
- final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(DEFAULT_DISPLAY);
- if (displayLayout != null) {
- mDisplayWidth = displayLayout.width();
- mDisplayHeight = displayLayout.height();
- }
- mInsetsState.set(mDisplayController.getInsetsState(DEFAULT_DISPLAY));
- mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY,
- mOnInsetsChangedListener);
- mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener);
- List<TaskAppearedInfo> taskAppearedInfos = registerOrganizer();
- for (int i = 0; i < taskAppearedInfos.size(); i++) {
- final TaskAppearedInfo info = taskAppearedInfos.get(i);
- onTaskAppeared(info.getTaskInfo(), info.getLeash());
- }
- createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN, mCookie);
- updateTask();
- }
-
- @VisibleForTesting
- void disable() {
- setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ false,
- /* fromOrientations */ null, /* toOrientations */ null);
- mDisplayInsetsController.removeInsetsChangedListener(DEFAULT_DISPLAY,
- mOnInsetsChangedListener);
- mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener);
- updateTask();
- final WindowContainerToken token = mLaunchRootTask.token;
- if (token != null) {
- deleteRootTask(token);
- }
- mLaunchRootTask = null;
- mLaunchRootLeash = null;
- if (mHomeTask != null && mHomeTask.token != null) {
- final WindowContainerToken homeToken = mHomeTask.token;
- final WindowContainerTransaction wct = getWindowContainerTransaction();
- wct.setBounds(homeToken, null);
- mSyncQueue.queue(wct);
- }
- mHomeTask = null;
- unregisterOrganizer();
- }
-
- private void updateTask() {
- updateTask(getWindowContainerTransaction());
- }
-
- private void updateTask(WindowContainerTransaction wct) {
- if (mLaunchRootTask == null || mLaunchRootLeash == null) {
- return;
- }
- final Rect taskBounds = calculateBounds();
- final WindowContainerToken rootToken = mLaunchRootTask.token;
- wct.setBounds(rootToken, mEnabled ? taskBounds : null);
- wct.setLaunchRoot(rootToken,
- mEnabled ? CONTROLLED_WINDOWING_MODES : null,
- mEnabled ? CONTROLLED_ACTIVITY_TYPES : null);
- wct.reparentTasks(
- mEnabled ? null : rootToken /* currentParent */,
- mEnabled ? rootToken : null /* newParent */,
- CONTROLLED_WINDOWING_MODES,
- CONTROLLED_ACTIVITY_TYPES,
- true /* onTop */);
- wct.reorder(rootToken, mEnabled /* onTop */);
- mSyncQueue.queue(wct);
- if (mEnabled) {
- final SurfaceControl rootLeash = mLaunchRootLeash;
- mSyncQueue.runInSync(t -> {
- t.setPosition(rootLeash, taskBounds.left, taskBounds.top);
- t.setWindowCrop(rootLeash, mDisplayWidth, mDisplayHeight);
- });
- }
- }
-
- private Rect calculateBounds() {
- final Rect bounds = new Rect(0, 0, mDisplayWidth, mDisplayHeight);
- final InsetsSource navBarSource = mInsetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR);
- final InsetsSource taskBarSource = mInsetsState.peekSource(
- InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
- if (navBarSource != null && !navBarSource.getFrame().isEmpty()) {
- bounds.inset(navBarSource.calculateInsets(bounds, false /* ignoreVisibility */));
- } else if (taskBarSource != null && !taskBarSource.getFrame().isEmpty()) {
- bounds.inset(taskBarSource.calculateInsets(bounds, false /* ignoreVisibility */));
- } else {
- bounds.setEmpty();
- }
- return bounds;
- }
-
- private void updateBounds() {
- if (mLaunchRootTask == null) {
- return;
- }
- final WindowContainerTransaction wct = getWindowContainerTransaction();
- final Rect taskBounds = calculateBounds();
- wct.setBounds(mLaunchRootTask.token, taskBounds);
- wct.setBounds(mHomeTask.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight));
- mSyncQueue.queue(wct);
- final SurfaceControl finalLeash = mLaunchRootLeash;
- mSyncQueue.runInSync(t -> {
- t.setPosition(finalLeash, taskBounds.left, taskBounds.top);
- t.setWindowCrop(finalLeash, mDisplayWidth, mDisplayHeight);
- });
- }
-
- @VisibleForTesting
- WindowContainerTransaction getWindowContainerTransaction() {
- return new WindowContainerTransaction();
- }
-
- @Override
- public void dump(@NonNull PrintWriter pw, String prefix) {
- final String innerPrefix = prefix + " ";
- pw.println(prefix + TAG);
- pw.println(innerPrefix + " mEnabled=" + mEnabled);
- pw.println(innerPrefix + " mLaunchRootTask=" + mLaunchRootTask);
- pw.println(innerPrefix + " mLaunchRootLeash=" + mLaunchRootLeash);
- pw.println(innerPrefix + " mDisplayWidth=" + mDisplayWidth);
- pw.println(innerPrefix + " mDisplayHeight=" + mDisplayHeight);
- pw.println(innerPrefix + " mInsetsState=" + mInsetsState);
- super.dump(pw, innerPrefix);
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
index b310ee2095bf..71cc8df80cad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
@@ -104,7 +104,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager {
}
@Override
- protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+ protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
.setColorLayer()
.setBufferSize(mDisplayBounds.width(), mDisplayBounds.height())
@@ -113,7 +113,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager {
.setName(TAG)
.setCallsite("BackgroundWindowManager#attachToParentSurface");
mLeash = builder.build();
- b.setParent(mLeash);
+ return mLeash;
}
/** Inflates background view on to the root surface. */
@@ -122,7 +122,8 @@ public final class BackgroundWindowManager extends WindowlessWindowManager {
return false;
}
- mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+ mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this,
+ "BackgroundWindowManager");
mBackgroundView = (View) LayoutInflater.from(mContext)
.inflate(R.layout.background_panel, null /* root */);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
index 451afa08040c..38ce16489b06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
@@ -154,6 +154,8 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
@Override
public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
@NonNull SurfaceControl leash) {
+ leash.setUnreleasedWarningCallSite(
+ "OneHandedSiaplyAreaOrganizer.onDisplayAreaAppeared");
mDisplayAreaTokenMap.put(displayAreaInfo.token, leash);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java
index 5b9f0c41e31e..4ec1351aa22b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java
@@ -19,7 +19,7 @@ package com.android.wm.shell.onehanded;
import static android.view.Display.DEFAULT_DISPLAY;
import android.graphics.Rect;
-import android.hardware.input.InputManager;
+import android.hardware.input.InputManagerGlobal;
import android.os.Looper;
import android.view.InputChannel;
import android.view.InputEvent;
@@ -129,7 +129,7 @@ public class OneHandedTouchHandler implements OneHandedTransitionCallback {
private void updateIsEnabled() {
disposeInputChannel();
if (mIsEnabled) {
- mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ mInputMonitor = InputManagerGlobal.getInstance().monitorGestureInput(
"onehanded-touch", DEFAULT_DISPLAY);
try {
mMainExecutor.executeBlocking(() -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index 78de5f3e7a1f..3906599b7581 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -57,27 +57,35 @@ interface IPip {
in Rect destinationBounds, in SurfaceControl overlay) = 2;
/**
+ * Notifies the swiping Activity to PiP onto home transition is aborted
+ *
+ * @param taskId the Task id that the Activity and overlay are currently in.
+ * @param componentName ComponentName represents the Activity
+ */
+ oneway void abortSwipePipToHome(int taskId, in ComponentName componentName) = 3;
+
+ /**
* Sets listener to get pinned stack animation callbacks.
*/
- oneway void setPipAnimationListener(IPipAnimationListener listener) = 3;
+ oneway void setPipAnimationListener(IPipAnimationListener listener) = 4;
/**
* Sets the shelf height and visibility.
*/
- oneway void setShelfHeight(boolean visible, int shelfHeight) = 4;
+ oneway void setShelfHeight(boolean visible, int shelfHeight) = 5;
/**
* Sets the next pip animation type to be the alpha animation.
*/
- oneway void setPipAnimationTypeToAlpha() = 5;
+ oneway void setPipAnimationTypeToAlpha() = 6;
/**
* Sets the height and visibility of the Launcher keep clear area.
*/
- oneway void setLauncherKeepClearAreaHeight(boolean visible, int height) = 6;
+ oneway void setLauncherKeepClearAreaHeight(boolean visible, int height) = 7;
/**
* Sets the app icon size in pixel used by Launcher
*/
- oneway void setLauncherAppIconSize(int iconSizePx) = 7;
+ oneway void setLauncherAppIconSize(int iconSizePx) = 8;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS
index afddfab99a2b..ec09827fa4d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS
@@ -1,2 +1,3 @@
# WM shell sub-module pip owner
hwwang@google.com
+mateuszc@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 4c53f607a5f8..57cc28d1dde5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -30,14 +30,17 @@ import android.app.TaskInfo;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
+import android.os.SystemClock;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.transition.Transitions;
import java.lang.annotation.Retention;
@@ -49,7 +52,7 @@ import java.util.Objects;
*/
public class PipAnimationController {
static final float FRACTION_START = 0f;
- private static final float FRACTION_END = 1f;
+ static final float FRACTION_END = 1f;
public static final int ANIM_TYPE_BOUNDS = 0;
public static final int ANIM_TYPE_ALPHA = 1;
@@ -61,6 +64,14 @@ public class PipAnimationController {
@Retention(RetentionPolicy.SOURCE)
public @interface AnimationType {}
+ /**
+ * The alpha type is set for swiping to home. But the swiped task may not enter PiP. And if
+ * another task enters PiP by non-swipe ways, e.g. call API in foreground or switch to 3-button
+ * navigation, then the alpha type is unexpected. So use a timeout to avoid applying wrong
+ * animation style to an unrelated task.
+ */
+ private static final int ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS = 800;
+
public static final int TRANSITION_DIRECTION_NONE = 0;
public static final int TRANSITION_DIRECTION_SAME = 1;
public static final int TRANSITION_DIRECTION_TO_PIP = 2;
@@ -109,6 +120,9 @@ public class PipAnimationController {
});
private PipTransitionAnimator mCurrentAnimator;
+ @AnimationType
+ private int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+ private long mLastOneShotAlphaAnimationTime;
public PipAnimationController(PipSurfaceTransactionHelper helper) {
mSurfaceTransactionHelper = helper;
@@ -188,6 +202,11 @@ public class PipAnimationController {
return mCurrentAnimator;
}
+ /** Reset animator state to prevent it from being used after its lifetime. */
+ public void resetAnimatorState() {
+ mCurrentAnimator = null;
+ }
+
private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) {
animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper);
animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
@@ -217,6 +236,37 @@ public class PipAnimationController {
}
/**
+ * Sets the preferred enter animation type for one time. This is typically used to set the
+ * animation type to {@link PipAnimationController#ANIM_TYPE_ALPHA}.
+ * <p>
+ * For example, gesture navigation would first fade out the PiP activity, and the transition
+ * should be responsible to animate in (such as fade in) the PiP.
+ */
+ public void setOneShotEnterAnimationType(@AnimationType int animationType) {
+ mOneShotAnimationType = animationType;
+ if (animationType == ANIM_TYPE_ALPHA) {
+ mLastOneShotAlphaAnimationTime = SystemClock.uptimeMillis();
+ }
+ }
+
+ /** Returns the preferred animation type and consumes the one-shot type if needed. */
+ @AnimationType
+ public int takeOneShotEnterAnimationType() {
+ final int type = mOneShotAnimationType;
+ if (type == ANIM_TYPE_ALPHA) {
+ // Restore to default type.
+ mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+ if (SystemClock.uptimeMillis() - mLastOneShotAlphaAnimationTime
+ > ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "Alpha animation is expired. Use bounds animation.");
+ return ANIM_TYPE_BOUNDS;
+ }
+ }
+ return type;
+ }
+
+ /**
* Additional callback interface for PiP animation
*/
public static class PipAnimationCallback {
@@ -250,7 +300,7 @@ public class PipAnimationController {
* @return true if handled by the handler, false otherwise.
*/
public boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
- Rect destinationBounds) {
+ Rect destinationBounds, float alpha) {
return false;
}
}
@@ -278,6 +328,8 @@ public class PipAnimationController {
private PipSurfaceTransactionHelper mSurfaceTransactionHelper;
private @TransitionDirection int mTransitionDirection;
protected PipContentOverlay mContentOverlay;
+ // Flag to avoid double-end
+ private boolean mHasRequestedEnd;
private PipTransitionAnimator(TaskInfo taskInfo, SurfaceControl leash,
@AnimationType int animationType,
@@ -307,6 +359,7 @@ public class PipAnimationController {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
+ if (mHasRequestedEnd) return;
applySurfaceControlTransaction(mLeash,
mSurfaceControlTransactionFactory.getTransaction(),
animation.getAnimatedFraction());
@@ -314,6 +367,8 @@ public class PipAnimationController {
@Override
public void onAnimationEnd(Animator animation) {
+ if (mHasRequestedEnd) return;
+ mHasRequestedEnd = true;
mCurrentValue = mEndValue;
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
@@ -351,9 +406,10 @@ public class PipAnimationController {
}
boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx,
- Rect destinationBounds) {
+ Rect destinationBounds, float alpha) {
if (mPipTransactionHandler != null) {
- return mPipTransactionHandler.handlePipTransaction(leash, tx, destinationBounds);
+ return mPipTransactionHandler.handlePipTransaction(
+ leash, tx, destinationBounds, alpha);
}
return false;
}
@@ -498,7 +554,9 @@ public class PipAnimationController {
getSurfaceTransactionHelper().alpha(tx, leash, alpha)
.round(tx, leash, shouldApplyCornerRadius())
.shadow(tx, leash, shouldApplyShadowRadius());
- tx.apply();
+ if (!handlePipTransaction(leash, tx, destinationBounds, alpha)) {
+ tx.apply();
+ }
}
@Override
@@ -613,7 +671,7 @@ public class PipAnimationController {
.shadow(tx, leash, shouldApplyShadowRadius());
}
}
- if (!handlePipTransaction(leash, tx, bounds)) {
+ if (!handlePipTransaction(leash, tx, bounds, /* alpha= */ 1f)) {
tx.apply();
}
}
@@ -665,7 +723,9 @@ public class PipAnimationController {
.round(tx, leash, sourceBounds, bounds)
.shadow(tx, leash, shouldApplyShadowRadius());
}
- tx.apply();
+ if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) {
+ tx.apply();
+ }
}
private Rect computeInsets(float fraction) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index 24d0b996a3cb..f51eb5299dd9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -180,6 +180,35 @@ public class PipBoundsAlgorithm {
return null;
}
+
+ /**
+ * Returns the source hint rect if it is valid (if provided and is contained by the current
+ * task bounds, while not smaller than the destination bounds).
+ */
+ @Nullable
+ public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds,
+ Rect destinationBounds) {
+ Rect sourceRectHint = getValidSourceHintRect(params, sourceBounds);
+ if (!isSourceRectHintValidForEnterPip(sourceRectHint, destinationBounds)) {
+ sourceRectHint = null;
+ }
+ return sourceRectHint;
+ }
+
+ /**
+ * This is a situation in which the source rect hint on at least one axis is smaller
+ * than the destination bounds, which represents a problem because we would have to scale
+ * up that axis to fit the bounds. So instead, just fallback to the non-source hint
+ * animation in this case.
+ *
+ * @return {@code false} if the given source is too small to use for the entering animation.
+ */
+ static boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) {
+ return sourceRectHint != null
+ && sourceRectHint.width() > destinationBounds.width()
+ && sourceRectHint.height() > destinationBounds.height();
+ }
+
public float getDefaultAspectRatio() {
return mDefaultAspectRatio;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
index 92cf8cbf643e..9a775dff1f69 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
@@ -30,7 +30,6 @@ import android.graphics.Rect;
import android.os.RemoteException;
import android.util.ArraySet;
import android.util.Size;
-import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
@@ -79,6 +78,7 @@ public class PipBoundsState {
private final @NonNull Rect mExpandedBounds = new Rect();
private final @NonNull Rect mNormalMovementBounds = new Rect();
private final @NonNull Rect mExpandedMovementBounds = new Rect();
+ private final @NonNull PipDisplayLayoutState mPipDisplayLayoutState;
private final Point mMaxSize = new Point();
private final Point mMinSize = new Point();
private final @NonNull Context mContext;
@@ -89,8 +89,6 @@ public class PipBoundsState {
private final LauncherState mLauncherState = new LauncherState();
private final @Nullable PipSizeSpecHandler mPipSizeSpecHandler;
private @Nullable ComponentName mLastPipComponentName;
- private int mDisplayId = Display.DEFAULT_DISPLAY;
- private final @NonNull DisplayLayout mDisplayLayout = new DisplayLayout();
private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState();
private boolean mIsImeShowing;
private int mImeHeight;
@@ -129,10 +127,12 @@ public class PipBoundsState {
private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback;
private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>();
- public PipBoundsState(@NonNull Context context, PipSizeSpecHandler pipSizeSpecHandler) {
+ public PipBoundsState(@NonNull Context context, PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState) {
mContext = context;
reloadResources();
mPipSizeSpecHandler = pipSizeSpecHandler;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
}
/** Reloads the resources. */
@@ -299,31 +299,16 @@ public class PipBoundsState {
return mLastPipComponentName;
}
- /** Get the current display id. */
- public int getDisplayId() {
- return mDisplayId;
- }
-
- /** Set the current display id for the associated display layout. */
- public void setDisplayId(int displayId) {
- mDisplayId = displayId;
- }
-
/** Returns the display's bounds. */
@NonNull
public Rect getDisplayBounds() {
- return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
- }
-
- /** Update the display layout. */
- public void setDisplayLayout(@NonNull DisplayLayout displayLayout) {
- mDisplayLayout.set(displayLayout);
+ return mPipDisplayLayoutState.getDisplayBounds();
}
/** Get a copy of the display layout. */
@NonNull
public DisplayLayout getDisplayLayout() {
- return new DisplayLayout(mDisplayLayout);
+ return mPipDisplayLayoutState.getDisplayLayout();
}
@VisibleForTesting
@@ -613,7 +598,6 @@ public class PipBoundsState {
pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds);
pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName);
pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio);
- pw.println(innerPrefix + "mDisplayId=" + mDisplayId);
pw.println(innerPrefix + "mStashedState=" + mStashedState);
pw.println(innerPrefix + "mStashOffset=" + mStashOffset);
pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index 9fa57cacb11f..c701b9581ca2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -206,6 +206,7 @@ public abstract class PipContentOverlay {
tx.show(mLeash);
tx.setLayer(mLeash, Integer.MAX_VALUE);
tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+ tx.setAlpha(mLeash, 0f);
tx.reparent(mLeash, parentLeash);
tx.apply();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java
new file mode 100644
index 000000000000..0f76af48199f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java
@@ -0,0 +1,91 @@
+/*
+ * 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.pip;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.dagger.WMSingleton;
+
+import java.io.PrintWriter;
+
+import javax.inject.Inject;
+
+/**
+ * Acts as a source of truth for display related information for PIP.
+ */
+@WMSingleton
+public class PipDisplayLayoutState {
+ private static final String TAG = PipDisplayLayoutState.class.getSimpleName();
+
+ private Context mContext;
+ private int mDisplayId;
+ @NonNull private DisplayLayout mDisplayLayout;
+
+ @Inject
+ public PipDisplayLayoutState(Context context) {
+ mContext = context;
+ mDisplayLayout = new DisplayLayout();
+ }
+
+ /** Update the display layout. */
+ public void setDisplayLayout(@NonNull DisplayLayout displayLayout) {
+ mDisplayLayout.set(displayLayout);
+ }
+
+ /** Get a copy of the display layout. */
+ @NonNull
+ public DisplayLayout getDisplayLayout() {
+ return new DisplayLayout(mDisplayLayout);
+ }
+
+ /** Get the display bounds */
+ @NonNull
+ public Rect getDisplayBounds() {
+ return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
+ }
+
+ /**
+ * Apply a rotation to this layout and its parameters.
+ * @param targetRotation
+ */
+ public void rotateTo(@Surface.Rotation int targetRotation) {
+ mDisplayLayout.rotateTo(mContext.getResources(), targetRotation);
+ }
+
+ /** Get the current display id */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /** Set the current display id for the associated display layout. */
+ public void setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ }
+
+ /** Dumps internal state. */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mDisplayId=" + mDisplayId);
+ pw.println(innerPrefix + "getDisplayBounds=" + getDisplayBounds());
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
index 65a12d629c5a..2590cab9ff2e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
@@ -252,13 +252,16 @@ public class PipMediaController {
// It can be removed when min_sdk of the app is set to 31 or greater.
@SuppressLint("NewApi")
private List<RemoteAction> getMediaActions() {
- if (mMediaController == null || mMediaController.getPlaybackState() == null) {
+ // Cache the PlaybackState since it's a Binder call.
+ final PlaybackState playbackState;
+ if (mMediaController == null
+ || (playbackState = mMediaController.getPlaybackState()) == null) {
return Collections.emptyList();
}
ArrayList<RemoteAction> mediaActions = new ArrayList<>();
- boolean isPlaying = mMediaController.getPlaybackState().isActive();
- long actions = mMediaController.getPlaybackState().getActions();
+ boolean isPlaying = playbackState.isActive();
+ long actions = playbackState.getActions();
// Prev action
mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
index 16f1d1c2944c..0775f5279e31 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
@@ -26,11 +26,14 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.RemoteAction;
+import android.content.Context;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.view.SurfaceControl;
import android.view.WindowManager;
+import com.android.wm.shell.R;
+
import java.util.List;
/**
@@ -42,6 +45,13 @@ public interface PipMenuController {
String MENU_WINDOW_TITLE = "PipMenuView";
/**
+ * Used with
+ * {@link PipMenuController#movePipMenu(SurfaceControl, SurfaceControl.Transaction, Rect,
+ * float)} to indicate that we don't want to affect the alpha value of the menu surfaces.
+ */
+ float ALPHA_NO_CHANGE = -1f;
+
+ /**
* Called when
* {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)}
* is called.
@@ -82,8 +92,8 @@ public interface PipMenuController {
* need to synchronize the movements on the same frame as PiP.
*/
default void movePipMenu(@Nullable SurfaceControl pipLeash,
- @Nullable SurfaceControl.Transaction t,
- Rect destinationBounds) {}
+ @Nullable SurfaceControl.Transaction t, Rect destinationBounds, float alpha) {
+ }
/**
* Update the PiP menu with the given bounds for re-layout purposes.
@@ -97,16 +107,20 @@ public interface PipMenuController {
/**
* Returns a default LayoutParams for the PIP Menu.
+ * @param context the context.
* @param width the PIP stack width.
* @param height the PIP stack height.
*/
- default WindowManager.LayoutParams getPipMenuLayoutParams(String title, int width, int height) {
+ default WindowManager.LayoutParams getPipMenuLayoutParams(Context context, String title,
+ int width, int height) {
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(width, height,
TYPE_APPLICATION_OVERLAY,
FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY | FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY;
lp.setTitle(title);
+ lp.accessibilityTitle = context.getResources().getString(
+ R.string.pip_menu_accessibility_title);
return lp;
}
}
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 c4b5470f461a..08da4857a0b0 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
@@ -27,6 +27,7 @@ import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP;
import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS;
import static com.android.wm.shell.pip.PipAnimationController.FRACTION_START;
@@ -62,9 +63,7 @@ import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.RemoteException;
-import android.os.SystemClock;
import android.os.SystemProperties;
-import android.util.Log;
import android.view.Choreographer;
import android.view.Display;
import android.view.Surface;
@@ -73,7 +72,6 @@ import android.window.TaskOrganizer;
import android.window.TaskSnapshot;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
-import android.window.WindowContainerTransactionCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
@@ -81,13 +79,11 @@ import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.pip.phone.PipMotionHelper;
-import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.transition.Transitions;
@@ -112,13 +108,6 @@ import java.util.function.IntConsumer;
public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
DisplayController.OnDisplaysChangedListener, ShellTaskOrganizer.FocusListener {
private static final String TAG = PipTaskOrganizer.class.getSimpleName();
- private static final boolean DEBUG = false;
- /**
- * The alpha type is set for swiping to home. But the swiped task may not enter PiP. And if
- * another task enters PiP by non-swipe ways, e.g. call API in foreground or switch to 3-button
- * navigation, then the alpha type is unexpected.
- */
- private static final int ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS = 1000;
/**
* The fixed start delay in ms when fading out the content overlay from bounds animation.
@@ -130,7 +119,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private final Context mContext;
private final SyncTransactionQueue mSyncTransactionQueue;
private final PipBoundsState mPipBoundsState;
- private final PipSizeSpecHandler mPipSizeSpecHandler;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
private final @NonNull PipMenuController mPipMenuController;
private final PipAnimationController mPipAnimationController;
@@ -148,19 +137,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// the runnable to execute after WindowContainerTransactions is applied to finish resizing pip
private Runnable mPipFinishResizeWCTRunnable;
- private final WindowContainerTransactionCallback mPipFinishResizeWCTCallback =
- new WindowContainerTransactionCallback() {
- @Override
- public void onTransactionReady(int id, SurfaceControl.Transaction t) {
- t.apply();
-
- // execute the runnable if non-null after WCT is applied to finish resizing pip
- if (mPipFinishResizeWCTRunnable != null) {
- mPipFinishResizeWCTRunnable.run();
- mPipFinishResizeWCTRunnable = null;
- }
+ private void maybePerformFinishResizeCallback() {
+ if (mPipFinishResizeWCTRunnable != null) {
+ mPipFinishResizeWCTRunnable.run();
+ mPipFinishResizeWCTRunnable = null;
}
- };
+ }
// These callbacks are called on the update thread
private final PipAnimationController.PipAnimationCallback mPipAnimationCallback =
@@ -180,6 +162,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
final int direction = animator.getTransitionDirection();
if (mIsCancelled) {
sendOnPipTransitionFinished(direction);
+ maybePerformFinishResizeCallback();
return;
}
final int animationType = animator.getAnimationType();
@@ -204,6 +187,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
|| isRemovePipDirection(direction);
if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP
|| isExitPipDirection) {
+ // execute the finish resize callback if needed after the transaction is committed
+ tx.addTransactionCommittedListener(mMainExecutor,
+ PipTaskOrganizer.this::maybePerformFinishResizeCallback);
+
// Finish resize as long as we're not exiting PIP, or, if we are, only if this is
// the end of an exit PIP animation.
// This is necessary in case there was a resize animation ongoing when exit PIP
@@ -258,7 +245,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}, null);
}
- private boolean shouldSyncPipTransactionWithMenu() {
+ protected boolean shouldSyncPipTransactionWithMenu() {
return mPipMenuController.isMenuVisible();
}
@@ -286,9 +273,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
new PipAnimationController.PipTransactionHandler() {
@Override
public boolean handlePipTransaction(SurfaceControl leash,
- SurfaceControl.Transaction tx, Rect destinationBounds) {
+ SurfaceControl.Transaction tx, Rect destinationBounds, float alpha) {
if (shouldSyncPipTransactionWithMenu()) {
- mPipMenuController.movePipMenu(leash, tx, destinationBounds);
+ mPipMenuController.movePipMenu(leash, tx, destinationBounds, alpha);
return true;
}
return false;
@@ -303,8 +290,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private WindowContainerToken mToken;
private SurfaceControl mLeash;
protected PipTransitionState mPipTransitionState;
- private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
- private long mLastOneShotAlphaAnimationTime;
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
protected PictureInPictureParams mPictureInPictureParams;
@@ -342,7 +327,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@NonNull SyncTransactionQueue syncTransactionQueue,
@NonNull PipTransitionState pipTransitionState,
@NonNull PipBoundsState pipBoundsState,
- @NonNull PipSizeSpecHandler pipSizeSpecHandler,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState,
@NonNull PipBoundsAlgorithm boundsHandler,
@NonNull PipMenuController pipMenuController,
@NonNull PipAnimationController pipAnimationController,
@@ -358,7 +343,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mSyncTransactionQueue = syncTransactionQueue;
mPipTransitionState = pipTransitionState;
mPipBoundsState = pipBoundsState;
- mPipSizeSpecHandler = pipSizeSpecHandler;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
mPipBoundsAlgorithm = boundsHandler;
mPipMenuController = pipMenuController;
mPipTransitionController = pipTransitionController;
@@ -392,6 +377,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
return mPipTransitionController;
}
+ PipAnimationController.PipTransactionHandler getPipTransactionHandler() {
+ return mPipTransactionHandler;
+ }
+
public Rect getCurrentOrAnimatingBounds() {
PipAnimationController.PipTransitionAnimator animator =
mPipAnimationController.getCurrentAnimator();
@@ -424,15 +413,23 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
/**
- * Sets the preferred animation type for one time.
- * This is typically used to set the animation type to
+ * Override if the PiP should always use a fade-in animation during PiP entry.
+ *
+ * @return true if the mOneShotAnimationType should always be
* {@link PipAnimationController#ANIM_TYPE_ALPHA}.
*/
- public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) {
- mOneShotAnimationType = animationType;
- if (animationType == ANIM_TYPE_ALPHA) {
- mLastOneShotAlphaAnimationTime = SystemClock.uptimeMillis();
- }
+ protected boolean shouldAlwaysFadeIn() {
+ return false;
+ }
+
+ /**
+ * Whether the menu should get attached as early as possible when entering PiP.
+ *
+ * @return whether the menu should be attached before
+ * {@link PipBoundsAlgorithm#getEntryDestinationBounds()} is called.
+ */
+ protected boolean shouldAttachMenuEarly() {
+ return false;
}
/**
@@ -441,6 +438,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "startSwipePipToHome: %s, state=%s", componentName, mPipTransitionState);
mPipTransitionState.setInSwipePipToHomeTransition(true);
sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo);
@@ -448,11 +447,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
/**
- * Callback when launcher finishes swipe-pip-to-home operation.
+ * Callback when launcher finishes preparation of swipe-pip-to-home operation.
* Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards.
*/
public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
SurfaceControl overlay) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "stopSwipePipToHome: %s, state=%s", componentName, mPipTransitionState);
// do nothing if there is no startSwipePipToHome being called before
if (!mPipTransitionState.getInSwipePipToHomeTransition()) {
return;
@@ -465,13 +466,29 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// to the actual Task surface now.
// PipTransition is responsible to fade it out and cleanup when finishing the enter PIP
// transition.
- final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ final SurfaceControl.Transaction t = mSurfaceControlTransactionFactory.getTransaction();
mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t);
t.setLayer(overlay, Integer.MAX_VALUE);
t.apply();
}
}
+ /**
+ * Callback when launcher aborts swipe-pip-to-home operation.
+ */
+ public void abortSwipePipToHome(int taskId, ComponentName componentName) {
+ if (!mPipTransitionState.getInSwipePipToHomeTransition()) {
+ return;
+ }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "Abort swipe-pip-to-home for %s", componentName);
+ sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP);
+ // Cleanup internal states
+ mPipTransitionState.setInSwipePipToHomeTransition(false);
+ mPictureInPictureParams = null;
+ mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED);
+ }
+
public ActivityManager.RunningTaskInfo getTaskInfo() {
return mTaskInfo;
}
@@ -509,29 +526,23 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
return;
}
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (isLaunchIntoPipTask()) {
exitLaunchIntoPipTask(wct);
return;
}
- if (ENABLE_SHELL_TRANSITIONS) {
- if (requestEnterSplit && mSplitScreenOptional.isPresent()) {
- mSplitScreenOptional.get().prepareEnterSplitScreen(wct, mTaskInfo,
- isPipTopLeft()
- ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
- mPipTransitionController.startExitTransition(
- TRANSIT_EXIT_PIP_TO_SPLIT, wct, null /* destinationBounds */);
- return;
- }
- }
-
- final Rect destinationBounds = getExitDestinationBounds();
+ final Rect destinationBounds = new Rect(getExitDestinationBounds());
final int direction = syncWithSplitScreenBounds(destinationBounds, requestEnterSplit)
? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN
: TRANSITION_DIRECTION_LEAVE_PIP;
+ // For exiting to fullscreen, the windowing mode of task will be changed to fullscreen
+ // until the animation is finished. Otherwise if the activity is resumed and focused at the
+ // begin of aniamtion, the app may do something too early to distub the animation.
- if (Transitions.ENABLE_SHELL_TRANSITIONS && direction == TRANSITION_DIRECTION_LEAVE_PIP) {
+ if (Transitions.SHELL_TRANSITIONS_ROTATION) {
// When exit to fullscreen with Shell transition enabled, we update the Task windowing
// mode directly so that it can also trigger display rotation and visibility update in
// the same transition if there will be any.
@@ -540,6 +551,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// destinationBounds calculated above will be incorrect if this is with rotation.
wct.setBounds(mToken, null);
} else {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "exitPip: %s, dest=%s", mTaskInfo.topActivity, destinationBounds);
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds,
@@ -561,9 +574,29 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ if (requestEnterSplit && mSplitScreenOptional.isPresent()) {
+ wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
+ mSplitScreenOptional.get().prepareEnterSplitScreen(wct, mTaskInfo,
+ isPipToTopLeft()
+ ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ mPipTransitionController.startExitTransition(
+ TRANSIT_EXIT_PIP_TO_SPLIT, wct, destinationBounds);
+ return;
+ }
+
+ if (mSplitScreenOptional.isPresent()) {
+ // If pip activity will reparent to origin task case and if the origin task still
+ // under split root, apply exit split transaction to make it expand to fullscreen.
+ SplitScreenController split = mSplitScreenOptional.get();
+ if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) {
+ split.prepareExitSplitScreen(wct, split.getStageOfTask(
+ mTaskInfo.lastParentTaskIdBeforePip));
+ }
+ }
mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds);
return;
}
+
if (mSplitScreenOptional.isPresent()) {
// If pip activity will reparent to origin task case and if the origin task still under
// split root, just exit split screen here to ensure it could expand to fullscreen.
@@ -607,7 +640,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
removePip();
}
- private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) {
+ void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) {
// Reset the final windowing mode.
wct.setWindowingMode(mToken, getOutPipWindowingMode());
// Simply reset the activity mode set prior to the animation running.
@@ -618,11 +651,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
* Removes PiP immediately.
*/
public void removePip() {
- if (!mPipTransitionState.isInPip() || mToken == null) {
+ if (!mPipTransitionState.isInPip() || mToken == null || mLeash == null) {
ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Not allowed to removePip in current state"
- + " mState=%d mToken=%s", TAG, mPipTransitionState.getTransitionState(),
- mToken);
+ + " mState=%d mToken=%s mLeash=%s", TAG,
+ mPipTransitionState.getTransitionState(), mToken, mLeash);
return;
}
@@ -637,9 +670,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
animator.setInterpolator(Interpolators.ALPHA_OUT);
animator.start();
mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "removePip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
}
private void removePipImmediately() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "removePipImmediately: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(mToken, null);
@@ -688,7 +725,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// If the displayId of the task is different than what PipBoundsHandler has, then update
// it. This is possible if we entered PiP on an external display.
- if (info.displayId != mPipBoundsState.getDisplayId()
+ if (info.displayId != mPipDisplayLayoutState.getDisplayId()
&& mOnDisplayIdChangeCallback != null) {
mOnDisplayIdChangeCallback.accept(info.displayId);
}
@@ -704,6 +741,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
mPipUiEventLoggerLogger.log(uiEventEnum);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onTaskAppeared: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
if (mPipTransitionState.getInSwipePipToHomeTransition()) {
if (!mWaitForFixedRotation) {
onEndOfSwipePipToHomeTransition();
@@ -715,47 +754,50 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
return;
}
- if (mOneShotAnimationType == ANIM_TYPE_ALPHA
- && SystemClock.uptimeMillis() - mLastOneShotAlphaAnimationTime
- > ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Alpha animation is expired. Use bounds animation.", TAG);
- mOneShotAnimationType = ANIM_TYPE_BOUNDS;
- }
-
+ final int animationType = shouldAlwaysFadeIn()
+ ? ANIM_TYPE_ALPHA
+ : mPipAnimationController.takeOneShotEnterAnimationType();
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mPipTransitionController.setEnterAnimationType(animationType);
// For Shell transition, we will animate the window in PipTransition#startAnimation
// instead of #onTaskAppeared.
return;
}
if (mWaitForFixedRotation) {
- onTaskAppearedWithFixedRotation();
+ onTaskAppearedWithFixedRotation(animationType);
return;
}
+ if (shouldAttachMenuEarly()) {
+ mPipMenuController.attach(mLeash);
+ }
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
Objects.requireNonNull(destinationBounds, "Missing destination bounds");
final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
- if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
- mPipMenuController.attach(mLeash);
+ if (animationType == ANIM_TYPE_BOUNDS) {
+ if (!shouldAttachMenuEarly()) {
+ mPipMenuController.attach(mLeash);
+ }
final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
info.pictureInPictureParams, currentBounds);
scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */,
sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration,
null /* updateBoundsCallback */);
mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
- } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ } else if (animationType == ANIM_TYPE_ALPHA) {
enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration);
- mOneShotAnimationType = ANIM_TYPE_BOUNDS;
} else {
- throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType);
+ throw new RuntimeException("Unrecognized animation type: " + animationType);
}
}
- private void onTaskAppearedWithFixedRotation() {
- if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ private void onTaskAppearedWithFixedRotation(int animationType) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onTaskAppearedWithFixedRotation: %s, state=%s animationType=%d",
+ mTaskInfo.topActivity, mPipTransitionState, animationType);
+ if (animationType == ANIM_TYPE_ALPHA) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Defer entering PiP alpha animation, fixed rotation is ongoing", TAG);
// If deferred, hside the surface till fixed rotation is completed.
@@ -764,7 +806,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
tx.setAlpha(mLeash, 0f);
tx.show(mLeash);
tx.apply();
- mOneShotAnimationType = ANIM_TYPE_BOUNDS;
return;
}
final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
@@ -824,6 +865,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private void onEndOfSwipePipToHomeTransition() {
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mPipTransitionController.setEnterAnimationType(ANIM_TYPE_BOUNDS);
return;
}
@@ -854,7 +896,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@Nullable SurfaceControl.Transaction boundsChangeTransaction) {
// PiP menu is attached late in the process here to avoid any artifacts on the leash
// caused by addShellRoot when in gesture navigation mode.
- mPipMenuController.attach(mLeash);
+ if (!shouldAttachMenuEarly()) {
+ mPipMenuController.attach(mLeash);
+ }
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED);
wct.setBounds(mToken, destinationBounds);
@@ -899,6 +943,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
@Override
public void onTaskVanished(ActivityManager.RunningTaskInfo info) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onTaskVanished: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
return;
}
@@ -940,6 +986,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mPipBoundsState.setOverrideMinSize(
mPipBoundsAlgorithm.getMinimalSize(info.topActivityInfo));
final PictureInPictureParams newParams = info.pictureInPictureParams;
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onTaskInfoChanged: %s, state=%s oldParams=%s newParams=%s",
+ mTaskInfo.topActivity, mPipTransitionState, mPictureInPictureParams, newParams);
// mPictureInPictureParams is only null if there is no PiP
if (newParams == null || mPictureInPictureParams == null) {
@@ -980,6 +1029,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@Override
public void onFixedRotationStarted(int displayId, int newRotation) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onFixedRotationStarted: %s, state=%s", mTaskInfo, mPipTransitionState);
mNextRotation = newRotation;
mWaitForFixedRotation = true;
@@ -1001,10 +1052,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@Override
public void onFixedRotationFinished(int displayId) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onFixedRotationFinished: %s, state=%s", mTaskInfo, mPipTransitionState);
if (!mWaitForFixedRotation) {
return;
}
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mPipTransitionController.onFixedRotationFinished();
clearWaitForFixedRotation();
return;
}
@@ -1035,9 +1089,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
/** Called when exiting PIP transition is finished to do the state cleanup. */
void onExitPipFinished(TaskInfo info) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onExitPipFinished: %s, state=%s leash=%s",
+ info.topActivity, mPipTransitionState, mLeash);
if (mLeash == null) {
// TODO(239461594): Remove once the double call to onExitPipFinished() is fixed
- Log.w(TAG, "Warning, onExitPipFinished() called multiple times in the same sessino");
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "Warning, onExitPipFinished() called multiple times in the same session");
return;
}
@@ -1085,6 +1143,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
/** Explicitly set the visibility of PiP window. */
public void setPipVisibility(boolean visible) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "setPipVisibility: %s, state=%s visible=%s",
+ (mTaskInfo != null ? mTaskInfo.topActivity : null), mPipTransitionState, visible);
if (!isInPip()) {
return;
}
@@ -1126,15 +1187,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
&& (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP);
if ((mPipTransitionState.getInSwipePipToHomeTransition()
|| waitForFixedRotationOnEnteringPip) && fromRotation) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Skip onMovementBoundsChanged on rotation change"
- + " InSwipePipToHomeTransition=%b"
- + " mWaitForFixedRotation=%b"
- + " getTransitionState=%d", TAG,
- mPipTransitionState.getInSwipePipToHomeTransition(), mWaitForFixedRotation,
- mPipTransitionState.getTransitionState());
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Skip onMovementBoundsChanged on rotation change"
+ + " InSwipePipToHomeTransition=%b"
+ + " mWaitForFixedRotation=%b"
+ + " getTransitionState=%d", TAG,
+ mPipTransitionState.getInSwipePipToHomeTransition(), mWaitForFixedRotation,
+ mPipTransitionState.getTransitionState());
return;
}
final PipAnimationController.PipTransitionAnimator animator =
@@ -1388,7 +1447,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
.scale(tx, mLeash, startBounds, toBounds, degrees)
.round(tx, mLeash, startBounds, toBounds);
if (shouldSyncPipTransactionWithMenu()) {
- mPipMenuController.movePipMenu(mLeash, tx, toBounds);
+ mPipMenuController.movePipMenu(mLeash, tx, toBounds, PipMenuController.ALPHA_NO_CHANGE);
} else {
tx.apply();
}
@@ -1429,8 +1488,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
if (mLeash == null || !mLeash.isValid()) {
- Log.e(TAG, String.format("scheduleFinishResizePip with null leash! mState=%d",
- mPipTransitionState.getTransitionState()));
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: scheduleFinishResizePip with null leash! mState=%d",
+ TAG, mPipTransitionState.getTransitionState());
return;
}
@@ -1525,6 +1585,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
if (snapshotSurface != null) {
mSyncTransactionQueue.queue(wct);
mSyncTransactionQueue.runInSync(t -> {
+ // reset the pinch gesture
+ maybePerformFinishResizeCallback();
+
// Scale the snapshot from its pre-resize bounds to the post-resize bounds.
mSurfaceTransactionHelper.scale(t, snapshotSurface, preResizeBounds,
snapshotDest);
@@ -1554,7 +1617,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
if (!isInPip()) {
return;
}
- mPipMenuController.movePipMenu(null, null, destinationBounds);
+ mPipMenuController.movePipMenu(null, null, destinationBounds,
+ PipMenuController.ALPHA_NO_CHANGE);
mPipMenuController.updateMenuBounds(destinationBounds);
}
@@ -1562,6 +1626,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@PipAnimationController.TransitionDirection int direction,
SurfaceControl.Transaction tx,
WindowContainerTransaction wct) {
+ if (mLeash == null || !mLeash.isValid()) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Invalid leash on prepareFinishResizeTransaction: %s", TAG, mLeash);
+ return;
+ }
final Rect taskBounds;
if (isInPipDirection(direction)) {
// If we are animating from fullscreen using a bounds animation, then reset the
@@ -1599,19 +1668,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mSplitScreenOptional.ifPresent(splitScreenController ->
splitScreenController.enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct));
} else {
- mTaskOrganizer.applySyncTransaction(wct, mPipFinishResizeWCTCallback);
- }
- }
-
- private boolean isPipTopLeft() {
- if (!mSplitScreenOptional.isPresent()) {
- return false;
+ mTaskOrganizer.applyTransaction(wct);
}
- final Rect topLeft = new Rect();
- final Rect bottomRight = new Rect();
- mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight);
-
- return topLeft.contains(mPipBoundsState.getBounds());
}
private boolean isPipToTopLeft() {
@@ -1641,8 +1699,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
"%s: Abort animation, invalid leash", TAG);
return null;
}
- if (isInPipDirection(direction)
- && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) {
+ if (isInPipDirection(direction) && !PipBoundsAlgorithm
+ .isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) {
// The given source rect hint is too small for enter PiP animation, reset it to null.
sourceHintRect = null;
}
@@ -1670,8 +1728,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// Similar to auto-enter-pip transition, we use content overlay when there is no
// source rect hint to enter PiP use bounds animation.
if (sourceHintRect == null) {
+ // We use content overlay when there is no source rect hint to enter PiP use bounds
+ // animation.
+ // TODO(b/272819817): cleanup the null-check and extra logging.
+ final boolean hasTopActivityInfo = mTaskInfo.topActivityInfo != null;
+ if (!hasTopActivityInfo) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "%s: TaskInfo.topActivityInfo is null", TAG);
+ }
if (SystemProperties.getBoolean(
- "persist.wm.debug.enable_pip_app_icon_overlay", true)) {
+ "persist.wm.debug.enable_pip_app_icon_overlay", true)
+ && hasTopActivityInfo) {
animator.setAppIconContentOverlay(
mContext, currentBounds, mTaskInfo.topActivityInfo,
mPipBoundsState.getLauncherState().getAppIconSizePx());
@@ -1699,15 +1766,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
return animator;
}
- /** Computes destination bounds in old rotation and returns source hint rect if available. */
+ /** Computes destination bounds in old rotation and returns source hint rect if available.
+ *
+ * Note: updates the internal state of {@link PipDisplayLayoutState} by applying a rotation
+ * transformation onto the display layout.
+ */
private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction,
Rect outDestinationBounds, Rect sourceHintRect) {
if (direction == TRANSITION_DIRECTION_TO_PIP) {
- DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
-
- layoutCopy.rotateTo(mContext.getResources(), mNextRotation);
- mPipBoundsState.setDisplayLayout(layoutCopy);
- mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+ mPipDisplayLayoutState.rotateTo(mNextRotation);
final Rect displayBounds = mPipBoundsState.getDisplayBounds();
outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
@@ -1732,20 +1799,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
/**
- * This is a situation in which the source rect hint on at least one axis is smaller
- * than the destination bounds, which represents a problem because we would have to scale
- * up that axis to fit the bounds. So instead, just fallback to the non-source hint
- * animation in this case.
- *
- * @return {@code false} if the given source is too small to use for the entering animation.
- */
- private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) {
- return sourceRectHint != null
- && sourceRectHint.width() > destinationBounds.width()
- && sourceRectHint.height() > destinationBounds.height();
- }
-
- /**
* Sync with {@link SplitScreenController} on destination bounds if PiP is going to
* split screen.
*
@@ -1753,14 +1806,26 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
* @return {@code true} if destinationBounds is altered for split screen
*/
private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut, boolean enterSplit) {
- if (!enterSplit || !mSplitScreenOptional.isPresent()) {
+ if (mSplitScreenOptional.isEmpty()) {
+ return false;
+ }
+ final SplitScreenController split = mSplitScreenOptional.get();
+ final int position = mTaskInfo.lastParentTaskIdBeforePip > 0
+ ? split.getSplitPosition(mTaskInfo.lastParentTaskIdBeforePip)
+ : SPLIT_POSITION_UNDEFINED;
+ if (position == SPLIT_POSITION_UNDEFINED && !enterSplit) {
return false;
}
final Rect topLeft = new Rect();
final Rect bottomRight = new Rect();
- mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight);
- destinationBoundsOut.set(isPipToTopLeft() ? topLeft : bottomRight);
- return true;
+ split.getStageBounds(topLeft, bottomRight);
+ if (enterSplit) {
+ destinationBoundsOut.set(isPipToTopLeft() ? topLeft : bottomRight);
+ return true;
+ }
+ // Moving to an existing split task.
+ destinationBoundsOut.set(position == SPLIT_POSITION_TOP_OR_LEFT ? topLeft : bottomRight);
+ return false;
}
/**
@@ -1834,6 +1899,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
animator::clearContentOverlay);
}
PipAnimationController.quietCancel(animator);
+ mPipAnimationController.resetAnimatorState();
}
}
@@ -1860,8 +1926,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
+ " binder=" + (mToken != null ? mToken.asBinder() : null));
pw.println(innerPrefix + "mLeash=" + mLeash);
pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState());
- pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType);
pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
+ mPipTransitionController.dump(pw, innerPrefix);
}
@Override
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 8e7b30eb60d0..e3d53fc415db 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
@@ -26,6 +26,7 @@ import static android.view.Surface.ROTATION_90;
import static android.view.WindowManager.TRANSIT_CHANGE;
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 android.window.TransitionInfo.FLAG_IS_DISPLAY;
@@ -40,13 +41,11 @@ import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
-import static com.android.wm.shell.transition.Transitions.isOpeningType;
import android.animation.Animator;
import android.app.ActivityManager;
import android.app.TaskInfo;
import android.content.Context;
-import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
@@ -54,6 +53,7 @@ import android.os.SystemProperties;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
+import android.window.TaskSnapshot;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerToken;
@@ -65,14 +65,14 @@ import androidx.annotation.Nullable;
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.DisplayLayout;
-import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.CounterRotatorHelper;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
+import java.io.PrintWriter;
import java.util.Optional;
/**
@@ -85,16 +85,18 @@ public class PipTransition extends PipTransitionController {
private final Context mContext;
private final PipTransitionState mPipTransitionState;
- private final PipSizeSpecHandler mPipSizeSpecHandler;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
private final int mEnterExitAnimationDuration;
private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
private final Optional<SplitScreenController> mSplitScreenOptional;
- private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+ private @PipAnimationController.AnimationType int mEnterAnimationType = ANIM_TYPE_BOUNDS;
private Transitions.TransitionFinishCallback mFinishCallback;
private SurfaceControl.Transaction mFinishTransaction;
private final Rect mExitDestinationBounds = new Rect();
@Nullable
private IBinder mExitTransition;
+ @Nullable
+ private IBinder mMoveToBackTransition;
private IBinder mRequestedEnterTransition;
private WindowContainerToken mRequestedEnterTask;
/** The Task window that is currently in PIP windowing mode. */
@@ -111,12 +113,23 @@ public class PipTransition extends PipTransitionController {
/** Whether the PIP window has fade out for fixed rotation. */
private boolean mHasFadeOut;
+ /** Used for setting transform to a transaction from animator. */
+ private final PipAnimationController.PipTransactionHandler mTransactionConsumer =
+ new PipAnimationController.PipTransactionHandler() {
+ @Override
+ public boolean handlePipTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, Rect destinationBounds, float alpha) {
+ // Only set the operation to transaction but do not apply.
+ return true;
+ }
+ };
+
public PipTransition(Context context,
@NonNull ShellInit shellInit,
@NonNull ShellTaskOrganizer shellTaskOrganizer,
@NonNull Transitions transitions,
PipBoundsState pipBoundsState,
- PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
PipTransitionState pipTransitionState,
PipMenuController pipMenuController,
PipBoundsAlgorithm pipBoundsAlgorithm,
@@ -127,7 +140,7 @@ public class PipTransition extends PipTransitionController {
pipBoundsAlgorithm, pipAnimationController);
mContext = context;
mPipTransitionState = pipTransitionState;
- mPipSizeSpecHandler = pipSizeSpecHandler;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
mEnterExitAnimationDuration = context.getResources()
.getInteger(R.integer.config_pipResizeAnimationDuration);
mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
@@ -135,20 +148,6 @@ public class PipTransition extends PipTransitionController {
}
@Override
- public void setIsFullAnimation(boolean isFullAnimation) {
- setOneShotAnimationType(isFullAnimation ? ANIM_TYPE_BOUNDS : ANIM_TYPE_ALPHA);
- }
-
- /**
- * Sets the preferred animation type for one time.
- * This is typically used to set the animation type to
- * {@link PipAnimationController#ANIM_TYPE_ALPHA}.
- */
- private void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) {
- mOneShotAnimationType = animationType;
- }
-
- @Override
public void startExitTransition(int type, WindowContainerTransaction out,
@Nullable Rect destinationBounds) {
if (destinationBounds != null) {
@@ -177,9 +176,10 @@ public class PipTransition extends PipTransitionController {
// Exiting PIP.
final int type = info.getType();
- if (transition.equals(mExitTransition)) {
+ if (transition.equals(mExitTransition) || transition.equals(mMoveToBackTransition)) {
mExitDestinationBounds.setEmpty();
mExitTransition = null;
+ mMoveToBackTransition = null;
mHasFadeOut = false;
if (mFinishCallback != null) {
callFinishCallback(null /* wct */);
@@ -207,6 +207,8 @@ public class PipTransition extends PipTransitionController {
startExitToSplitAnimation(info, startTransaction, finishTransaction,
finishCallback, pipTaskInfo);
break;
+ case TRANSIT_TO_BACK:
+ // pass through here is intended
case TRANSIT_REMOVE_PIP:
removePipImmediately(info, startTransaction, finishTransaction, finishCallback,
pipTaskInfo);
@@ -249,11 +251,6 @@ public class PipTransition extends PipTransitionController {
finishTransaction);
}
- // Fade in the fadeout PIP when the fixed rotation is finished.
- if (mPipTransitionState.isInPip() && !mInFixedRotation && mHasFadeOut) {
- fadeExistingPip(true /* show */);
- }
-
return false;
}
@@ -274,9 +271,20 @@ public class PipTransition extends PipTransitionController {
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
+ mMoveToBackTransition = transition;
+ // update the transition state to avoid {@link PipTaskOrganizer#onTaskVanished()} calls
+ mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP);
+
+ // return an empty WindowContainerTransaction so that we don't check other handlers
+ return new WindowContainerTransaction();
} else {
return null;
}
@@ -288,7 +296,7 @@ public class PipTransition extends PipTransitionController {
if (!requestHasPipEnter(request)) {
throw new IllegalStateException("Called PiP augmentRequest when request has no PiP");
}
- if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ if (mEnterAnimationType == ANIM_TYPE_ALPHA) {
mRequestedEnterTransition = transition;
mRequestedEnterTask = request.getTriggerTask().token;
outWCT.setActivityWindowingMode(request.getTriggerTask().token,
@@ -301,23 +309,20 @@ public class PipTransition extends PipTransitionController {
@Override
public void end() {
Animator animator = mPipAnimationController.getCurrentAnimator();
- if (animator == null) return;
- animator.end();
+ if (animator != null && animator.isRunning()) {
+ animator.end();
+ }
}
@Override
public boolean handleRotateDisplay(int startRotation, int endRotation,
WindowContainerTransaction wct) {
- if (mRequestedEnterTransition != null && mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ if (mRequestedEnterTransition != null && mEnterAnimationType == ANIM_TYPE_ALPHA) {
// A fade-in was requested but not-yet started. In this case, just recalculate the
// initial state under the new rotation.
int rotationDelta = deltaRotation(startRotation, endRotation);
if (rotationDelta != Surface.ROTATION_0) {
- DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
-
- layoutCopy.rotateTo(mContext.getResources(), endRotation);
- mPipBoundsState.setDisplayLayout(layoutCopy);
- mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+ mPipDisplayLayoutState.rotateTo(endRotation);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
wct.setBounds(mRequestedEnterTask, destinationBounds);
@@ -335,19 +340,36 @@ public class PipTransition extends PipTransitionController {
}
// This means an expand happened before enter-pip finished and we are now "merging" a
// no-op transition that happens to match our exit-pip.
+ // Or that the keyguard is up and preventing the transition from applying, in which case we
+ // want to manually reset pip. (b/283783868)
boolean cancelled = false;
if (mPipAnimationController.getCurrentAnimator() != null) {
mPipAnimationController.getCurrentAnimator().cancel();
+ mPipAnimationController.resetAnimatorState();
cancelled = true;
}
+
// Unset exitTransition AFTER cancel so that finishResize knows we are merging.
mExitTransition = null;
- if (!cancelled || aborted) return;
+ if (!cancelled) return;
final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo();
if (taskInfo != null) {
- startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(),
- mPipBoundsState.getBounds(), mPipBoundsState.getBounds(),
- new Rect(mExitDestinationBounds), Surface.ROTATION_0);
+ if (aborted) {
+ // keyguard case - the transition got aborted, so we want to reset state and
+ // windowing mode before reapplying the resize transaction
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_LEAVE_PIP);
+ mPipOrganizer.onExitPipFinished(taskInfo);
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ mPipOrganizer.applyWindowingModeChangeOnExit(wct, TRANSITION_DIRECTION_LEAVE_PIP);
+ wct.setBounds(taskInfo.token, null);
+ mPipOrganizer.applyFinishBoundsResize(wct, TRANSITION_DIRECTION_LEAVE_PIP, false);
+ } else {
+ // merge case
+ startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(),
+ mPipBoundsState.getBounds(), mPipBoundsState.getBounds(),
+ new Rect(mExitDestinationBounds), Surface.ROTATION_0, null /* startT */);
+ }
}
mExitDestinationBounds.setEmpty();
mCurrentPipTaskToken = null;
@@ -356,7 +378,7 @@ public class PipTransition extends PipTransitionController {
@Override
public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds,
@PipAnimationController.TransitionDirection int direction,
- @Nullable SurfaceControl.Transaction tx) {
+ @NonNull SurfaceControl.Transaction tx) {
final boolean enteringPip = isInPipDirection(direction);
if (enteringPip) {
mPipTransitionState.setTransitionState(ENTERED_PIP);
@@ -366,13 +388,15 @@ public class PipTransition extends PipTransitionController {
// (likely a remote like launcher), so don't fire the finish-callback here -- wait until
// the exit transition is merged.
if ((mExitTransition == null || isAnimatingLocally()) && mFinishCallback != null) {
+ final SurfaceControl leash = mPipOrganizer.getSurfaceControl();
+ final boolean hasValidLeash = leash != null && leash.isValid();
WindowContainerTransaction wct = null;
if (isOutPipDirection(direction)) {
// Only need to reset surface properties. The server-side operations were already
// done at the start. But if it is running fixed rotation, there will be a seamless
// display transition later. So the last rotation transform needs to be kept to
// avoid flickering, and then the display transition will reset the transform.
- if (tx != null && !mInFixedRotation) {
+ if (!mInFixedRotation && mFinishTransaction != null) {
mFinishTransaction.merge(tx);
}
} else {
@@ -381,27 +405,36 @@ public class PipTransition extends PipTransitionController {
// If we are animating from fullscreen using a bounds animation, then reset the
// activity windowing mode, and set the task bounds to the final bounds
wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
- wct.scheduleFinishEnterPip(taskInfo.token, destinationBounds);
wct.setBounds(taskInfo.token, destinationBounds);
} else {
wct.setBounds(taskInfo.token, null /* bounds */);
}
- if (tx != null) {
- wct.setBoundsChangeTransaction(taskInfo.token, tx);
+ // Reset the scale with bounds change synchronously.
+ if (hasValidLeash) {
+ mSurfaceTransactionHelper.crop(tx, leash, destinationBounds)
+ .resetScale(tx, leash, destinationBounds)
+ .round(tx, leash, true /* applyCornerRadius */);
}
+ wct.setBoundsChangeTransaction(taskInfo.token, tx);
}
- final SurfaceControl leash = mPipOrganizer.getSurfaceControl();
final int displayRotation = taskInfo.getConfiguration().windowConfiguration
.getDisplayRotation();
if (enteringPip && mInFixedRotation && mEndFixedRotation != displayRotation
- && leash != null && leash.isValid()) {
+ && hasValidLeash) {
// Launcher may update the Shelf height during the animation, which will update the
// destination bounds. Because this is in fixed rotation, We need to make sure the
// finishTransaction is using the updated bounds in the display rotation.
- final Rect displayBounds = mPipBoundsState.getDisplayBounds();
+ final PipAnimationController.PipTransitionAnimator<?> animator =
+ mPipAnimationController.getCurrentAnimator();
+ final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds();
final Rect finishBounds = new Rect(destinationBounds);
rotateBounds(finishBounds, displayBounds, mEndFixedRotation, displayRotation);
- mSurfaceTransactionHelper.crop(mFinishTransaction, leash, finishBounds);
+ if (!finishBounds.equals(animator.getEndValue())) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Destination bounds were changed during animation", TAG);
+ rotateBounds(finishBounds, displayBounds, mEndFixedRotation, displayRotation);
+ mSurfaceTransactionHelper.crop(mFinishTransaction, leash, finishBounds);
+ }
}
mFinishTransaction = null;
callFinishCallback(wct);
@@ -419,6 +452,9 @@ public class PipTransition extends PipTransitionController {
@Override
public void forceFinishTransition() {
+ // mFinishCallback might be null with an outdated mCurrentPipTaskToken
+ // for example, when app crashes while in PiP and exit transition has not started
+ mCurrentPipTaskToken = null;
if (mFinishCallback == null) return;
mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */);
mFinishCallback = null;
@@ -427,9 +463,31 @@ public class PipTransition extends PipTransitionController {
@Override
public void onFixedRotationStarted() {
+ fadeEnteredPipIfNeed(false /* show */);
+ }
+
+ @Override
+ public void onFixedRotationFinished() {
+ fadeEnteredPipIfNeed(true /* show */);
+ }
+
+ private void fadeEnteredPipIfNeed(boolean show) {
// The transition with this fixed rotation may be handled by other handler before reaching
// PipTransition, so we cannot do this in #startAnimation.
- if (mPipTransitionState.getTransitionState() == ENTERED_PIP && !mHasFadeOut) {
+ if (!mPipTransitionState.hasEnteredPip()) {
+ return;
+ }
+ if (show && mHasFadeOut) {
+ // If there is a pending transition, then let startAnimation handle it. And if it is
+ // handled, mHasFadeOut will be set to false and this runnable will be no-op. Otherwise
+ // make sure the PiP will reshow, e.g. swipe-up with fixed rotation (fade-out) but
+ // return to the current app (only finish the recent transition).
+ mTransitions.runOnIdle(() -> {
+ if (mHasFadeOut && mPipTransitionState.hasEnteredPip()) {
+ fadeExistingPip(true /* show */);
+ }
+ });
+ } else if (!show && !mHasFadeOut) {
// Fade out the existing PiP to avoid jump cut during seamless rotation.
fadeExistingPip(false /* show */);
}
@@ -466,6 +524,7 @@ public class PipTransition extends PipTransitionController {
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) {
TransitionInfo.Change pipChange = pipTaskChange;
+ 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);
@@ -478,6 +537,7 @@ public class PipTransition extends PipTransitionController {
if (mCurrentPipTaskToken.equals(change.getLastParent())) {
// Find the activity that is exiting PiP.
pipChange = change;
+ activitySc = change.getLeash();
break;
}
}
@@ -494,17 +554,62 @@ public class PipTransition extends PipTransitionController {
// case it may not be in the screen coordinate.
// Reparent the pip leash to the root with max layer so that we can animate it outside of
// parent crop, and make sure it is not covered by other windows.
- final SurfaceControl pipLeash = pipChange.getLeash();
- startTransaction.reparent(pipLeash, info.getRootLeash());
+ 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);
// Note: because of this, the bounds to animate should be translated to the root coordinate.
- final Point offset = info.getRootOffset();
+ final Point offset = root.getOffset();
final Rect currentBounds = mPipBoundsState.getBounds();
currentBounds.offset(-offset.x, -offset.y);
startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top);
+ final WindowContainerToken pipTaskToken = pipChange.getContainer();
+ final boolean useLocalLeash = activitySc != null;
+ final boolean toFullscreen = pipChange.getEndAbsBounds().equals(
+ mPipBoundsState.getDisplayBounds());
mFinishCallback = (wct, wctCB) -> {
mPipOrganizer.onExitPipFinished(taskInfo);
+
+ // TODO(b/286346098): remove the OPEN app flicker completely
+ // not checking if we go to fullscreen helps avoid getting pip into an inconsistent
+ // state after the flicker occurs. This is a temp solution until flicker is removed.
+ if (!Transitions.SHELL_TRANSITIONS_ROTATION) {
+ // will help to debug the case when we are not exiting to fullscreen
+ if (!toFullscreen) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: startExitAnimation() not exiting to fullscreen", TAG);
+ }
+ wct = wct != null ? wct : new WindowContainerTransaction();
+ wct.setBounds(pipTaskToken, null);
+ mPipOrganizer.applyWindowingModeChangeOnExit(wct, TRANSITION_DIRECTION_LEAVE_PIP);
+ }
+ if (useLocalLeash) {
+ if (mPipAnimationController.isAnimating()) {
+ mPipAnimationController.getCurrentAnimator().end();
+ }
+ // Make sure the animator don't use the released leash, e.g. mergeAnimation.
+ mPipAnimationController.resetAnimatorState();
+ finishTransaction.remove(pipLeash);
+ }
finishCallback.onTransitionFinished(wct, wctCB);
};
mFinishTransaction = finishTransaction;
@@ -529,13 +634,8 @@ public class PipTransition extends PipTransitionController {
}
}
- // Set the initial frame as scaling the end to the start.
final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds());
destinationBounds.offset(-offset.x, -offset.y);
- startTransaction.setWindowCrop(pipLeash, destinationBounds);
- mSurfaceTransactionHelper.scale(startTransaction, pipLeash, destinationBounds,
- currentBounds);
- startTransaction.apply();
// Check if it is fixed rotation.
final int rotationDelta;
@@ -565,7 +665,7 @@ public class PipTransition extends PipTransitionController {
rotationDelta = Surface.ROTATION_0;
}
startExpandAnimation(taskInfo, pipLeash, currentBounds, currentBounds, destinationBounds,
- rotationDelta);
+ rotationDelta, startTransaction);
}
private void startExpandAndRotationAnimation(@NonNull TransitionInfo info,
@@ -621,14 +721,22 @@ public class PipTransition extends PipTransitionController {
private void startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
final Rect baseBounds, final Rect startBounds, final Rect endBounds,
- final int rotationDelta) {
+ final int rotationDelta, @Nullable SurfaceControl.Transaction startTransaction) {
+ final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
+ taskInfo.pictureInPictureParams, endBounds);
final PipAnimationController.PipTransitionAnimator animator =
mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds,
- endBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_LEAVE_PIP,
+ endBounds, sourceHintRect, TRANSITION_DIRECTION_LEAVE_PIP,
0 /* startingAngle */, rotationDelta);
animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
- .setPipAnimationCallback(mPipAnimationCallback)
- .setDuration(mEnterExitAnimationDuration)
+ .setDuration(mEnterExitAnimationDuration);
+ if (startTransaction != null) {
+ animator.setPipTransactionHandler(mTransactionConsumer).applySurfaceControlTransaction(
+ leash, startTransaction, PipAnimationController.FRACTION_START);
+ startTransaction.apply();
+ }
+ animator.setPipAnimationCallback(mPipAnimationCallback)
+ .setPipTransactionHandler(mPipOrganizer.getPipTransactionHandler())
.start();
}
@@ -640,7 +748,7 @@ public class PipTransition extends PipTransitionController {
@NonNull TaskInfo taskInfo) {
startTransaction.apply();
finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
- mPipBoundsState.getDisplayBounds());
+ mPipDisplayLayoutState.getDisplayBounds());
mPipOrganizer.onExitPipFinished(taskInfo);
finishCallback.onTransitionFinished(null, null);
}
@@ -679,6 +787,11 @@ public class PipTransition extends PipTransitionController {
return false;
}
+ @Override
+ public void setEnterAnimationType(@PipAnimationController.AnimationType int type) {
+ mEnterAnimationType = type;
+ }
+
private void startEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -702,7 +815,7 @@ public class PipTransition extends PipTransitionController {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (change == enterPip) continue;
- if (isOpeningType(change.getMode())) {
+ if (TransitionUtil.isOpeningType(change.getMode())) {
final SurfaceControl leash = change.getLeash();
startTransaction.show(leash).setAlpha(leash, 1.f);
}
@@ -732,71 +845,55 @@ public class PipTransition extends PipTransitionController {
final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo();
final SurfaceControl leash = pipChange.getLeash();
final int startRotation = pipChange.getStartRotation();
+ // Check again in case some callers use startEnterAnimation directly so the flag was not
+ // set in startAnimation, e.g. from DefaultMixedHandler.
+ if (!mInFixedRotation) {
+ mEndFixedRotation = pipChange.getEndFixedRotation();
+ mInFixedRotation = mEndFixedRotation != ROTATION_UNDEFINED;
+ }
final int endRotation = mInFixedRotation ? mEndFixedRotation : pipChange.getEndRotation();
setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
taskInfo.topActivityInfo);
+
+ if (mPipOrganizer.shouldAttachMenuEarly()) {
+ mPipMenuController.attach(leash);
+ }
+
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
- final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds();
+ final Rect currentBounds = pipChange.getStartAbsBounds();
int rotationDelta = deltaRotation(startRotation, endRotation);
Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
- taskInfo.pictureInPictureParams, currentBounds);
+ taskInfo.pictureInPictureParams, currentBounds, destinationBounds);
if (rotationDelta != Surface.ROTATION_0 && mInFixedRotation) {
// Need to get the bounds of new rotation in old rotation for fixed rotation,
computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo,
destinationBounds, sourceHintRect);
}
- // Set corner radius for entering pip.
- mSurfaceTransactionHelper
- .crop(finishTransaction, leash, destinationBounds)
- .round(finishTransaction, leash, true /* applyCornerRadius */);
- mTransitions.getMainExecutor().executeDelayed(() -> mPipMenuController.attach(leash), 0);
+ if (!mPipOrganizer.shouldAttachMenuEarly()) {
+ mTransitions.getMainExecutor().executeDelayed(
+ () -> mPipMenuController.attach(leash), 0);
+ }
if (taskInfo.pictureInPictureParams != null
&& taskInfo.pictureInPictureParams.isAutoEnterEnabled()
&& mPipTransitionState.getInSwipePipToHomeTransition()) {
- mOneShotAnimationType = ANIM_TYPE_BOUNDS;
- final SurfaceControl swipePipToHomeOverlay = mPipOrganizer.mSwipePipToHomeOverlay;
- startTransaction.setMatrix(leash, Matrix.IDENTITY_MATRIX, new float[9])
- .setPosition(leash, destinationBounds.left, destinationBounds.top)
- .setWindowCrop(leash, destinationBounds.width(), destinationBounds.height());
- if (swipePipToHomeOverlay != null) {
- // Launcher fade in the overlay on top of the fullscreen Task. It is possible we
- // reparent the PIP activity to a new PIP task (in case there are other activities
- // in the original Task), so we should also reparent the overlay to the PIP task.
- startTransaction.reparent(swipePipToHomeOverlay, leash)
- .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE);
- mPipOrganizer.mSwipePipToHomeOverlay = null;
- }
- startTransaction.apply();
- if (rotationDelta != Surface.ROTATION_0 && mInFixedRotation) {
- // For fixed rotation, set the destination bounds to the new rotation coordinates
- // at the end.
- destinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
- }
- mPipBoundsState.setBounds(destinationBounds);
- onFinishResize(taskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, null /* tx */);
- sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
- if (swipePipToHomeOverlay != null) {
- mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay,
- null /* callback */, false /* withStartDelay */);
- }
- mPipTransitionState.setInSwipePipToHomeTransition(false);
+ handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash,
+ sourceHintRect, destinationBounds, taskInfo);
return;
}
- if (rotationDelta != Surface.ROTATION_0) {
- Matrix tmpTransform = new Matrix();
- tmpTransform.postRotate(rotationDelta);
- startTransaction.setMatrix(leash, tmpTransform, new float[9]);
- }
- if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ final int enterAnimationType = mEnterAnimationType;
+ if (enterAnimationType == ANIM_TYPE_ALPHA) {
startTransaction.setAlpha(leash, 0f);
+ } else {
+ // set alpha to 1, because for multi-activity PiP it will create a new task with alpha 0
+ startTransaction.setAlpha(leash, 1f);
}
startTransaction.apply();
PipAnimationController.PipTransitionAnimator animator;
- if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
+ if (enterAnimationType == ANIM_TYPE_BOUNDS) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
0 /* startingAngle */, rotationDelta);
@@ -818,14 +915,23 @@ public class PipTransition extends PipTransitionController {
} else {
animator.setColorContentOverlay(mContext);
}
+ } else {
+ final TaskSnapshot snapshot = PipUtils.getTaskSnapshot(
+ taskInfo.launchIntoPipHostTaskId, false /* isLowResolution */);
+ if (snapshot != null) {
+ // use the task snapshot during the animation, this is for
+ // launch-into-pip aka. content-pip use case.
+ animator.setSnapshotContentOverlay(snapshot, sourceHintRect);
+ }
}
- } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
+ } else if (enterAnimationType == ANIM_TYPE_ALPHA) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
0f, 1f);
- mOneShotAnimationType = ANIM_TYPE_BOUNDS;
+ mSurfaceTransactionHelper
+ .crop(finishTransaction, leash, destinationBounds)
+ .round(finishTransaction, leash, true /* applyCornerRadius */);
} else {
- throw new RuntimeException("Unrecognized animation type: "
- + mOneShotAnimationType);
+ throw new RuntimeException("Unrecognized animation type: " + enterAnimationType);
}
animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
.setPipAnimationCallback(mPipAnimationCallback)
@@ -836,19 +942,20 @@ public class PipTransition extends PipTransitionController {
// ComputeRotatedBounds has changed the DisplayLayout without affecting the animation.
animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds());
}
- animator.start();
+ // Keep the last appearance when finishing the transition. The transform will be reset when
+ // setting bounds.
+ animator.setPipTransactionHandler(mTransactionConsumer).applySurfaceControlTransaction(
+ leash, finishTransaction, PipAnimationController.FRACTION_END);
+ // Start to animate enter PiP.
+ animator.setPipTransactionHandler(mPipOrganizer.getPipTransactionHandler()).start();
}
/** Computes destination bounds in old rotation and updates source hint rect if available. */
private void computeEnterPipRotatedBounds(int rotationDelta, int startRotation, int endRotation,
TaskInfo taskInfo, Rect outDestinationBounds, @Nullable Rect outSourceHintRect) {
- DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
-
- layoutCopy.rotateTo(mContext.getResources(), endRotation);
- mPipBoundsState.setDisplayLayout(layoutCopy);
- mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+ mPipDisplayLayoutState.rotateTo(endRotation);
- final Rect displayBounds = mPipBoundsState.getDisplayBounds();
+ final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds();
outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
// Transform the destination bounds to current display coordinates.
rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation);
@@ -862,17 +969,60 @@ public class PipTransition extends PipTransitionController {
}
}
+ private void handleSwipePipToHomeTransition(
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull SurfaceControl leash, @Nullable Rect sourceHintRect,
+ @NonNull Rect destinationBounds,
+ @NonNull ActivityManager.RunningTaskInfo pipTaskInfo) {
+ if (mInFixedRotation) {
+ // If rotation changes when returning to home, the transition should contain both the
+ // entering PiP and the display change (PipController#startSwipePipToHome has updated
+ // the display layout to new rotation). So it is not expected to see fixed rotation.
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "%s: SwipePipToHome should not use fixed rotation %d", TAG, mEndFixedRotation);
+ }
+ final SurfaceControl swipePipToHomeOverlay = mPipOrganizer.mSwipePipToHomeOverlay;
+ if (swipePipToHomeOverlay != null) {
+ // Launcher fade in the overlay on top of the fullscreen Task. It is possible we
+ // reparent the PIP activity to a new PIP task (in case there are other activities
+ // in the original Task, in other words multi-activity apps), so we should also reparent
+ // 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();
+ final PipAnimationController.PipTransitionAnimator animator =
+ mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds,
+ destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
+ 0 /* startingAngle */, 0 /* rotationDelta */)
+ .setPipTransactionHandler(mTransactionConsumer)
+ .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP);
+ // The start state is the end state for swipe-auto-pip.
+ startTransaction.merge(finishTransaction);
+ animator.applySurfaceControlTransaction(leash, startTransaction,
+ PipAnimationController.FRACTION_END);
+ startTransaction.apply();
+
+ mPipBoundsState.setBounds(destinationBounds);
+ final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ onFinishResize(pipTaskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, tx);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ if (swipePipToHomeOverlay != null) {
+ mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay,
+ null /* callback */, false /* withStartDelay */);
+ }
+ mPipTransitionState.setInSwipePipToHomeTransition(false);
+ }
+
private void startExitToSplitAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull TaskInfo taskInfo) {
- final int changeSize = info.getChanges().size();
- if (changeSize < 4) {
- throw new RuntimeException(
- "Got an exit-pip-to-split transition with unexpected change-list");
- }
- for (int i = changeSize - 1; i >= 0; i--) {
+ for (int i = info.getChanges().size() - 1; i >= 0; i--) {
final TransitionInfo.Change change = info.getChanges().get(i);
final int mode = change.getMode();
@@ -881,7 +1031,7 @@ public class PipTransition extends PipTransitionController {
continue;
}
- if (isOpeningType(mode) && change.getParent() == null) {
+ if (TransitionUtil.isOpeningType(mode) && change.getParent() == null) {
final SurfaceControl leash = change.getLeash();
final Rect endBounds = change.getEndAbsBounds();
startTransaction
@@ -891,7 +1041,7 @@ public class PipTransition extends PipTransitionController {
.setWindowCrop(leash, endBounds.width(), endBounds.height());
}
}
- mSplitScreenOptional.get().finishEnterSplitScreen(startTransaction);
+ mSplitScreenOptional.get().finishEnterSplitScreen(finishTransaction);
startTransaction.apply();
mPipOrganizer.onExitPipFinished(taskInfo);
@@ -901,24 +1051,27 @@ public class PipTransition extends PipTransitionController {
private void resetPrevPip(@NonNull TransitionInfo.Change prevPipTaskChange,
@NonNull SurfaceControl.Transaction startTransaction) {
final SurfaceControl leash = prevPipTaskChange.getLeash();
- final Rect bounds = prevPipTaskChange.getEndAbsBounds();
- final Point offset = prevPipTaskChange.getEndRelOffset();
- bounds.offset(-offset.x, -offset.y);
-
- startTransaction.setWindowCrop(leash, null);
- startTransaction.setMatrix(leash, 1, 0, 0, 1);
- startTransaction.setCornerRadius(leash, 0);
- startTransaction.setPosition(leash, bounds.left, bounds.top);
-
- if (mHasFadeOut && prevPipTaskChange.getTaskInfo().isVisible()) {
- if (mPipAnimationController.getCurrentAnimator() != null) {
- mPipAnimationController.getCurrentAnimator().cancel();
- }
- startTransaction.setAlpha(leash, 1);
- }
+ startTransaction.remove(leash);
+
mHasFadeOut = false;
mCurrentPipTaskToken = null;
- mPipOrganizer.onExitPipFinished(prevPipTaskChange.getTaskInfo());
+
+ // clean-up the state in PipTaskOrganizer if the PipTaskOrganizer#onTaskAppeared() hasn't
+ // been called yet with its leash reference now pointing to a new SurfaceControl not
+ // matching the leash of the pip we are removing.
+ if (mPipOrganizer.getSurfaceControl() == leash) {
+ mPipOrganizer.onExitPipFinished(prevPipTaskChange.getTaskInfo());
+ }
+ }
+
+ @Override
+ public boolean syncPipSurfaceState(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final TransitionInfo.Change pipChange = findCurrentPipTaskChange(info);
+ if (pipChange == null) return false;
+ updatePipForUnhandledTransition(pipChange, startTransaction, finishTransaction);
+ return true;
}
private void updatePipForUnhandledTransition(@NonNull TransitionInfo.Change pipChange,
@@ -927,14 +1080,22 @@ public class PipTransition extends PipTransitionController {
// When the PIP window is visible and being a part of the transition, such as display
// rotation, we need to update its bounds and rounded corner.
final SurfaceControl leash = pipChange.getLeash();
- final Rect destBounds = mPipBoundsState.getBounds();
+ final Rect destBounds = mPipOrganizer.getCurrentOrAnimatingBounds();
final boolean isInPip = mPipTransitionState.isInPip();
mSurfaceTransactionHelper
.crop(startTransaction, leash, destBounds)
- .round(startTransaction, leash, isInPip);
+ .round(startTransaction, leash, isInPip)
+ .shadow(startTransaction, leash, isInPip);
mSurfaceTransactionHelper
.crop(finishTransaction, leash, destBounds)
- .round(finishTransaction, leash, isInPip);
+ .round(finishTransaction, leash, isInPip)
+ .shadow(finishTransaction, leash, isInPip);
+ // Make sure the PiP keeps invisible if it was faded out. If it needs to fade in, that will
+ // be handled by onFixedRotationFinished().
+ if (isInPip && mHasFadeOut) {
+ startTransaction.setAlpha(leash, 0f);
+ finishTransaction.setAlpha(leash, 0f);
+ }
}
/** Hides and shows the existing PIP during fixed rotation transition of other activities. */
@@ -948,17 +1109,44 @@ public class PipTransition extends PipTransitionController {
}
final float alphaStart = show ? 0 : 1;
final float alphaEnd = show ? 1 : 0;
+ final PipAnimationController.PipTransactionHandler transactionHandler =
+ new PipAnimationController.PipTransactionHandler() {
+ @Override
+ public boolean handlePipTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, Rect destinationBounds, float alpha) {
+ if (alpha == 0) {
+ if (show) {
+ tx.setPosition(leash, destinationBounds.left, destinationBounds.top);
+ } else {
+ // Put PiP out of the display so it won't block touch when it is hidden.
+ final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds();
+ final int max = Math.max(displayBounds.width(), displayBounds.height());
+ tx.setPosition(leash, max, max);
+ }
+ }
+ return false;
+ }
+ };
mPipAnimationController
.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(), alphaStart, alphaEnd)
.setTransitionDirection(TRANSITION_DIRECTION_SAME)
- .setPipAnimationCallback(mPipAnimationCallback)
+ .setPipTransactionHandler(transactionHandler)
.setDuration(mEnterExitAnimationDuration)
.start();
mHasFadeOut = !show;
}
private void finishResizeForMenu(Rect destinationBounds) {
- mPipMenuController.movePipMenu(null, null, destinationBounds);
+ mPipMenuController.movePipMenu(null, null, destinationBounds,
+ PipMenuController.ALPHA_NO_CHANGE);
mPipMenuController.updateMenuBounds(destinationBounds);
}
+
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mCurrentPipTaskToken=" + mCurrentPipTaskToken);
+ pw.println(innerPrefix + "mFinishCallback=" + mFinishCallback);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index f51e247fe112..63627938ec87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -38,9 +38,11 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -105,15 +107,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH
}
/**
- * Called to inform the transition that the animation should start with the assumption that
- * PiP is not animating from its original bounds, but rather a continuation of another
- * animation. For example, gesture navigation would first fade out the PiP activity, and the
- * transition should be responsible to animate in (such as fade in) the PiP.
- */
- public void setIsFullAnimation(boolean isFullAnimation) {
- }
-
- /**
* Called when the Shell wants to start an exit Pip transition/animation.
*/
public void startExitTransition(int type, WindowContainerTransaction out,
@@ -132,6 +125,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH
public void onFixedRotationStarted() {
}
+ /** Called when the fixed rotation finished. */
+ public void onFixedRotationFinished() {
+ }
+
public PipTransitionController(
@NonNull ShellInit shellInit,
@NonNull ShellTaskOrganizer shellTaskOrganizer,
@@ -228,12 +225,23 @@ public abstract class PipTransitionController implements Transitions.TransitionH
return false;
}
+ /** Whether a particular package is same as current pip package. */
+ public boolean isInPipPackage(String packageName) {
+ final TaskInfo inPipTask = mPipOrganizer.getTaskInfo();
+ return packageName != null && inPipTask != null
+ && packageName.equals(SplitScreenUtils.getPackageName(inPipTask.baseIntent));
+ }
+
/** Add PiP-related changes to `outWCT` for the given request. */
public void augmentRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT) {
throw new IllegalStateException("Request isn't entering PiP");
}
+ /** Sets the type of animation when a PiP task appears. */
+ public void setEnterAnimationType(@PipAnimationController.AnimationType int type) {
+ }
+
/** Play a transition animation for entering PiP on a specific PiP change. */
public void startEnterAnimation(@NonNull final TransitionInfo.Change pipChange,
@NonNull final SurfaceControl.Transaction startTransaction,
@@ -241,6 +249,18 @@ public abstract class PipTransitionController implements Transitions.TransitionH
@NonNull final Transitions.TransitionFinishCallback finishCallback) {
}
+ /**
+ * Applies the proper surface states (rounded corners/shadows) to pip surfaces in `info`.
+ * This is intended to be used when PiP is part of another animation but isn't, itself,
+ * animating (eg. unlocking).
+ * @return `true` if there was a pip in `info`.
+ */
+ public boolean syncPipSurfaceState(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ return false;
+ }
+
/** End the currently-playing PiP animation. */
public void end() {
}
@@ -264,4 +284,9 @@ public abstract class PipTransitionController implements Transitions.TransitionH
*/
void onPipTransitionCanceled(int direction);
}
+
+ /**
+ * Dumps internal states.
+ */
+ public void dump(PrintWriter pw, String prefix) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
index db6138a0891f..b5f44835802f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
@@ -140,6 +140,24 @@ public class PipTransitionState {
return state == ENTERING_PIP;
}
+ private String stateToString() {
+ switch (mState) {
+ case UNDEFINED: return "undefined";
+ case TASK_APPEARED: return "task-appeared";
+ case ENTRY_SCHEDULED: return "entry-scheduled";
+ case ENTERING_PIP: return "entering-pip";
+ case ENTERED_PIP: return "entered-pip";
+ case EXITING_PIP: return "exiting-pip";
+ }
+ throw new IllegalStateException("Unknown state: " + mState);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)",
+ stateToString(), mInSwipePipToHomeTransition);
+ }
+
public interface OnPipTransitionStateChangedListener {
void onPipTransitionStateChanged(@TransitionState int oldState,
@TransitionState int newState);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index ed8dc7ded654..f9332e4bdb2e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -63,11 +63,29 @@ public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterfac
if (pipBoundsState.isImeShowing()) {
insets.bottom -= pipBoundsState.getImeHeight();
}
+ // if PiP is stashed we only adjust the vertical position if it's outside of insets and
+ // ignore all keep clear areas, since it's already on the side
+ if (pipBoundsState.isStashed()) {
+ if (startingBounds.bottom > insets.bottom || startingBounds.top < insets.top) {
+ // bring PiP back to be aligned by bottom inset
+ startingBounds.offset(0, insets.bottom - startingBounds.bottom);
+ }
+ return startingBounds;
+ }
Rect pipBounds = new Rect(startingBounds);
- // move PiP towards corner if user hasn't moved it manually or the flag is on
- if (mKeepClearAreaGravityEnabled
- || (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip())) {
+ boolean shouldApplyGravity = false;
+ // if PiP is outside of screen insets, reposition using gravity
+ if (!insets.contains(pipBounds)) {
+ shouldApplyGravity = true;
+ }
+ // if user has not interacted with PiP, reposition using gravity
+ if (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip()) {
+ shouldApplyGravity = true;
+ }
+
+ // apply gravity that will position PiP in bottom left or bottom right corner within insets
+ if (mKeepClearAreaGravityEnabled || shouldApplyGravity) {
float snapFraction = pipBoundsAlgorithm.getSnapFraction(startingBounds);
int verticalGravity = Gravity.BOTTOM;
int horizontalGravity;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 431bd7b08142..5e1b6becfa45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -31,6 +31,8 @@ import android.os.RemoteException;
import android.util.Size;
import android.view.MotionEvent;
import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewRootImpl;
import android.view.WindowManagerGlobal;
import com.android.internal.protolog.common.ProtoLog;
@@ -131,6 +133,8 @@ public class PhonePipMenuController implements PipMenuController {
private PipMenuView mPipMenuView;
+ private SurfaceControl mLeash;
+
private ActionListener mMediaActionListener = new ActionListener() {
@Override
public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
@@ -166,6 +170,7 @@ public class PhonePipMenuController implements PipMenuController {
*/
@Override
public void attach(SurfaceControl leash) {
+ mLeash = leash;
attachPipMenuView();
}
@@ -176,6 +181,7 @@ public class PhonePipMenuController implements PipMenuController {
public void detach() {
hideMenu();
detachPipMenuView();
+ mLeash = null;
}
void attachPipMenuView() {
@@ -185,8 +191,38 @@ public class PhonePipMenuController implements PipMenuController {
}
mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler,
mSplitScreenController, mPipUiEventLogger);
+ mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ v.getViewRootImpl().addSurfaceChangedCallback(
+ new ViewRootImpl.SurfaceChangedCallback() {
+ @Override
+ public void surfaceCreated(SurfaceControl.Transaction t) {
+ final SurfaceControl sc = getSurfaceControl();
+ if (sc != null) {
+ t.reparent(sc, mLeash);
+ // make menu on top of the surface
+ t.setLayer(sc, Integer.MAX_VALUE);
+ }
+ }
+
+ @Override
+ public void surfaceReplaced(SurfaceControl.Transaction t) {
+ }
+
+ @Override
+ public void surfaceDestroyed() {
+ }
+ });
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ }
+ });
+
mSystemWindows.addView(mPipMenuView,
- getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
+ getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
0, SHELL_ROOT_LAYER_PIP);
setShellRootAccessibilityWindow();
@@ -210,7 +246,7 @@ public class PhonePipMenuController implements PipMenuController {
@Override
public void updateMenuBounds(Rect destinationBounds) {
mSystemWindows.updateViewLayout(mPipMenuView,
- getPipMenuLayoutParams(MENU_WINDOW_TITLE, destinationBounds.width(),
+ getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(),
destinationBounds.height()));
updateMenuLayout(destinationBounds);
}
@@ -298,7 +334,8 @@ public class PhonePipMenuController implements PipMenuController {
}
// Sync the menu bounds before showing it in case it is out of sync.
- movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds);
+ movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds,
+ PipMenuController.ALPHA_NO_CHANGE);
updateMenuBounds(stackBounds);
mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
@@ -311,7 +348,7 @@ public class PhonePipMenuController implements PipMenuController {
@Override
public void movePipMenu(@Nullable SurfaceControl pipLeash,
@Nullable SurfaceControl.Transaction t,
- Rect destinationBounds) {
+ Rect destinationBounds, float alpha) {
if (destinationBounds.isEmpty()) {
return;
}
@@ -320,30 +357,10 @@ public class PhonePipMenuController implements PipMenuController {
return;
}
- // If there is no pip leash supplied, that means the PiP leash is already finalized
- // resizing and the PiP menu is also resized. We then want to do a scale from the current
- // new menu bounds.
- if (pipLeash != null && t != null) {
- mPipMenuView.getBoundsOnScreen(mTmpSourceBounds);
- } else {
- mTmpSourceBounds.set(0, 0, destinationBounds.width(), destinationBounds.height());
- }
-
- mTmpSourceRectF.set(mTmpSourceBounds);
- mTmpDestinationRectF.set(destinationBounds);
- mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
- final SurfaceControl surfaceControl = getSurfaceControl();
- if (surfaceControl == null) {
- return;
- }
- final SurfaceControl.Transaction menuTx =
- mSurfaceControlTransactionFactory.getTransaction();
- menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
+ // TODO(b/286307861) transaction should be applied outside of PiP menu controller
if (pipLeash != null && t != null) {
- // Merge the two transactions, vsyncId has been set on menuTx.
- menuTx.merge(t);
+ t.apply();
}
- menuTx.apply();
}
/**
@@ -361,18 +378,10 @@ public class PhonePipMenuController implements PipMenuController {
return;
}
- final SurfaceControl surfaceControl = getSurfaceControl();
- if (surfaceControl == null) {
- return;
- }
- final SurfaceControl.Transaction menuTx =
- mSurfaceControlTransactionFactory.getTransaction();
- menuTx.setCrop(surfaceControl, destinationBounds);
+ // TODO(b/286307861) transaction should be applied outside of PiP menu controller
if (pipLeash != null && t != null) {
- // Merge the two transactions, vsyncId has been set on menuTx.
- menuTx.merge(t);
+ t.apply();
}
- menuTx.apply();
}
private boolean checkPipMenuState() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
index 7365b9525919..4a06d84ce90d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
@@ -24,11 +24,13 @@ import android.graphics.Region;
import android.os.Bundle;
import android.os.RemoteException;
import android.view.MagnificationSpec;
+import android.view.SurfaceControl;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.window.ScreenCapture;
import androidx.annotation.BinderThread;
@@ -362,6 +364,15 @@ public class PipAccessibilityInteractionConnection {
}
@Override
+ public void takeScreenshotOfWindow(int interactionId,
+ ScreenCapture.ScreenCaptureListener listener,
+ IAccessibilityInteractionConnectionCallback callback) throws RemoteException {
+ // AbstractAccessibilityServiceConnection uses the standard
+ // IAccessibilityInteractionConnection for takeScreenshotOfWindow for Pip windows,
+ // so do nothing here.
+ }
+
+ @Override
public void clearAccessibilityFocus() throws RemoteException {
// Do nothing
}
@@ -370,5 +381,8 @@ public class PipAccessibilityInteractionConnection {
public void notifyOutsideTouch() throws RemoteException {
// Do nothing
}
- }
+
+ @Override
+ public void attachAccessibilityOverlayToWindow(SurfaceControl sc) {}
}
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 2bd5f1c5817c..65727b6145e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -85,6 +85,7 @@ import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
@@ -142,6 +143,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
private PipBoundsState mPipBoundsState;
private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
private PipMotionHelper mPipMotionHelper;
private PipTouchHandler mTouchHandler;
private PipTransitionController mPipTransitionController;
@@ -322,7 +324,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void onDisplayAdded(int displayId) {
- if (displayId != mPipBoundsState.getDisplayId()) {
+ if (displayId != mPipDisplayLayoutState.getDisplayId()) {
return;
}
onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
@@ -331,7 +333,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
- if (displayId != mPipBoundsState.getDisplayId()) {
+ if (displayId != mPipDisplayLayoutState.getDisplayId()) {
return;
}
onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
@@ -341,7 +343,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
Set<Rect> unrestricted) {
- if (mPipBoundsState.getDisplayId() == displayId) {
+ if (mPipDisplayLayoutState.getDisplayId() == displayId) {
if (mEnablePipKeepClearAlgorithm) {
mPipBoundsState.setKeepClearAreas(restricted, unrestricted);
@@ -401,6 +403,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
PipBoundsState pipBoundsState,
PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
@@ -424,8 +427,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
return new PipController(context, shellInit, shellCommandHandler, shellController,
displayController, pipAnimationController, pipAppOpsListener,
pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler,
- pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
- pipTransitionState, pipTouchHandler, pipTransitionController,
+ pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController,
+ pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController,
windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
displayInsetsController, pipTabletopController, oneHandedController, mainExecutor)
.mImpl;
@@ -442,6 +445,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
@NonNull PipBoundsState pipBoundsState,
PipSizeSpecHandler pipSizeSpecHandler,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
@@ -469,6 +473,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
mPipBoundsState = pipBoundsState;
mPipSizeSpecHandler = pipSizeSpecHandler;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
mPipMotionHelper = pipMotionHelper;
mPipTaskOrganizer = pipTaskOrganizer;
mPipTransitionState = pipTransitionState;
@@ -497,7 +502,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
INPUT_CONSUMER_PIP, mMainExecutor);
mPipTransitionController.registerPipTransitionCallback(this);
mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> {
- mPipBoundsState.setDisplayId(displayId);
+ mPipDisplayLayoutState.setDisplayId(displayId);
onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
false /* saveRestoreSnapFraction */);
});
@@ -537,11 +542,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb
// Ensure that we have the display info in case we get calls to update the bounds before the
// listener calls back
- mPipBoundsState.setDisplayId(mContext.getDisplayId());
+ mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId());
DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay());
- mPipSizeSpecHandler.setDisplayLayout(layout);
- mPipBoundsState.setDisplayLayout(layout);
+ mPipDisplayLayoutState.setDisplayLayout(layout);
try {
mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener);
@@ -570,6 +574,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void onActivityPinned(String packageName, int userId, int taskId,
int stackId) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onActivityPinned: %s", packageName);
mTouchHandler.onActivityPinned();
mMediaController.onActivityPinned();
mAppOpsListener.onActivityPinned(packageName);
@@ -581,6 +587,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
final Pair<ComponentName, Integer> topPipActivityInfo =
PipUtils.getTopPipActivity(mContext);
final ComponentName topActivity = topPipActivityInfo.first;
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onActivityUnpinned: %s", topActivity);
mTouchHandler.onActivityUnpinned(topActivity);
mAppOpsListener.onActivityUnpinned();
mPipInputConsumer.unregisterInputConsumer();
@@ -589,6 +597,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "onActivityRestartAttempt: %s", task.topActivity);
if (task.getWindowingMode() != WINDOWING_MODE_PINNED) {
return;
}
@@ -636,12 +646,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
});
- mDisplayInsetsController.addInsetsChangedListener(mPipBoundsState.getDisplayId(),
+ mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
new DisplayInsetsController.OnInsetsChangedListener() {
@Override
public void insetsChanged(InsetsState insetsState) {
- DisplayLayout pendingLayout =
- mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId());
+ DisplayLayout pendingLayout = mDisplayController
+ .getDisplayLayout(mPipDisplayLayoutState.getDisplayId());
if (mIsInFixedRotation
|| mIsKeyguardShowingOrAnimating
|| pendingLayout.rotation()
@@ -651,8 +661,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
return;
}
int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
- onDisplayChangedUncheck(
- mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()),
+ onDisplayChangedUncheck(mDisplayController
+ .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()),
false /* saveRestoreSnapFraction */);
int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
if (!mEnablePipKeepClearAlgorithm) {
@@ -673,7 +683,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
});
mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> {
- if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return;
final String tag = "tabletop-mode";
if (!isInTabletopMode) {
mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag);
@@ -774,7 +783,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) {
- if (!mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) {
+ if (!mPipDisplayLayoutState.getDisplayLayout().isSameGeometry(layout)) {
PipAnimationController.PipTransitionAnimator animator =
mPipAnimationController.getCurrentAnimator();
if (animator != null && animator.isRunning()) {
@@ -788,11 +797,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb
private void onDisplayChangedUncheck(DisplayLayout layout, boolean saveRestoreSnapFraction) {
Runnable updateDisplayLayout = () -> {
final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS
- && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation();
+ && mPipDisplayLayoutState.getDisplayLayout().rotation() != layout.rotation();
// update the internal state of objects subscribed to display changes
- mPipSizeSpecHandler.setDisplayLayout(layout);
- mPipBoundsState.setDisplayLayout(layout);
+ mPipDisplayLayoutState.setDisplayLayout(layout);
final WindowContainerTransaction wct =
fromRotation ? new WindowContainerTransaction() : null;
@@ -816,11 +824,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipBoundsState.getStashedState());
// Scale PiP on density dpi change, so it appears to be the same size physically.
- final boolean densityDpiChanged = mPipBoundsState.getDisplayLayout().densityDpi() != 0
- && (mPipBoundsState.getDisplayLayout().densityDpi() != layout.densityDpi());
+ final boolean densityDpiChanged =
+ mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0
+ && (mPipDisplayLayoutState.getDisplayLayout().densityDpi()
+ != layout.densityDpi());
if (densityDpiChanged) {
final float scale = (float) layout.densityDpi()
- / mPipBoundsState.getDisplayLayout().densityDpi();
+ / mPipDisplayLayoutState.getDisplayLayout().densityDpi();
postChangeBounds.set(0, 0,
(int) (postChangeBounds.width() * scale),
(int) (postChangeBounds.height() * scale));
@@ -835,8 +845,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
pipSnapAlgorithm.applySnapFraction(postChangeBounds, postChangeMovementBounds,
snapFraction, mPipBoundsState.getStashedState(),
mPipBoundsState.getStashOffset(),
- mPipBoundsState.getDisplayBounds(),
- mPipBoundsState.getDisplayLayout().stableInsets());
+ mPipDisplayLayoutState.getDisplayBounds(),
+ mPipDisplayLayoutState.getDisplayLayout().stableInsets());
if (densityDpiChanged) {
// Using PipMotionHelper#movePip directly here may cause race condition since
@@ -941,10 +951,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipBoundsState.getDisplayBounds().right,
mPipBoundsState.getDisplayBounds().bottom);
mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect);
+ updatePipPositionForKeepClearAreas();
} else {
mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG);
+ // postpone moving in response to hide of Launcher in case there's another change
+ mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback);
+ mMainExecutor.executeDelayed(
+ mMovePipInResponseToKeepClearAreasChangeCallback,
+ PIP_KEEP_CLEAR_AREAS_DELAY);
}
- updatePipPositionForKeepClearAreas();
}
private void setLauncherAppIconSize(int iconSizePx) {
@@ -963,12 +978,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipBoundsState.setShelfVisibility(visible, shelfHeight);
}
- private void setPinnedStackAnimationType(int animationType) {
- mPipTaskOrganizer.setOneShotAnimationType(animationType);
- mPipTransitionController.setIsFullAnimation(
- animationType == PipAnimationController.ANIM_TYPE_BOUNDS);
- }
-
@VisibleForTesting
void setPinnedStackAnimationListener(PipAnimationListener callback) {
mPinnedStackAnimationRecentsCallback = callback;
@@ -1013,6 +1022,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay);
}
+ private void abortSwipePipToHome(int taskId, ComponentName componentName) {
+ mPipTaskOrganizer.abortSwipePipToHome(taskId, componentName);
+ }
+
private String getTransitionTag(int direction) {
switch (direction) {
case TRANSITION_DIRECTION_TO_PIP:
@@ -1061,13 +1074,22 @@ public class PipController implements PipTransitionController.PipTransitionCallb
/** Save the state to restore to on re-entry. */
public void saveReentryState(Rect pipBounds) {
float snapFraction = mPipBoundsAlgorithm.getSnapFraction(pipBounds);
- if (mPipBoundsState.hasUserResizedPip()) {
- final Rect reentryBounds = mTouchHandler.getUserResizeBounds();
- final Size reentrySize = new Size(reentryBounds.width(), reentryBounds.height());
- mPipBoundsState.saveReentryState(reentrySize, snapFraction);
- } else {
+
+ if (!mPipBoundsState.hasUserResizedPip()) {
mPipBoundsState.saveReentryState(null /* bounds */, snapFraction);
+ return;
}
+
+ Size reentrySize = new Size(pipBounds.width(), pipBounds.height());
+
+ // TODO: b/279937014 Investigate why userResizeBounds are empty with shell transitions on
+ // fallback to using the userResizeBounds if userResizeBounds are not empty
+ if (!mTouchHandler.getUserResizeBounds().isEmpty()) {
+ Rect userResizeBounds = mTouchHandler.getUserResizeBounds();
+ reentrySize = new Size(userResizeBounds.width(), userResizeBounds.height());
+ }
+
+ mPipBoundsState.saveReentryState(reentrySize, snapFraction);
}
@Override
@@ -1095,7 +1117,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
// Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before
// passing to mTouchHandler/mPipTaskOrganizer
final Rect outBounds = new Rect(toBounds);
- final int rotation = mPipBoundsState.getDisplayLayout().rotation();
+ final int rotation = mPipDisplayLayoutState.getDisplayLayout().rotation();
mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds);
mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds());
@@ -1119,11 +1141,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
private void onDisplayRotationChangedNotInPip(Context context, int toRotation) {
// Update the display layout, note that we have to do this on every rotation even if we
// aren't in PIP since we need to update the display layout to get the right resources
- DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
-
- layoutCopy.rotateTo(context.getResources(), toRotation);
- mPipBoundsState.setDisplayLayout(layoutCopy);
- mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+ mPipDisplayLayoutState.rotateTo(toRotation);
}
/**
@@ -1136,7 +1154,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
Rect outInsetBounds,
int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) {
// Bail early if the event is not sent to current display
- if ((displayId != mPipBoundsState.getDisplayId()) || (fromRotation == toRotation)) {
+ if ((displayId != mPipDisplayLayoutState.getDisplayId()) || (fromRotation == toRotation)) {
return false;
}
@@ -1160,11 +1178,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipBoundsState.getStashedState());
// Update the display layout
- DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
-
- layoutCopy.rotateTo(context.getResources(), toRotation);
- mPipBoundsState.setDisplayLayout(layoutCopy);
- mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+ mPipDisplayLayoutState.rotateTo(toRotation);
// Calculate the stack bounds in the new orientation based on same fraction along the
// rotated movement bounds.
@@ -1172,8 +1186,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
postChangeStackBounds, false /* adjustForIme */);
pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(),
- mPipBoundsState.getDisplayBounds(),
- mPipBoundsState.getDisplayLayout().stableInsets());
+ mPipDisplayLayoutState.getDisplayBounds(),
+ mPipDisplayLayoutState.getDisplayLayout().stableInsets());
mPipBoundsAlgorithm.getInsetBounds(outInsetBounds);
outBounds.set(postChangeStackBounds);
@@ -1191,6 +1205,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
mPipBoundsState.dump(pw, innerPrefix);
mPipInputConsumer.dump(pw, innerPrefix);
mPipSizeSpecHandler.dump(pw, innerPrefix);
+ mPipDisplayLayoutState.dump(pw, innerPrefix);
}
/**
@@ -1298,12 +1313,21 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void stopSwipePipToHome(int taskId, ComponentName componentName,
Rect destinationBounds, SurfaceControl overlay) {
+ if (overlay != null) {
+ overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
+ }
executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
(controller) -> controller.stopSwipePipToHome(
taskId, componentName, destinationBounds, overlay));
}
@Override
+ public void abortSwipePipToHome(int taskId, ComponentName componentName) {
+ executeRemoteCallWithTaskPermission(mController, "abortSwipePipToHome",
+ (controller) -> controller.abortSwipePipToHome(taskId, componentName));
+ }
+
+ @Override
public void setShelfHeight(boolean visible, int height) {
executeRemoteCallWithTaskPermission(mController, "setShelfHeight",
(controller) -> controller.setShelfHeight(visible, height));
@@ -1336,7 +1360,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void setPipAnimationTypeToAlpha() {
executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha",
- (controller) -> controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA));
+ (controller) -> controller.mPipAnimationController.setOneShotEnterAnimationType(
+ ANIM_TYPE_ALPHA));
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
index acc0caf95e35..d7d335b856be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
@@ -43,16 +43,16 @@ public class PipDoubleTapHelper {
* <p>MAX - maximum allowed screen size</p>
*/
@IntDef(value = {
- SIZE_SPEC_CUSTOM,
SIZE_SPEC_DEFAULT,
- SIZE_SPEC_MAX
+ SIZE_SPEC_MAX,
+ SIZE_SPEC_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
@interface PipSizeSpec {}
- static final int SIZE_SPEC_CUSTOM = 2;
static final int SIZE_SPEC_DEFAULT = 0;
static final int SIZE_SPEC_MAX = 1;
+ static final int SIZE_SPEC_CUSTOM = 2;
/**
* Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
index 167c0321d3ad..779c539a2097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -45,6 +45,7 @@ import android.content.Intent;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -513,13 +514,19 @@ public class PipMenuView extends FrameLayout {
final boolean isCloseAction = mCloseAction != null && Objects.equals(
mCloseAction.getActionIntent(), action.getActionIntent());
- // TODO: Check if the action drawable has changed before we reload it
- action.getIcon().loadDrawableAsync(mContext, d -> {
- if (d != null) {
- d.setTint(Color.WHITE);
- actionView.setImageDrawable(d);
- }
- }, mMainHandler);
+ final int iconType = action.getIcon().getType();
+ if (iconType == Icon.TYPE_URI || iconType == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
+ // Disallow loading icon from content URI
+ actionView.setImageDrawable(null);
+ } else {
+ // TODO: Check if the action drawable has changed before we reload it
+ action.getIcon().loadDrawableAsync(mContext, d -> {
+ if (d != null) {
+ d.setTint(Color.WHITE);
+ actionView.setImageDrawable(d);
+ }
+ }, mMainHandler);
+ }
actionView.setCustomCloseBackgroundVisibility(
isCloseAction ? View.VISIBLE : View.GONE);
actionView.setContentDescription(action.getContentDescription());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index 3e83c5fcf9a0..abe2db094a5c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -70,6 +70,7 @@ public class PipResizeGestureHandler {
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
private final PipMotionHelper mMotionHelper;
private final PipBoundsState mPipBoundsState;
+ private final PipTouchState mPipTouchState;
private final PipTaskOrganizer mPipTaskOrganizer;
private final PhonePipMenuController mPhonePipMenuController;
private final PipDismissTargetHandler mPipDismissTargetHandler;
@@ -121,7 +122,8 @@ public class PipResizeGestureHandler {
public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm,
PipBoundsState pipBoundsState, PipMotionHelper motionHelper,
- PipTaskOrganizer pipTaskOrganizer, PipDismissTargetHandler pipDismissTargetHandler,
+ PipTouchState pipTouchState, PipTaskOrganizer pipTaskOrganizer,
+ PipDismissTargetHandler pipDismissTargetHandler,
Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable,
PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController,
ShellExecutor mainExecutor) {
@@ -131,6 +133,7 @@ public class PipResizeGestureHandler {
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipBoundsState = pipBoundsState;
mMotionHelper = motionHelper;
+ mPipTouchState = pipTouchState;
mPipTaskOrganizer = pipTaskOrganizer;
mPipDismissTargetHandler = pipDismissTargetHandler;
mMovementBoundsSupplier = movementBoundsSupplier;
@@ -228,7 +231,7 @@ public class PipResizeGestureHandler {
if (mIsEnabled) {
// Register input event receiver
- mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput(
"pip-resize", mDisplayId);
try {
mMainExecutor.executeBlocking(() -> {
@@ -248,6 +251,11 @@ public class PipResizeGestureHandler {
return;
}
+ if (!mPipTouchState.getAllowInputEvents()) {
+ // No need to handle anything if touches are not enabled
+ return;
+ }
+
// Don't allow resize when PiP is stashed.
if (mPipBoundsState.isStashed()) {
return;
@@ -581,14 +589,13 @@ public class PipResizeGestureHandler {
mLastResizeBounds, movementBounds);
mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction);
- // disable the pinch resizing until the final bounds are updated
- final boolean prevEnablePinchResize = mEnablePinchResize;
- mEnablePinchResize = false;
+ // disable any touch events beyond resizing too
+ mPipTouchState.setAllowInputEvents(false);
mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds,
PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> {
- // reset the pinch resizing to its default state
- mEnablePinchResize = prevEnablePinchResize;
+ // enable touch events
+ mPipTouchState.setAllowInputEvents(true);
});
} else {
mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
index ff5138d4bb91..7971c049ff3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
@@ -21,6 +21,7 @@ import static com.android.wm.shell.pip.PipUtils.dpToPx;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
@@ -31,6 +32,7 @@ import android.util.Size;
import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import java.io.PrintWriter;
@@ -40,10 +42,9 @@ import java.io.PrintWriter;
public class PipSizeSpecHandler {
private static final String TAG = PipSizeSpecHandler.class.getSimpleName();
- @NonNull private final DisplayLayout mDisplayLayout = new DisplayLayout();
+ @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState;
- @VisibleForTesting
- final SizeSpecSource mSizeSpecSourceImpl;
+ private final SizeSpecSource mSizeSpecSourceImpl;
/** The preferred minimum (and default minimum) size specified by apps. */
@Nullable private Size mOverrideMinSize;
@@ -134,14 +135,14 @@ public class PipSizeSpecHandler {
maxWidth = (int) (mOptimizedAspectRatio * shorterLength
+ shorterLength * (aspectRatio - mOptimizedAspectRatio) / (1
+ aspectRatio));
- maxHeight = (int) (maxWidth / aspectRatio);
+ maxHeight = Math.round(maxWidth / aspectRatio);
} else {
if (aspectRatio > 1f) {
maxWidth = shorterLength;
- maxHeight = (int) (maxWidth / aspectRatio);
+ maxHeight = Math.round(maxWidth / aspectRatio);
} else {
maxHeight = shorterLength;
- maxWidth = (int) (maxHeight * aspectRatio);
+ maxWidth = Math.round(maxHeight * aspectRatio);
}
}
@@ -164,10 +165,9 @@ public class PipSizeSpecHandler {
Size maxSize = this.getMaxSize(aspectRatio);
- int defaultWidth = Math.max((int) (maxSize.getWidth() * mDefaultSizePercent),
+ int defaultWidth = Math.max(Math.round(maxSize.getWidth() * mDefaultSizePercent),
minSize.getWidth());
- int defaultHeight = Math.max((int) (maxSize.getHeight() * mDefaultSizePercent),
- minSize.getHeight());
+ int defaultHeight = Math.round(defaultWidth / aspectRatio);
return new Size(defaultWidth, defaultHeight);
}
@@ -187,16 +187,16 @@ public class PipSizeSpecHandler {
Size maxSize = this.getMaxSize(aspectRatio);
- int minWidth = (int) (maxSize.getWidth() * mMinimumSizePercent);
- int minHeight = (int) (maxSize.getHeight() * mMinimumSizePercent);
+ int minWidth = Math.round(maxSize.getWidth() * mMinimumSizePercent);
+ int minHeight = Math.round(maxSize.getHeight() * mMinimumSizePercent);
// make sure the calculated min size is not smaller than the allowed default min size
if (aspectRatio > 1f) {
- minHeight = (int) Math.max(minHeight, mDefaultMinSize);
- minWidth = (int) (minHeight * aspectRatio);
+ minHeight = Math.max(minHeight, mDefaultMinSize);
+ minWidth = Math.round(minHeight * aspectRatio);
} else {
- minWidth = (int) Math.max(minWidth, mDefaultMinSize);
- minHeight = (int) (minWidth / aspectRatio);
+ minWidth = Math.max(minWidth, mDefaultMinSize);
+ minHeight = Math.round(minWidth / aspectRatio);
}
return new Size(minWidth, minHeight);
}
@@ -362,14 +362,12 @@ public class PipSizeSpecHandler {
}
}
- public PipSizeSpecHandler(Context context) {
+ public PipSizeSpecHandler(Context context, PipDisplayLayoutState pipDisplayLayoutState) {
mContext = context;
-
- boolean enablePipSizeLargeScreen = SystemProperties
- .getBoolean("persist.wm.debug.enable_pip_size_large_screen", true);
+ mPipDisplayLayoutState = pipDisplayLayoutState;
// choose between two implementations of size spec logic
- if (enablePipSizeLargeScreen) {
+ if (supportsPipSizeLargeScreen()) {
mSizeSpecSourceImpl = new SizeSpecLargeScreenOptimizedImpl();
} else {
mSizeSpecSourceImpl = new SizeSpecDefaultImpl();
@@ -404,15 +402,9 @@ public class PipSizeSpecHandler {
mSizeSpecSourceImpl.reloadResources();
}
- /** Returns the display's bounds. */
@NonNull
- public Rect getDisplayBounds() {
- return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
- }
-
- /** Update the display layout. */
- public void setDisplayLayout(@NonNull DisplayLayout displayLayout) {
- mDisplayLayout.set(displayLayout);
+ private Rect getDisplayBounds() {
+ return mPipDisplayLayoutState.getDisplayBounds();
}
public Point getScreenEdgeInsets() {
@@ -424,11 +416,12 @@ public class PipSizeSpecHandler {
*/
public Rect getInsetBounds() {
Rect insetBounds = new Rect();
- Rect insets = mDisplayLayout.stableInsets();
+ DisplayLayout displayLayout = mPipDisplayLayoutState.getDisplayLayout();
+ Rect insets = displayLayout.stableInsets();
insetBounds.set(insets.left + mScreenEdgeInsets.x,
insets.top + mScreenEdgeInsets.y,
- mDisplayLayout.width() - insets.right - mScreenEdgeInsets.x,
- mDisplayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
+ displayLayout.width() - insets.right - mScreenEdgeInsets.x,
+ displayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
return insetBounds;
}
@@ -519,12 +512,24 @@ public class PipSizeSpecHandler {
}
}
+ @VisibleForTesting
+ boolean supportsPipSizeLargeScreen() {
+ // TODO(b/271468706): switch Tv to having a dedicated SizeSpecSource once the SizeSpecSource
+ // can be injected
+ return SystemProperties
+ .getBoolean("persist.wm.debug.enable_pip_size_large_screen", true) && !isTv();
+ }
+
+ private boolean isTv() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+ }
+
/** Dumps internal state. */
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
- pw.println(innerPrefix + "mSizeSpecSourceImpl=" + mSizeSpecSourceImpl.toString());
- pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout);
+ pw.println(innerPrefix + "mSizeSpecSourceImpl=" + mSizeSpecSourceImpl);
pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize);
+ pw.println(innerPrefix + "mScreenEdgeInsets=" + mScreenEdgeInsets);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 8f6cee76b68a..500094335258 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -199,11 +199,6 @@ public class PipTouchHandler {
mMotionHelper = pipMotionHelper;
mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger,
mMotionHelper, mainExecutor);
- mPipResizeGestureHandler =
- new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState,
- mMotionHelper, pipTaskOrganizer, mPipDismissTargetHandler,
- this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger,
- menuController, mainExecutor);
mTouchState = new PipTouchState(ViewConfiguration.get(context),
() -> {
if (mPipBoundsState.isStashed()) {
@@ -220,6 +215,11 @@ public class PipTouchHandler {
},
menuController::hideMenu,
mainExecutor);
+ mPipResizeGestureHandler =
+ new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState,
+ mMotionHelper, mTouchState, pipTaskOrganizer, mPipDismissTargetHandler,
+ this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger,
+ menuController, mainExecutor);
mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState,
mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(),
this::onAccessibilityShowMenu, this::updateMovementBounds,
@@ -556,6 +556,11 @@ public class PipTouchHandler {
return true;
}
+ // do not process input event if not allowed
+ if (!mTouchState.getAllowInputEvents()) {
+ return true;
+ }
+
MotionEvent ev = (MotionEvent) inputEvent;
if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) {
// Initialize the touch state for the gesture, but immediately reset to invalidate the
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java
index d7d69f27f9f8..5d858fa9aa3f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java
@@ -37,7 +37,7 @@ public class PipTouchState {
private static final boolean DEBUG = false;
@VisibleForTesting
- public static final long DOUBLE_TAP_TIMEOUT = 200;
+ public static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
static final long HOVER_EXIT_TIMEOUT = 50;
private final ShellExecutor mMainExecutor;
@@ -55,6 +55,9 @@ public class PipTouchState {
private final PointF mLastDelta = new PointF();
private final PointF mVelocity = new PointF();
private boolean mAllowTouches = true;
+
+ // Set to false to block both PipTouchHandler and PipResizeGestureHandler's input processing
+ private boolean mAllowInputEvents = true;
private boolean mIsUserInteracting = false;
// Set to true only if the multiple taps occur within the double tap timeout
private boolean mIsDoubleTap = false;
@@ -77,6 +80,20 @@ public class PipTouchState {
}
/**
+ * @return true if input processing is enabled for PiP in general.
+ */
+ public boolean getAllowInputEvents() {
+ return mAllowInputEvents;
+ }
+
+ /**
+ * @param allowInputEvents true to enable input processing for PiP in general.
+ */
+ public void setAllowInputEvents(boolean allowInputEvents) {
+ mAllowInputEvents = allowInputEvents;
+ }
+
+ /**
* Resets this state.
*/
public void reset() {
@@ -393,6 +410,7 @@ public class PipTouchState {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches);
+ pw.println(innerPrefix + "mAllowInputEvents=" + mAllowInputEvents);
pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId);
pw.println(innerPrefix + "mLastTouchDisplayId=" + mLastTouchDisplayId);
pw.println(innerPrefix + "mDownTouch=" + mDownTouch);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS
index 85441af9a870..5aa3c4e2abef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/OWNERS
@@ -1,2 +1,3 @@
# WM shell sub-module TV pip owner
galinap@google.com
+bronger@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java
new file mode 100644
index 000000000000..5f6b3fe1e250
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.wm.shell.common.TvWindowMenuActionButton;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+abstract class TvPipAction {
+
+ /**
+ * Extras key for adding a boolean to the {@link Notification.Action} to differentiate custom
+ * from system actions, most importantly to identify custom close actions.
+ **/
+ public static final String EXTRA_IS_PIP_CUSTOM_ACTION = "EXTRA_IS_PIP_CUSTOM_ACTION";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"ACTION_"}, value = {
+ ACTION_FULLSCREEN,
+ ACTION_CLOSE,
+ ACTION_MOVE,
+ ACTION_EXPAND_COLLAPSE,
+ ACTION_CUSTOM,
+ ACTION_CUSTOM_CLOSE
+ })
+ public @interface ActionType {
+ }
+
+ public static final int ACTION_FULLSCREEN = 0;
+ public static final int ACTION_CLOSE = 1;
+ public static final int ACTION_MOVE = 2;
+ public static final int ACTION_EXPAND_COLLAPSE = 3;
+ public static final int ACTION_CUSTOM = 4;
+ public static final int ACTION_CUSTOM_CLOSE = 5;
+
+ @ActionType
+ private final int mActionType;
+
+ @NonNull
+ private final SystemActionsHandler mSystemActionsHandler;
+
+ TvPipAction(@ActionType int actionType, @NonNull SystemActionsHandler systemActionsHandler) {
+ Objects.requireNonNull(systemActionsHandler);
+ mActionType = actionType;
+ mSystemActionsHandler = systemActionsHandler;
+ }
+
+ boolean isCloseAction() {
+ return mActionType == ACTION_CLOSE || mActionType == ACTION_CUSTOM_CLOSE;
+ }
+
+ @ActionType
+ int getActionType() {
+ return mActionType;
+ }
+
+ abstract void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler);
+
+ abstract PendingIntent getPendingIntent();
+
+ void executeAction() {
+ mSystemActionsHandler.executeAction(mActionType);
+ }
+
+ abstract Notification.Action toNotificationAction(Context context);
+
+ interface SystemActionsHandler {
+ void executeAction(@TvPipAction.ActionType int actionType);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java
new file mode 100644
index 000000000000..3b44f10ebe62
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java
@@ -0,0 +1,246 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_CLOSE_PIP;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_MOVE_PIP;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TOGGLE_EXPANDED_PIP;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TO_FULLSCREEN;
+
+import android.annotation.NonNull;
+import android.app.RemoteAction;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip.PipMediaController;
+import com.android.wm.shell.pip.PipUtils;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Creates the system TvPipActions (fullscreen, close, move, expand/collapse), and handles all the
+ * changes to the actions, including the custom app actions and media actions. Other components can
+ * listen to those changes.
+ */
+public class TvPipActionsProvider implements TvPipAction.SystemActionsHandler {
+ private static final String TAG = TvPipActionsProvider.class.getSimpleName();
+
+ private static final int CLOSE_ACTION_INDEX = 1;
+ private static final int FIRST_CUSTOM_ACTION_INDEX = 2;
+
+ private final List<Listener> mListeners = new ArrayList<>();
+ private final TvPipAction.SystemActionsHandler mSystemActionsHandler;
+
+ private final List<TvPipAction> mActionsList;
+ private final TvPipSystemAction mDefaultCloseAction;
+ private final TvPipSystemAction mExpandCollapseAction;
+
+ private final List<RemoteAction> mMediaActions = new ArrayList<>();
+ private final List<RemoteAction> mAppActions = new ArrayList<>();
+
+ public TvPipActionsProvider(Context context, PipMediaController pipMediaController,
+ TvPipAction.SystemActionsHandler systemActionsHandler) {
+ mSystemActionsHandler = systemActionsHandler;
+
+ mActionsList = new ArrayList<>();
+ mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen,
+ R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context,
+ mSystemActionsHandler));
+
+ mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close,
+ R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context, mSystemActionsHandler);
+ mActionsList.add(mDefaultCloseAction);
+
+ mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move,
+ R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context, mSystemActionsHandler));
+
+ mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse,
+ R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context,
+ mSystemActionsHandler);
+ mActionsList.add(mExpandCollapseAction);
+
+ pipMediaController.addActionListener(this::onMediaActionsChanged);
+ }
+
+ @Override
+ public void executeAction(@TvPipAction.ActionType int actionType) {
+ if (mSystemActionsHandler != null) {
+ mSystemActionsHandler.executeAction(actionType);
+ }
+ }
+
+ private void notifyActionsChanged(int added, int changed, int startIndex) {
+ for (Listener listener : mListeners) {
+ listener.onActionsChanged(added, changed, startIndex);
+ }
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ public void setAppActions(@NonNull List<RemoteAction> appActions, RemoteAction closeAction) {
+ // Update close action.
+ mActionsList.set(CLOSE_ACTION_INDEX,
+ closeAction == null ? mDefaultCloseAction
+ : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction,
+ mSystemActionsHandler));
+ notifyActionsChanged(/* added= */ 0, /* updated= */ 1, CLOSE_ACTION_INDEX);
+
+ // Replace custom actions with new ones.
+ mAppActions.clear();
+ for (RemoteAction action : appActions) {
+ if (action != null && !PipUtils.remoteActionsMatch(action, closeAction)) {
+ // Only show actions that aren't duplicates of the custom close action.
+ mAppActions.add(action);
+ }
+ }
+
+ updateCustomActions(mAppActions);
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public void onMediaActionsChanged(List<RemoteAction> actions) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onMediaActionsChanged()", TAG);
+
+ mMediaActions.clear();
+ // Don't show disabled actions.
+ for (RemoteAction remoteAction : actions) {
+ if (remoteAction.isEnabled()) {
+ mMediaActions.add(remoteAction);
+ }
+ }
+
+ updateCustomActions(mMediaActions);
+ }
+
+ private void updateCustomActions(@NonNull List<RemoteAction> customActions) {
+ List<RemoteAction> newCustomActions = customActions;
+ if (newCustomActions == mMediaActions && !mAppActions.isEmpty()) {
+ // Don't show the media actions while there are app actions.
+ return;
+ } else if (newCustomActions == mAppActions && mAppActions.isEmpty()) {
+ // If all the app actions were removed, show the media actions.
+ newCustomActions = mMediaActions;
+ }
+
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: replaceCustomActions, count: %d", TAG, newCustomActions.size());
+ int oldCustomActionsCount = 0;
+ for (TvPipAction action : mActionsList) {
+ if (action.getActionType() == ACTION_CUSTOM) {
+ oldCustomActionsCount++;
+ }
+ }
+ mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM);
+
+ List<TvPipAction> actions = new ArrayList<>();
+ for (RemoteAction action : newCustomActions) {
+ actions.add(new TvPipCustomAction(ACTION_CUSTOM, action, mSystemActionsHandler));
+ }
+ mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, actions);
+
+ int added = newCustomActions.size() - oldCustomActionsCount;
+ int changed = Math.min(newCustomActions.size(), oldCustomActionsCount);
+ notifyActionsChanged(added, changed, FIRST_CUSTOM_ACTION_INDEX);
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ public void updateExpansionEnabled(boolean enabled) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: updateExpansionState, enabled: %b", TAG, enabled);
+ int actionIndex = mActionsList.indexOf(mExpandCollapseAction);
+ boolean actionInList = actionIndex != -1;
+ if (enabled && !actionInList) {
+ mActionsList.add(mExpandCollapseAction);
+ actionIndex = mActionsList.size() - 1;
+ } else if (!enabled && actionInList) {
+ mActionsList.remove(actionIndex);
+ } else {
+ return;
+ }
+ notifyActionsChanged(/* added= */ enabled ? 1 : -1, /* updated= */ 0, actionIndex);
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ public void updatePipExpansionState(boolean expanded) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onPipExpansionToggled, expanded: %b", TAG, expanded);
+
+ boolean changed = mExpandCollapseAction.update(
+ expanded ? R.string.pip_collapse : R.string.pip_expand,
+ expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand);
+ if (changed) {
+ notifyActionsChanged(/* added= */ 0, /* updated= */ 1,
+ mActionsList.indexOf(mExpandCollapseAction));
+ }
+ }
+
+ List<TvPipAction> getActionsList() {
+ return mActionsList;
+ }
+
+ @NonNull
+ TvPipAction getCloseAction() {
+ return mActionsList.get(CLOSE_ACTION_INDEX);
+ }
+
+ void addListener(Listener listener) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+
+ /**
+ * Returns the index of the first action of the given action type or -1 if none can be found.
+ */
+ int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) {
+ for (int i = 0; i < mActionsList.size(); i++) {
+ if (mActionsList.get(i).getActionType() == actionType) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Allow components to listen to updates to the actions list, including where they happen so
+ * that changes can be animated.
+ */
+ interface Listener {
+ /**
+ * Notifies the listener how many actions were added/removed or updated.
+ *
+ * @param added can be positive (number of actions added), negative (number of actions
+ * removed) or zero (the number of actions stayed the same).
+ * @param updated the number of actions that might have been updated and need to be
+ * refreshed.
+ * @param startIndex The index of the first updated action. The added/removed actions start
+ * at (startIndex + updated).
+ */
+ void onActionsChanged(int added, int updated, int startIndex);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java
new file mode 100644
index 000000000000..0221db836dda
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java
@@ -0,0 +1,106 @@
+/*
+ * 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.pip.tv;
+
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU;
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU;
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.View;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+/**
+ * This view is part of the Tv PiP menu. It is drawn behind the PiP surface and serves as a
+ * background behind the PiP content. If the PiP content is translucent, this view is visible
+ * behind it.
+ * It is also used to draw the shadow behind the Tv PiP menu. The shadow intensity is determined
+ * by the menu mode that the Tv PiP menu is in. See {@link TvPipMenuController.TvPipMenuMode}.
+ */
+class TvPipBackgroundView extends FrameLayout {
+ private static final String TAG = "TvPipBackgroundView";
+
+ private final View mBackgroundView;
+ private final int mElevationNoMenu;
+ private final int mElevationMoveMenu;
+ private final int mElevationAllActionsMenu;
+ private final int mPipMenuFadeAnimationDuration;
+
+ private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU;
+
+ TvPipBackgroundView(@NonNull Context context) {
+ super(context, null, 0, 0);
+ inflate(context, R.layout.tv_pip_menu_background, this);
+
+ mBackgroundView = findViewById(R.id.background_view);
+
+ final Resources res = mContext.getResources();
+ mElevationNoMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_no_menu);
+ mElevationMoveMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_move_menu);
+ mElevationAllActionsMenu =
+ res.getDimensionPixelSize(R.dimen.pip_menu_elevation_all_actions_menu);
+ mPipMenuFadeAnimationDuration =
+ res.getInteger(R.integer.tv_window_menu_fade_animation_duration);
+ }
+
+ void transitionToMenuMode(@TvPipMenuController.TvPipMenuMode int pipMenuMode) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: transitionToMenuMode(), old menu mode = %s, new menu mode = %s",
+ TAG, TvPipMenuController.getMenuModeString(mCurrentMenuMode),
+ TvPipMenuController.getMenuModeString(pipMenuMode));
+
+ if (mCurrentMenuMode == pipMenuMode) return;
+
+ int elevation = mElevationNoMenu;
+ Interpolator interpolator = TvPipInterpolators.ENTER;
+ switch(pipMenuMode) {
+ case MODE_NO_MENU:
+ elevation = mElevationNoMenu;
+ interpolator = TvPipInterpolators.EXIT;
+ break;
+ case MODE_MOVE_MENU:
+ elevation = mElevationMoveMenu;
+ break;
+ case MODE_ALL_ACTIONS_MENU:
+ elevation = mElevationAllActionsMenu;
+ if (mCurrentMenuMode == MODE_MOVE_MENU) {
+ interpolator = TvPipInterpolators.EXIT;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown TV PiP menu mode: " + pipMenuMode);
+ }
+
+ mBackgroundView.animate()
+ .translationZ(elevation)
+ .setInterpolator(interpolator)
+ .setDuration(mPipMenuFadeAnimationDuration)
+ .start();
+
+ mCurrentMenuMode = pipMenuMode;
+ }
+
+}
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 22feb43ffd62..825b96921a22 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
@@ -22,14 +22,12 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_HORIZONTAL;
-import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_UNDETERMINED;
import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_VERTICAL;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
-import android.util.ArraySet;
import android.util.Size;
import android.view.Gravity;
@@ -51,11 +49,10 @@ import java.util.Set;
* Contains pip bounds calculations that are specific to TV.
*/
public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
-
private static final String TAG = TvPipBoundsAlgorithm.class.getSimpleName();
- private static final boolean DEBUG = TvPipController.DEBUG;
- private final @NonNull TvPipBoundsState mTvPipBoundsState;
+ @NonNull
+ private final TvPipBoundsState mTvPipBoundsState;
private int mFixedExpandedHeightInPx;
private int mFixedExpandedWidthInPx;
@@ -94,16 +91,15 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
/** Returns the destination bounds to place the PIP window on entry. */
@Override
public Rect getEntryDestinationBounds() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: getEntryDestinationBounds()", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: getEntryDestinationBounds()", TAG);
+
updateExpandedPipSize();
final boolean isPipExpanded = mTvPipBoundsState.isTvExpandedPipSupported()
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
&& !mTvPipBoundsState.isTvPipManuallyCollapsed();
if (isPipExpanded) {
- updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true);
+ updateGravityOnExpansionToggled(/* expanding= */ true);
}
mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
@@ -112,10 +108,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
@Override
public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio);
return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
}
@@ -143,25 +137,9 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
final Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
- Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
- Set<Rect> unrestrictedKeepClearAreas = mTvPipBoundsState.getUnrestrictedKeepClearAreas();
-
- if (mTvPipBoundsState.isImeShowing()) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: IME showing, height: %d",
- TAG, mTvPipBoundsState.getImeHeight());
- }
-
- final Rect imeBounds = new Rect(
- 0,
- insetBounds.bottom - mTvPipBoundsState.getImeHeight(),
- insetBounds.right,
- insetBounds.bottom);
-
- unrestrictedKeepClearAreas = new ArraySet<>(unrestrictedKeepClearAreas);
- unrestrictedKeepClearAreas.add(imeBounds);
- }
+ final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
+ final Set<Rect> unrestrictedKeepClearAreas =
+ mTvPipBoundsState.getUnrestrictedKeepClearAreas();
mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity());
mKeepClearAlgorithm.setScreenSize(screenSize);
@@ -175,165 +153,105 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
restrictedKeepClearAreas,
unrestrictedKeepClearAreas);
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: screenSize: %s", TAG, screenSize);
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: stashOffset: %d", TAG, mTvPipBoundsState.getStashOffset());
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: insetBounds: %s", TAG, insetBounds.toShortString());
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: pipSize: %s", TAG, pipSize);
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: gravity: %s", TAG, Gravity.toString(mTvPipBoundsState.getTvPipGravity()));
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: restrictedKeepClearAreas: %s", TAG, restrictedKeepClearAreas);
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: unrestrictedKeepClearAreas: %s", TAG, unrestrictedKeepClearAreas);
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: placement: %s", TAG, placement);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: screenSize: %s", TAG, screenSize);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: stashOffset: %d", TAG, mTvPipBoundsState.getStashOffset());
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: insetBounds: %s", TAG, insetBounds.toShortString());
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: pipSize: %s", TAG, pipSize);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: gravity: %s", TAG, Gravity.toString(mTvPipBoundsState.getTvPipGravity()));
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: restrictedKeepClearAreas: %s", TAG, restrictedKeepClearAreas);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: unrestrictedKeepClearAreas: %s", TAG, unrestrictedKeepClearAreas);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: placement: %s", TAG, placement);
return placement;
}
- /**
- * @return previous gravity if it is to be saved, or {@link Gravity#NO_GRAVITY} if not.
- */
- int updateGravityOnExpandToggled(int previousGravity, boolean expanding) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: updateGravityOnExpandToggled(), expanding: %b"
- + ", mOrientation: %d, previous gravity: %s",
- TAG, expanding, mTvPipBoundsState.getTvFixedPipOrientation(),
- Gravity.toString(previousGravity));
- }
-
- if (!mTvPipBoundsState.isTvExpandedPipSupported()) {
- return Gravity.NO_GRAVITY;
- }
+ void updateGravityOnExpansionToggled(boolean expanding) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: updateGravity, expanding: %b, fixedExpandedOrientation: %d",
+ TAG, expanding, mTvPipBoundsState.getTvFixedPipOrientation());
- if (expanding && mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_UNDETERMINED) {
- float expandedRatio = mTvPipBoundsState.getDesiredTvExpandedAspectRatio();
- if (expandedRatio == 0) {
- return Gravity.NO_GRAVITY;
- }
- if (expandedRatio < 1) {
- mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_VERTICAL);
- } else {
- mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_HORIZONTAL);
- }
- }
+ int currentX = mTvPipBoundsState.getTvPipGravity() & Gravity.HORIZONTAL_GRAVITY_MASK;
+ int currentY = mTvPipBoundsState.getTvPipGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+ int previousCollapsedX = mTvPipBoundsState.getTvPipPreviousCollapsedGravity()
+ & Gravity.HORIZONTAL_GRAVITY_MASK;
+ int previousCollapsedY = mTvPipBoundsState.getTvPipPreviousCollapsedGravity()
+ & Gravity.VERTICAL_GRAVITY_MASK;
- int gravityToSave = Gravity.NO_GRAVITY;
- int currentGravity = mTvPipBoundsState.getTvPipGravity();
int updatedGravity;
-
if (expanding) {
- // save collapsed gravity
- gravityToSave = mTvPipBoundsState.getTvPipGravity();
+ // Save collapsed gravity.
+ mTvPipBoundsState.setTvPipPreviousCollapsedGravity(mTvPipBoundsState.getTvPipGravity());
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
- updatedGravity =
- Gravity.CENTER_HORIZONTAL | (currentGravity
- & Gravity.VERTICAL_GRAVITY_MASK);
+ updatedGravity = Gravity.CENTER_HORIZONTAL | currentY;
} else {
- updatedGravity =
- Gravity.CENTER_VERTICAL | (currentGravity
- & Gravity.HORIZONTAL_GRAVITY_MASK);
+ updatedGravity = currentX | Gravity.CENTER_VERTICAL;
}
} else {
- if (previousGravity != Gravity.NO_GRAVITY) {
- // The pip hasn't been moved since expanding,
- // go back to previous collapsed position.
- updatedGravity = previousGravity;
+ // Collapse to the edge that the user moved to before.
+ if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
+ updatedGravity = previousCollapsedX | currentY;
} else {
- if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
- updatedGravity =
- Gravity.RIGHT | (currentGravity & Gravity.VERTICAL_GRAVITY_MASK);
- } else {
- updatedGravity =
- Gravity.BOTTOM | (currentGravity & Gravity.HORIZONTAL_GRAVITY_MASK);
- }
+ updatedGravity = currentX | previousCollapsedY;
}
}
mTvPipBoundsState.setTvPipGravity(updatedGravity);
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity));
- }
-
- return gravityToSave;
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity));
}
/**
- * @return true if gravity changed
+ * @return true if the gravity changed
*/
boolean updateGravity(int keycode) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: updateGravity, keycode: %d", TAG, keycode);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: updateGravity, keycode: %d", TAG, keycode);
- // Check if position change is valid
+ // Check if position change is valid.
if (mTvPipBoundsState.isTvPipExpanded()) {
- int mOrientation = mTvPipBoundsState.getTvFixedPipOrientation();
- if (mOrientation == ORIENTATION_VERTICAL
+ int fixedOrientation = mTvPipBoundsState.getTvFixedPipOrientation();
+ if (fixedOrientation == ORIENTATION_VERTICAL
&& (keycode == KEYCODE_DPAD_UP || keycode == KEYCODE_DPAD_DOWN)
- || mOrientation == ORIENTATION_HORIZONTAL
+ || fixedOrientation == ORIENTATION_HORIZONTAL
&& (keycode == KEYCODE_DPAD_RIGHT || keycode == KEYCODE_DPAD_LEFT)) {
return false;
}
}
- int currentGravity = mTvPipBoundsState.getTvPipGravity();
- int updatedGravity;
- // First axis
+ int updatedX = mTvPipBoundsState.getTvPipGravity() & Gravity.HORIZONTAL_GRAVITY_MASK;
+ int updatedY = mTvPipBoundsState.getTvPipGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+
switch (keycode) {
case KEYCODE_DPAD_UP:
- updatedGravity = Gravity.TOP;
+ updatedY = Gravity.TOP;
break;
case KEYCODE_DPAD_DOWN:
- updatedGravity = Gravity.BOTTOM;
+ updatedY = Gravity.BOTTOM;
break;
case KEYCODE_DPAD_LEFT:
- updatedGravity = Gravity.LEFT;
+ updatedX = Gravity.LEFT;
break;
case KEYCODE_DPAD_RIGHT:
- updatedGravity = Gravity.RIGHT;
+ updatedX = Gravity.RIGHT;
break;
default:
- updatedGravity = currentGravity;
+ // NOOP - unsupported keycode
}
- // Second axis
- switch (keycode) {
- case KEYCODE_DPAD_UP:
- case KEYCODE_DPAD_DOWN:
- if (mTvPipBoundsState.isTvPipExpanded()) {
- updatedGravity |= Gravity.CENTER_HORIZONTAL;
- } else {
- updatedGravity |= (currentGravity & Gravity.HORIZONTAL_GRAVITY_MASK);
- }
- break;
- case KEYCODE_DPAD_LEFT:
- case KEYCODE_DPAD_RIGHT:
- if (mTvPipBoundsState.isTvPipExpanded()) {
- updatedGravity |= Gravity.CENTER_VERTICAL;
- } else {
- updatedGravity |= (currentGravity & Gravity.VERTICAL_GRAVITY_MASK);
- }
- break;
- default:
- break;
- }
+ int updatedGravity = updatedX | updatedY;
- if (updatedGravity != currentGravity) {
+ if (updatedGravity != mTvPipBoundsState.getTvPipGravity()) {
mTvPipBoundsState.setTvPipGravity(updatedGravity);
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity));
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: updateGravity, new gravity: %s", TAG, Gravity.toString(updatedGravity));
return true;
}
return false;
@@ -364,8 +282,8 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
final Size expandedSize;
if (expandedRatio == 0) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: updateExpandedPipSize(): Expanded mode aspect ratio"
- + " of 0 not supported", TAG);
+ "%s: updateExpandedPipSize(): Expanded mode aspect ratio"
+ + " of 0 not supported", TAG);
return;
} else if (expandedRatio < 1) {
// vertical
@@ -378,16 +296,12 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
float aspectRatioHeight = mFixedExpandedWidthInPx / expandedRatio;
if (maxHeight > aspectRatioHeight) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Accommodate aspect ratio", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Accommodate aspect ratio", TAG);
expandedSize = new Size(mFixedExpandedWidthInPx, (int) aspectRatioHeight);
} else {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Aspect ratio is too extreme, use max size", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Aspect ratio is too extreme, use max size", TAG);
expandedSize = new Size(mFixedExpandedWidthInPx, maxHeight);
}
}
@@ -401,26 +315,20 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
- pipDecorations.left - pipDecorations.right;
float aspectRatioWidth = mFixedExpandedHeightInPx * expandedRatio;
if (maxWidth > aspectRatioWidth) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Accommodate aspect ratio", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Accommodate aspect ratio", TAG);
expandedSize = new Size((int) aspectRatioWidth, mFixedExpandedHeightInPx);
} else {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Aspect ratio is too extreme, use max size", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Aspect ratio is too extreme, use max size", TAG);
expandedSize = new Size(maxWidth, mFixedExpandedHeightInPx);
}
}
}
mTvPipBoundsState.setTvExpandedSize(expandedSize);
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: updateExpandedPipSize(): expanded size, width: %d, height: %d",
- TAG, expandedSize.getWidth(), expandedSize.getHeight());
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: updateExpandedPipSize(): expanded size, width: %d, height: %d",
+ TAG, expandedSize.getWidth(), expandedSize.getHeight());
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java
index 3a6ce81821ec..8d4a38442ce5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java
@@ -39,7 +39,6 @@ import java.util.function.Supplier;
* Manages debouncing of PiP movements and scheduling of unstashing.
*/
public class TvPipBoundsController {
- private static final boolean DEBUG = false;
private static final String TAG = "TvPipBoundsController";
/**
@@ -122,22 +121,22 @@ public class TvPipBoundsController {
cancelScheduledPlacement();
applyPlacementBounds(placement.getUnstashedBounds(), animationDuration);
} else if (immediate) {
+ boolean shouldStash = mUnstashRunnable != null || placement.getTriggerStash();
cancelScheduledPlacement();
- applyPlacementBounds(placement.getBounds(), animationDuration);
- scheduleUnstashIfNeeded(placement);
+ applyPlacement(placement, shouldStash, animationDuration);
} else {
- applyPlacementBounds(mCurrentPlacementBounds, animationDuration);
+ if (mCurrentPlacementBounds != null) {
+ applyPlacementBounds(mCurrentPlacementBounds, animationDuration);
+ }
schedulePinnedStackPlacement(placement, animationDuration);
}
}
private void schedulePinnedStackPlacement(@NonNull final Placement placement,
int animationDuration) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: schedulePinnedStackPlacement() - pip bounds: %s",
- TAG, placement.getBounds().toShortString());
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: schedulePinnedStackPlacement() - pip bounds: %s",
+ TAG, placement.getBounds().toShortString());
if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(),
placement.getBounds())) {
@@ -171,30 +170,27 @@ public class TvPipBoundsController {
}
private void applyPendingPlacement() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: applyPendingPlacement()", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: applyPendingPlacement()", TAG);
if (mPendingPlacement != null) {
- if (mPendingStash) {
- mPendingStash = false;
- scheduleUnstashIfNeeded(mPendingPlacement);
- }
+ applyPlacement(mPendingPlacement, mPendingStash, mPendingPlacementAnimationDuration);
+ mPendingStash = false;
+ mPendingPlacement = null;
+ }
+ }
- if (mUnstashRunnable != null) {
- // currently stashed, use stashed pos
- applyPlacementBounds(mPendingPlacement.getBounds(),
- mPendingPlacementAnimationDuration);
- } else {
- applyPlacementBounds(mPendingPlacement.getUnstashedBounds(),
- mPendingPlacementAnimationDuration);
- }
+ private void applyPlacement(@NonNull final Placement placement, boolean shouldStash,
+ int animationDuration) {
+ if (placement.getStashType() != STASH_TYPE_NONE && shouldStash) {
+ scheduleUnstashIfNeeded(placement);
}
- mPendingPlacement = null;
+ Rect bounds =
+ mUnstashRunnable != null ? placement.getBounds() : placement.getUnstashedBounds();
+ applyPlacementBounds(bounds, animationDuration);
}
- void onPipDismissed() {
+ void reset() {
mCurrentPlacementBounds = null;
mPipTargetBounds = null;
cancelScheduledPlacement();
@@ -227,10 +223,8 @@ public class TvPipBoundsController {
}
mPipTargetBounds = bounds;
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString());
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString());
if (mListener != null) {
mListener.onPipTargetBoundsChange(bounds, animationDuration);
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 4e3ee51326c1..e1737eccc6e1 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
@@ -27,9 +27,11 @@ import android.content.pm.PackageManager;
import android.graphics.Insets;
import android.util.Size;
import android.view.Gravity;
+import android.view.View;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import java.lang.annotation.Retention;
@@ -53,25 +55,63 @@ public class TvPipBoundsState extends PipBoundsState {
public @interface Orientation {
}
- public static final int DEFAULT_TV_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT;
+ private final Context mContext;
+
+ private int mDefaultGravity;
+ private int mTvPipGravity;
+ private int mPreviousCollapsedGravity;
+ private boolean mIsRtl;
private final boolean mIsTvExpandedPipSupported;
private boolean mIsTvPipExpanded;
private boolean mTvPipManuallyCollapsed;
private float mDesiredTvExpandedAspectRatio;
- private @Orientation int mTvFixedPipOrientation;
- private int mTvPipGravity;
- private @Nullable Size mTvExpandedSize;
- private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE;
- private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
+ @Orientation
+ private int mTvFixedPipOrientation;
+ @Nullable
+ private Size mTvExpandedSize;
+ @NonNull
+ private Insets mPipMenuPermanentDecorInsets = Insets.NONE;
+ @NonNull
+ private Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
public TvPipBoundsState(@NonNull Context context,
- @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
- super(context, pipSizeSpecHandler);
+ @NonNull PipSizeSpecHandler pipSizeSpecHandler,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState) {
+ super(context, pipSizeSpecHandler, pipDisplayLayoutState);
+ mContext = context;
+ updateDefaultGravity();
+ mPreviousCollapsedGravity = mDefaultGravity;
mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE);
}
+ public int getDefaultGravity() {
+ return mDefaultGravity;
+ }
+
+ private void updateDefaultGravity() {
+ boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_RTL;
+ mDefaultGravity = Gravity.BOTTOM | (isRtl ? Gravity.LEFT : Gravity.RIGHT);
+
+ if (mIsRtl != isRtl) {
+ int prevGravityX = mPreviousCollapsedGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ int prevGravityY = mPreviousCollapsedGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if ((prevGravityX & Gravity.RIGHT) == Gravity.RIGHT) {
+ mPreviousCollapsedGravity = Gravity.LEFT | prevGravityY;
+ } else if ((prevGravityX & Gravity.LEFT) == Gravity.LEFT) {
+ mPreviousCollapsedGravity = Gravity.RIGHT | prevGravityY;
+ }
+ }
+ mIsRtl = isRtl;
+ }
+
+ @Override
+ public void onConfigurationChanged() {
+ updateDefaultGravity();
+ }
+
/**
* Initialize states when first entering PiP.
*/
@@ -88,7 +128,9 @@ public class TvPipBoundsState extends PipBoundsState {
/** Resets the TV PiP state for a new activity. */
public void resetTvPipState() {
mTvFixedPipOrientation = ORIENTATION_UNDETERMINED;
- mTvPipGravity = DEFAULT_TV_GRAVITY;
+ mTvPipGravity = mDefaultGravity;
+ mPreviousCollapsedGravity = mDefaultGravity;
+ mTvPipManuallyCollapsed = false;
}
/** Set the tv expanded bounds of PiP */
@@ -103,16 +145,23 @@ public class TvPipBoundsState extends PipBoundsState {
}
/** Set the PiP aspect ratio for the expanded PiP (TV) that is desired by the app. */
- public void setDesiredTvExpandedAspectRatio(float aspectRatio, boolean override) {
+ public void setDesiredTvExpandedAspectRatio(float expandedAspectRatio, boolean override) {
if (override || mTvFixedPipOrientation == ORIENTATION_UNDETERMINED) {
- mDesiredTvExpandedAspectRatio = aspectRatio;
resetTvPipState();
+ mDesiredTvExpandedAspectRatio = expandedAspectRatio;
+ if (expandedAspectRatio != 0) {
+ if (expandedAspectRatio > 1) {
+ mTvFixedPipOrientation = ORIENTATION_HORIZONTAL;
+ } else {
+ mTvFixedPipOrientation = ORIENTATION_VERTICAL;
+ }
+ }
return;
}
- if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL)
- || (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)
- || aspectRatio == 0) {
- mDesiredTvExpandedAspectRatio = aspectRatio;
+ if ((expandedAspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL)
+ || (expandedAspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)
+ || expandedAspectRatio == 0) {
+ mDesiredTvExpandedAspectRatio = expandedAspectRatio;
}
}
@@ -124,11 +173,6 @@ public class TvPipBoundsState extends PipBoundsState {
return mDesiredTvExpandedAspectRatio;
}
- /** Sets the orientation the expanded TV PiP activity has been fixed to. */
- public void setTvFixedPipOrientation(@Orientation int orientation) {
- mTvFixedPipOrientation = orientation;
- }
-
/** Returns the fixed orientation of the expanded PiP on TV. */
@Orientation
public int getTvFixedPipOrientation() {
@@ -145,6 +189,14 @@ public class TvPipBoundsState extends PipBoundsState {
return mTvPipGravity;
}
+ public void setTvPipPreviousCollapsedGravity(int gravity) {
+ mPreviousCollapsedGravity = gravity;
+ }
+
+ public int getTvPipPreviousCollapsedGravity() {
+ return mPreviousCollapsedGravity;
+ }
+
/** Sets whether the TV PiP is currently expanded. */
public void setTvPipExpanded(boolean expanded) {
mIsTvPipExpanded = expanded;
@@ -174,7 +226,8 @@ public class TvPipBoundsState extends PipBoundsState {
mPipMenuPermanentDecorInsets = permanentInsets;
}
- public @NonNull Insets getPipMenuPermanentDecorInsets() {
+ @NonNull
+ public Insets getPipMenuPermanentDecorInsets() {
return mPipMenuPermanentDecorInsets;
}
@@ -182,7 +235,8 @@ public class TvPipBoundsState extends PipBoundsState {
mPipMenuTemporaryDecorInsets = temporaryDecorInsets;
}
- public @NonNull Insets getPipMenuTemporaryDecorInsets() {
+ @NonNull
+ public Insets getPipMenuTemporaryDecorInsets() {
return mPipMenuTemporaryDecorInsets;
}
}
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 6bc666f074e9..02eeb2ac4fd5 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
@@ -18,17 +18,22 @@ package com.android.wm.shell.pip.tv;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
+import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import android.annotation.IntDef;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
-import android.app.PendingIntent;
import android.app.RemoteAction;
import android.app.TaskInfo;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.os.Handler;
import android.os.RemoteException;
import android.view.Gravity;
@@ -46,11 +51,11 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipMediaController;
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.phone.PipSizeSpecHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellController;
@@ -68,10 +73,9 @@ import java.util.Set;
*/
public class TvPipController implements PipTransitionController.PipTransitionCallback,
TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
- TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener,
- ConfigurationChangeListener, UserChangeListener {
+ DisplayController.OnDisplaysChangedListener, ConfigurationChangeListener,
+ UserChangeListener {
private static final String TAG = "TvPipController";
- static final boolean DEBUG = false;
private static final int NONEXISTENT_TASK_ID = -1;
@@ -81,7 +85,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
STATE_PIP,
STATE_PIP_MENU,
})
- public @interface State {}
+ public @interface State {
+ }
/**
* State when there is no applications in Pip.
@@ -99,16 +104,28 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
*/
private static final int STATE_PIP_MENU = 2;
+ static final String ACTION_SHOW_PIP_MENU =
+ "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
+ static final String ACTION_CLOSE_PIP =
+ "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP";
+ static final String ACTION_MOVE_PIP =
+ "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
+ static final String ACTION_TOGGLE_EXPANDED_PIP =
+ "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP";
+ static final String ACTION_TO_FULLSCREEN =
+ "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN";
+
private final Context mContext;
private final ShellController mShellController;
private final TvPipBoundsState mTvPipBoundsState;
- private final PipSizeSpecHandler mPipSizeSpecHandler;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final TvPipBoundsController mTvPipBoundsController;
private final PipAppOpsListener mAppOpsListener;
private final PipTaskOrganizer mPipTaskOrganizer;
private final PipMediaController mPipMediaController;
+ private final TvPipActionsProvider mTvPipActionsProvider;
private final TvPipNotificationController mPipNotificationController;
private final TvPipMenuController mTvPipMenuController;
private final PipTransitionController mPipTransitionController;
@@ -117,25 +134,27 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private final DisplayController mDisplayController;
private final WindowManagerShellWrapper mWmShellWrapper;
private final ShellExecutor mMainExecutor;
+ private final Handler mMainHandler; // For registering the broadcast receiver
private final TvPipImpl mImpl = new TvPipImpl();
- private @State int mState = STATE_NO_PIP;
- private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
+ private final ActionBroadcastReceiver mActionBroadcastReceiver;
+
+ @State
+ private int mState = STATE_NO_PIP;
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
- private RemoteAction mCloseAction;
// How long the shell will wait for the app to close the PiP if a custom action is set.
private int mPipForceCloseDelay;
private int mResizeAnimationDuration;
- private int mEduTextWindowExitAnimationDurationMs;
+ private int mEduTextWindowExitAnimationDuration;
public static Pip create(
Context context,
ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
- PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
@@ -148,13 +167,14 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper wmShell,
+ Handler mainHandler,
ShellExecutor mainExecutor) {
return new TvPipController(
context,
shellInit,
shellController,
tvPipBoundsState,
- pipSizeSpecHandler,
+ pipDisplayLayoutState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
pipAppOpsListener,
@@ -167,6 +187,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
pipParamsChangedForwarder,
displayController,
wmShell,
+ mainHandler,
mainExecutor).mImpl;
}
@@ -175,7 +196,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
- PipSizeSpecHandler pipSizeSpecHandler,
+ PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
@@ -188,8 +209,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper wmShellWrapper,
+ Handler mainHandler,
ShellExecutor mainExecutor) {
mContext = context;
+ mMainHandler = mainHandler;
mMainExecutor = mainExecutor;
mShellController = shellController;
mDisplayController = displayController;
@@ -197,21 +220,27 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
DisplayLayout layout = new DisplayLayout(context, context.getDisplay());
mTvPipBoundsState = tvPipBoundsState;
- mTvPipBoundsState.setDisplayLayout(layout);
- mTvPipBoundsState.setDisplayId(context.getDisplayId());
- mPipSizeSpecHandler = pipSizeSpecHandler;
- mPipSizeSpecHandler.setDisplayLayout(layout);
+
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+ mPipDisplayLayoutState.setDisplayLayout(layout);
+ mPipDisplayLayoutState.setDisplayId(context.getDisplayId());
+
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
mTvPipBoundsController = tvPipBoundsController;
mTvPipBoundsController.setListener(this);
mPipMediaController = pipMediaController;
+ mTvPipActionsProvider = new TvPipActionsProvider(context, pipMediaController,
+ this::executeAction);
mPipNotificationController = pipNotificationController;
- mPipNotificationController.setDelegate(this);
+ mPipNotificationController.setTvPipActionsProvider(mTvPipActionsProvider);
mTvPipMenuController = tvPipMenuController;
mTvPipMenuController.setDelegate(this);
+ mTvPipMenuController.setTvPipActionsProvider(mTvPipActionsProvider);
+
+ mActionBroadcastReceiver = new ActionBroadcastReceiver();
mAppOpsListener = pipAppOpsListener;
mPipTaskOrganizer = pipTaskOrganizer;
@@ -225,7 +254,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private void onInit() {
mPipTransitionController.registerPipTransitionCallback(this);
- loadConfigurations();
+ reloadResources();
registerPipParamsChangedListener(mPipParamsChangedForwarder);
registerTaskStackListenerCallback(mTaskStackListener);
@@ -245,22 +274,41 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
@Override
public void onConfigurationChanged(Configuration newConfig) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
- if (isPipShown()) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: > closing Pip.", TAG);
- }
- closePip();
- }
+ int previousDefaultGravityX = mTvPipBoundsState.getDefaultGravity()
+ & Gravity.HORIZONTAL_GRAVITY_MASK;
+
+ reloadResources();
- loadConfigurations();
- mPipNotificationController.onConfigurationChanged(mContext);
+ mPipNotificationController.onConfigurationChanged();
mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
+ mTvPipBoundsState.onConfigurationChanged();
+
+ int defaultGravityX = mTvPipBoundsState.getDefaultGravity()
+ & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if (isPipShown() && previousDefaultGravityX != defaultGravityX) {
+ movePipToOppositeSide();
+ }
+ }
+
+ private void reloadResources() {
+ final Resources res = mContext.getResources();
+ mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
+ mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
+ mEduTextWindowExitAnimationDuration =
+ res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
+ }
+
+ private void movePipToOppositeSide() {
+ ProtoLog.i(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: movePipToOppositeSide", TAG);
+ if ((mTvPipBoundsState.getTvPipGravity() & Gravity.RIGHT) == Gravity.RIGHT) {
+ movePip(KEYCODE_DPAD_LEFT);
+ } else if ((mTvPipBoundsState.getTvPipGravity() & Gravity.LEFT) == Gravity.LEFT) {
+ movePip(KEYCODE_DPAD_RIGHT);
+ }
}
/**
@@ -274,33 +322,32 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
* Starts the process if bringing up the Pip menu if by issuing a command to move Pip
* task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip
* task/window is properly positioned in {@link #onPipTransitionFinished(int)}.
+ *
+ * @param moveMenu If true, show the moveMenu, otherwise show the regular menu.
*/
- @Override
- public void showPictureInPictureMenu() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState));
- }
+ private void showPictureInPictureMenu(boolean moveMenu) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState));
if (mState == STATE_NO_PIP) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: > cannot open Menu from the current state.", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: > cannot open Menu from the current state.", TAG);
return;
}
setState(STATE_PIP_MENU);
- mTvPipMenuController.showMenu();
+ if (moveMenu) {
+ mTvPipMenuController.showMovementMenu();
+ } else {
+ mTvPipMenuController.showMenu();
+ }
updatePinnedStackBounds();
}
@Override
public void onMenuClosed() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: closeMenu(), state before=%s", TAG, stateToName(mState));
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: closeMenu(), state before=%s", TAG, stateToName(mState));
setState(STATE_PIP);
updatePinnedStackBounds();
}
@@ -313,69 +360,40 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
/**
* Opens the "Pip-ed" Activity fullscreen.
*/
- @Override
- public void movePipToFullscreen() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
- }
+ private void movePipToFullscreen() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */);
onPipDisappeared();
}
- @Override
- public void togglePipExpansion() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: togglePipExpansion()", TAG);
- }
+ private void togglePipExpansion() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: togglePipExpansion()", TAG);
boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
- int saveGravity = mTvPipBoundsAlgorithm
- .updateGravityOnExpandToggled(mPreviousGravity, expanding);
- if (saveGravity != Gravity.NO_GRAVITY) {
- mPreviousGravity = saveGravity;
- }
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(expanding);
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
mTvPipBoundsState.setTvPipExpanded(expanding);
- mPipNotificationController.updateExpansionState();
updatePinnedStackBounds();
}
@Override
- public void enterPipMovementMenu() {
- setState(STATE_PIP_MENU);
- mTvPipMenuController.showMovementMenuOnly();
- }
-
- @Override
public void movePip(int keycode) {
if (mTvPipBoundsAlgorithm.updateGravity(keycode)) {
mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity());
- mPreviousGravity = Gravity.NO_GRAVITY;
updatePinnedStackBounds();
} else {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Position hasn't changed", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Position hasn't changed", TAG);
}
}
@Override
- public int getPipGravity() {
- return mTvPipBoundsState.getTvPipGravity();
- }
-
- public int getOrientation() {
- return mTvPipBoundsState.getTvFixedPipOrientation();
- }
-
- @Override
public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
Set<Rect> unrestricted) {
- if (mTvPipBoundsState.getDisplayId() == displayId) {
+ if (mPipDisplayLayoutState.getDisplayId() == displayId) {
boolean unrestrictedAreasChanged = !Objects.equals(unrestricted,
mTvPipBoundsState.getUnrestrictedKeepClearAreas());
mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
@@ -401,34 +419,25 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
@Override
- public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
- mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
- animationDuration, rect -> mTvPipMenuController.updateExpansionState());
- mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
+ public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+ mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
+ animationDuration, null);
+ mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
}
/**
* Closes Pip window.
*/
- @Override
public void closePip() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState),
- mCloseAction);
- }
+ closeCurrentPiP(mPinnedTaskId);
+ }
- if (mCloseAction != null) {
- try {
- mCloseAction.getActionIntent().send();
- } catch (PendingIntent.CanceledException e) {
- ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Failed to send close action, %s", TAG, e);
- }
- mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay);
- } else {
- closeCurrentPiP(mPinnedTaskId);
- }
+ /**
+ * Force close the current PiP after some time in case the custom action hasn't done it by
+ * itself.
+ */
+ public void customClosePip() {
+ mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay);
}
private void closeCurrentPiP(int pinnedTaskId) {
@@ -437,37 +446,22 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
"%s: PiP has already been closed by custom close action", TAG);
return;
}
- removeTask(mPinnedTaskId);
+ mPipTaskOrganizer.removePip();
onPipDisappeared();
}
@Override
public void closeEduText() {
- updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
+ updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false);
}
private void registerSessionListenerForCurrentUser() {
mPipMediaController.registerSessionListenerForCurrentUser();
}
- private void checkIfPinnedTaskAppeared() {
- final TaskInfo pinnedTask = getPinnedTaskInfo();
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: checkIfPinnedTaskAppeared(), task=%s", TAG, pinnedTask);
- }
- if (pinnedTask == null || pinnedTask.topActivity == null) return;
- mPinnedTaskId = pinnedTask.taskId;
-
- mPipMediaController.onActivityPinned();
- mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
- }
-
private void checkIfPinnedTaskIsGone() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onTaskStackChanged()", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onTaskStackChanged()", TAG);
if (isPipShown() && getPinnedTaskInfo() == null) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -477,71 +471,79 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
private void onPipDisappeared() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipDisappeared() state=%s", TAG, stateToName(mState));
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onPipDisappeared() state=%s", TAG, stateToName(mState));
mPipNotificationController.dismiss();
+ mActionBroadcastReceiver.unregister();
+
mTvPipMenuController.closeMenu();
mTvPipBoundsState.resetTvPipState();
- mTvPipBoundsController.onPipDismissed();
+ mTvPipBoundsController.reset();
setState(STATE_NO_PIP);
mPinnedTaskId = NONEXISTENT_TASK_ID;
}
@Override
- public void onPipTransitionStarted(int direction, Rect pipBounds) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
+ public void onPipTransitionStarted(int direction, Rect currentPipBounds) {
+ final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
+ if (enterPipTransition && mState == STATE_NO_PIP) {
+ // Set the initial ability to expand the PiP when entering PiP.
+ updateExpansionState();
}
- mTvPipMenuController.notifyPipAnimating(true);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onPipTransition_Started(), state=%s, direction=%d",
+ TAG, stateToName(mState), direction);
}
@Override
public void onPipTransitionCanceled(int direction) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
- }
- mTvPipMenuController.notifyPipAnimating(false);
+ 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());
}
@Override
public void onPipTransitionFinished(int direction) {
- if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
+ final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
+ if (enterPipTransition && mState == STATE_NO_PIP) {
setState(STATE_PIP);
}
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
- }
- mTvPipMenuController.notifyPipAnimating(false);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onPipTransition_Finished(), state=%s, direction=%d",
+ TAG, stateToName(mState), direction);
+ mTvPipMenuController.onPipTransitionFinished(enterPipTransition);
+ mTvPipActionsProvider.updatePipExpansionState(mTvPipBoundsState.isTvPipExpanded());
}
- private void setState(@State int state) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: setState(), state=%s, prev=%s",
- TAG, stateToName(state), stateToName(mState));
- }
- mState = state;
+ private void updateExpansionState() {
+ mTvPipActionsProvider.updateExpansionEnabled(mTvPipBoundsState.isTvExpandedPipSupported()
+ && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0);
}
- private void loadConfigurations() {
- final Resources res = mContext.getResources();
- mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
- mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
- mEduTextWindowExitAnimationDurationMs =
- res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
+ private void setState(@State int state) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: setState(), state=%s, prev=%s",
+ TAG, stateToName(state), stateToName(mState));
+ mState = state;
}
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
taskStackListener.addListener(new TaskStackListenerCallback() {
@Override
public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
- checkIfPinnedTaskAppeared();
+ final TaskInfo pinnedTask = getPinnedTaskInfo();
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onActivityPinned(), task=%s", TAG, pinnedTask);
+ if (pinnedTask == null || pinnedTask.topActivity == null) return;
+ mPinnedTaskId = pinnedTask.taskId;
+
+ mPipMediaController.onActivityPinned();
+ mActionBroadcastReceiver.register();
+ mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
+ mTvPipBoundsController.reset();
mAppOpsListener.onActivityPinned(packageName);
}
@@ -559,11 +561,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
if (task.getWindowingMode() == WINDOWING_MODE_PINNED) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPinnedActivityRestartAttempt()", TAG);
- }
-
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onPinnedActivityRestartAttempt()", TAG);
// If the "Pip-ed" Activity is launched again by Launcher or intent, make it
// fullscreen.
movePipToFullscreen();
@@ -578,16 +577,15 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public void onActionsChanged(List<RemoteAction> actions,
RemoteAction closeAction) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onActionsChanged()", TAG);
+ "%s: onActionsChanged()", TAG);
- mTvPipMenuController.setAppActions(actions, closeAction);
- mCloseAction = closeAction;
+ mTvPipActionsProvider.setAppActions(actions, closeAction);
}
@Override
public void onAspectRatioChanged(float ratio) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onAspectRatioChanged: %f", TAG, ratio);
+ "%s: onAspectRatioChanged: %f", TAG, ratio);
mTvPipBoundsState.setAspectRatio(ratio);
if (!mTvPipBoundsState.isTvPipExpanded()) {
@@ -598,10 +596,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
@Override
public void onExpandedAspectRatioChanged(float ratio) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onExpandedAspectRatioChanged: %f", TAG, ratio);
+ "%s: onExpandedAspectRatioChanged: %f", TAG, ratio);
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false);
- mTvPipMenuController.updateExpansionState();
+ updateExpansionState();
// 1) PiP is expanded and only aspect ratio changed, but wasn't disabled
// --> update bounds, but don't toggle
@@ -613,11 +611,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
// 2) PiP is expanded, but expanded PiP was disabled
// --> collapse PiP
if (mTvPipBoundsState.isTvPipExpanded() && ratio == 0) {
- int saveGravity = mTvPipBoundsAlgorithm
- .updateGravityOnExpandToggled(mPreviousGravity, false);
- if (saveGravity != Gravity.NO_GRAVITY) {
- mPreviousGravity = saveGravity;
- }
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(/* expanding= */ false);
mTvPipBoundsState.setTvPipExpanded(false);
updatePinnedStackBounds();
}
@@ -627,11 +621,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
mTvPipBoundsAlgorithm.updateExpandedPipSize();
- int saveGravity = mTvPipBoundsAlgorithm
- .updateGravityOnExpandToggled(mPreviousGravity, true);
- if (saveGravity != Gravity.NO_GRAVITY) {
- mPreviousGravity = saveGravity;
- }
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(/* expanding= */ true);
mTvPipBoundsState.setTvPipExpanded(true);
updatePinnedStackBounds();
}
@@ -644,11 +634,9 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
wmShell.addPinnedStackListener(new PinnedStackListenerForwarder.PinnedTaskListener() {
@Override
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onImeVisibilityChanged(), visible=%b, height=%d",
- TAG, imeVisible, imeHeight);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onImeVisibilityChanged(), visible=%b, height=%d",
+ TAG, imeVisible, imeHeight);
if (imeVisible == mTvPipBoundsState.isImeShowing()
&& (!imeVisible || imeHeight == mTvPipBoundsState.getImeHeight())) {
@@ -670,17 +658,13 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
private static TaskInfo getPinnedTaskInfo() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: getPinnedTaskInfo()", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: getPinnedTaskInfo()", TAG);
try {
final TaskInfo taskInfo = ActivityTaskManager.getService().getRootTaskInfo(
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: taskInfo=%s", TAG, taskInfo);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: taskInfo=%s", TAG, taskInfo);
return taskInfo;
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -689,19 +673,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
}
- private static void removeTask(int taskId) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: removeTask(), taskId=%d", TAG, taskId);
- }
- try {
- ActivityTaskManager.getService().removeTask(taskId);
- } catch (Exception e) {
- ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Atm.removeTask() failed, %s", TAG, e);
- }
- }
-
private static String stateToName(@State int state) {
switch (state) {
case STATE_NO_PIP:
@@ -716,6 +687,90 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
}
+ private void executeAction(@TvPipAction.ActionType int actionType) {
+ switch (actionType) {
+ case TvPipAction.ACTION_FULLSCREEN:
+ movePipToFullscreen();
+ break;
+ case TvPipAction.ACTION_CLOSE:
+ closePip();
+ break;
+ case TvPipAction.ACTION_MOVE:
+ showPictureInPictureMenu(/* moveMenu= */ true);
+ break;
+ case TvPipAction.ACTION_CUSTOM_CLOSE:
+ customClosePip();
+ break;
+ case TvPipAction.ACTION_EXPAND_COLLAPSE:
+ togglePipExpansion();
+ break;
+ default:
+ // NOOP
+ break;
+ }
+ }
+
+ private class ActionBroadcastReceiver extends BroadcastReceiver {
+ private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
+
+ final IntentFilter mIntentFilter;
+
+ {
+ mIntentFilter = new IntentFilter();
+ mIntentFilter.addAction(ACTION_CLOSE_PIP);
+ mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
+ mIntentFilter.addAction(ACTION_MOVE_PIP);
+ mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
+ mIntentFilter.addAction(ACTION_TO_FULLSCREEN);
+ }
+
+ boolean mRegistered = false;
+
+ void register() {
+ if (mRegistered) return;
+
+ mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION,
+ mMainHandler, Context.RECEIVER_NOT_EXPORTED);
+ mRegistered = true;
+ }
+
+ void unregister() {
+ if (!mRegistered) return;
+
+ mContext.unregisterReceiver(this);
+ mRegistered = false;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: on(Broadcast)Receive(), action=%s", TAG, action);
+
+ if (ACTION_SHOW_PIP_MENU.equals(action)) {
+ showPictureInPictureMenu(/* moveMenu= */ false);
+ } else {
+ executeAction(getCorrespondingActionType(action));
+ }
+ }
+
+ @TvPipAction.ActionType
+ private int getCorrespondingActionType(String broadcast) {
+ if (ACTION_CLOSE_PIP.equals(broadcast)) {
+ return TvPipAction.ACTION_CLOSE;
+ } else if (ACTION_MOVE_PIP.equals(broadcast)) {
+ return TvPipAction.ACTION_MOVE;
+ } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(broadcast)) {
+ return TvPipAction.ACTION_EXPAND_COLLAPSE;
+ } else if (ACTION_TO_FULLSCREEN.equals(broadcast)) {
+ return TvPipAction.ACTION_FULLSCREEN;
+ }
+
+ // Default: handle it like an action we don't know the content of.
+ return TvPipAction.ACTION_CUSTOM;
+ }
+ }
+
private class TvPipImpl implements Pip {
// Not used
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java
new file mode 100644
index 000000000000..977aad4a898a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE;
+import static android.app.Notification.Action.SEMANTIC_ACTION_NONE;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.TvWindowMenuActionButton;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A TvPipAction for actions that the app provides via {@link
+ * android.app.PictureInPictureParams.Builder#setCloseAction(RemoteAction)} or {@link
+ * android.app.PictureInPictureParams.Builder#setActions(List)}.
+ */
+public class TvPipCustomAction extends TvPipAction {
+ private static final String TAG = TvPipCustomAction.class.getSimpleName();
+
+ private final RemoteAction mRemoteAction;
+
+ TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction,
+ SystemActionsHandler systemActionsHandler) {
+ super(actionType, systemActionsHandler);
+ Objects.requireNonNull(remoteAction);
+ mRemoteAction = remoteAction;
+ }
+
+ void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) {
+ if (button == null || mainHandler == null) return;
+ if (mRemoteAction.getContentDescription().length() > 0) {
+ button.setTextAndDescription(mRemoteAction.getContentDescription());
+ } else {
+ button.setTextAndDescription(mRemoteAction.getTitle());
+ }
+ button.setImageIconAsync(mRemoteAction.getIcon(), mainHandler);
+ button.setEnabled(isCloseAction() || mRemoteAction.isEnabled());
+ button.setIsCustomCloseAction(isCloseAction());
+ }
+
+ PendingIntent getPendingIntent() {
+ return mRemoteAction.getActionIntent();
+ }
+
+ void executeAction() {
+ super.executeAction();
+ try {
+ mRemoteAction.getActionIntent().send();
+ } catch (PendingIntent.CanceledException e) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Failed to send action, %s", TAG, e);
+ }
+ }
+
+ @Override
+ Notification.Action toNotificationAction(Context context) {
+ Notification.Action.Builder builder = new Notification.Action.Builder(
+ mRemoteAction.getIcon(),
+ mRemoteAction.getTitle(),
+ mRemoteAction.getActionIntent());
+ Bundle extras = new Bundle();
+ extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION,
+ mRemoteAction.getContentDescription());
+ extras.putBoolean(TvPipAction.EXTRA_IS_PIP_CUSTOM_ACTION, true);
+ builder.addExtras(extras);
+
+ builder.setSemanticAction(isCloseAction()
+ ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE);
+ builder.setContextual(true);
+ return builder.build();
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
index 1e54436ebce9..a94bd6ec1040 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
@@ -284,8 +284,10 @@ class TvPipKeepClearAlgorithm() {
): Rect? {
val movementBounds = transformedMovementBounds
val candidateEdgeRects = mutableListOf<Rect>()
+ val maxRestrictedXDistanceFraction =
+ if (isPipAnchoredToCorner()) maxRestrictedDistanceFraction else 0.0
val minRestrictedLeft =
- pipAnchorBounds.right - screenSize.width * maxRestrictedDistanceFraction
+ pipAnchorBounds.right - screenSize.width * maxRestrictedXDistanceFraction
candidateEdgeRects.add(
movementBounds.offsetCopy(movementBounds.width() + pipAreaPadding, 0)
@@ -296,7 +298,6 @@ class TvPipKeepClearAlgorithm() {
// throw out edges that are too close to the left screen edge to fit the PiP
val minLeft = movementBounds.left + pipAnchorBounds.width()
candidateEdgeRects.retainAll { it.left - pipAreaPadding > minLeft }
- candidateEdgeRects.sortBy { -it.left }
val maxRestrictedDY = (screenSize.height * maxRestrictedDistanceFraction).roundToInt()
@@ -335,8 +336,7 @@ class TvPipKeepClearAlgorithm() {
}
}
- candidateBounds.sortBy { candidateCost(it, pipAnchorBounds) }
- return candidateBounds.firstOrNull()
+ return candidateBounds.minByOrNull { candidateCost(it, pipAnchorBounds) }
}
private fun getNearbyStashedPosition(bounds: Rect, keepClearAreas: Set<Rect>): Rect {
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 4ce45e142c64..b2a189b45d6c 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
@@ -18,79 +18,83 @@ package com.android.wm.shell.pip.tv;
import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP;
-import android.app.ActivityManager;
+import android.annotation.IntDef;
import android.app.RemoteAction;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Insets;
-import android.graphics.Matrix;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.os.Handler;
-import android.view.LayoutInflater;
import android.view.SurfaceControl;
-import android.view.SyncRtSurfaceTransactionApplier;
import android.view.View;
import android.view.ViewRootImpl;
+import android.view.WindowManager;
import android.view.WindowManagerGlobal;
+import android.window.SurfaceSyncGroup;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
/**
* Manages the visibility of the PiP Menu as user interacts with PiP.
*/
public class TvPipMenuController implements PipMenuController, TvPipMenuView.Listener {
private static final String TAG = "TvPipMenuController";
- private static final boolean DEBUG = TvPipController.DEBUG;
private static final String BACKGROUND_WINDOW_TITLE = "PipBackgroundView";
private final Context mContext;
private final SystemWindows mSystemWindows;
private final TvPipBoundsState mTvPipBoundsState;
private final Handler mMainHandler;
- private final int mPipMenuBorderWidth;
- private final int mPipEduTextShowDurationMs;
- private final int mPipEduTextHeight;
+ private TvPipActionsProvider mTvPipActionsProvider;
private Delegate mDelegate;
private SurfaceControl mLeash;
private TvPipMenuView mPipMenuView;
- private View mPipBackgroundView;
+ private TvPipBackgroundView mPipBackgroundView;
+ private boolean mMenuIsFocused;
- // User can actively move the PiP via the DPAD.
- private boolean mInMoveMode;
- // Used when only showing the move menu since we want to close the menu completely when
- // exiting the move menu instead of showing the regular button menu.
- private boolean mCloseAfterExitMoveMenu;
+ @TvPipMenuMode
+ private int mCurrentMenuMode = MODE_NO_MENU;
+ @TvPipMenuMode
+ private int mPrevMenuMode = MODE_NO_MENU;
- private final List<RemoteAction> mMediaActions = new ArrayList<>();
- private final List<RemoteAction> mAppActions = new ArrayList<>();
- private RemoteAction mCloseAction;
+ @IntDef(prefix = { "MODE_" }, value = {
+ MODE_NO_MENU,
+ MODE_MOVE_MENU,
+ MODE_ALL_ACTIONS_MENU,
+ })
+ public @interface TvPipMenuMode {}
- private SyncRtSurfaceTransactionApplier mApplier;
- private SyncRtSurfaceTransactionApplier mBackgroundApplier;
- RectF mTmpSourceRectF = new RectF();
- RectF mTmpDestinationRectF = new RectF();
- Matrix mMoveTransform = new Matrix();
+ /**
+ * In this mode the PiP menu is not focused and no user controls are displayed.
+ */
+ static final int MODE_NO_MENU = 0;
- private final Runnable mCloseEduTextRunnable = this::closeEduText;
+ /**
+ * In this mode the PiP menu is focused and the user can use the DPAD controls to move the PiP
+ * to a different position on the screen. We draw arrows in all possible movement directions.
+ */
+ static final int MODE_MOVE_MENU = 1;
+
+ /**
+ * In this mode the PiP menu is focused and we display an array of actions that the user can
+ * select. See {@link TvPipActionsProvider} for the types of available actions.
+ */
+ static final int MODE_ALL_ACTIONS_MENU = 2;
public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
- SystemWindows systemWindows, PipMediaController pipMediaController,
- Handler mainHandler) {
+ SystemWindows systemWindows, Handler mainHandler) {
mContext = context;
mTvPipBoundsState = tvPipBoundsState;
mSystemWindows = systemWindows;
@@ -107,22 +111,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver,
new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */,
mainHandler, Context.RECEIVER_EXPORTED);
-
- pipMediaController.addActionListener(this::onMediaActionsChanged);
-
- mPipEduTextShowDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_show_duration_ms);
- mPipEduTextHeight = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
- mPipMenuBorderWidth = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_border_width);
}
void setDelegate(Delegate delegate) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: setDelegate(), delegate=%s", TAG, delegate);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: setDelegate(), delegate=%s", TAG, delegate);
if (mDelegate != null) {
throw new IllegalStateException(
"The delegate has already been set and should not change.");
@@ -134,6 +127,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mDelegate = delegate;
}
+ void setTvPipActionsProvider(TvPipActionsProvider tvPipActionsProvider) {
+ mTvPipActionsProvider = tvPipActionsProvider;
+ }
+
@Override
public void attach(SurfaceControl leash) {
if (mDelegate == null) {
@@ -145,10 +142,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
private void attachPipMenu() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: attachPipMenu()", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: attachPipMenu()", TAG);
if (mPipMenuView != null) {
detachPipMenu();
@@ -157,117 +152,95 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
attachPipBackgroundView();
attachPipMenuView();
- mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth,
- -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth));
- mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight));
- mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs);
+ int pipEduTextHeight = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
+ int pipMenuBorderWidth = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.pip_menu_border_width);
+ mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
+ -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
+ mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
}
private void attachPipMenuView() {
- mPipMenuView = new TvPipMenuView(mContext);
- mPipMenuView.setListener(this);
+ if (mTvPipActionsProvider == null) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Actions provider is not set", TAG);
+ return;
+ }
+ mPipMenuView = createTvPipMenuView();
setUpViewSurfaceZOrder(mPipMenuView, 1);
addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
- maybeUpdateMenuViewActions();
+ }
+
+ @VisibleForTesting
+ TvPipMenuView createTvPipMenuView() {
+ return new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider);
}
private void attachPipBackgroundView() {
- mPipBackgroundView = LayoutInflater.from(mContext)
- .inflate(R.layout.tv_pip_menu_background, null);
+ mPipBackgroundView = createTvPipBackgroundView();
setUpViewSurfaceZOrder(mPipBackgroundView, -1);
addPipMenuViewToSystemWindows(mPipBackgroundView, BACKGROUND_WINDOW_TITLE);
}
+ @VisibleForTesting
+ TvPipBackgroundView createTvPipBackgroundView() {
+ return new TvPipBackgroundView(mContext);
+ }
+
private void setUpViewSurfaceZOrder(View v, int zOrderRelativeToPip) {
v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- v.getViewRootImpl().addSurfaceChangedCallback(
- new PipMenuSurfaceChangedCallback(v, zOrderRelativeToPip));
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- }
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ v.getViewRootImpl().addSurfaceChangedCallback(
+ new PipMenuSurfaceChangedCallback(v, zOrderRelativeToPip));
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ }
});
}
private void addPipMenuViewToSystemWindows(View v, String title) {
- mSystemWindows.addView(v, getPipMenuLayoutParams(title, 0 /* width */, 0 /* height */),
- 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
- }
-
- void notifyPipAnimating(boolean animating) {
- mPipMenuView.setEduTextActive(!animating);
- if (!animating) {
- mPipMenuView.onPipTransitionFinished();
- }
+ final WindowManager.LayoutParams layoutParams =
+ getPipMenuLayoutParams(mContext, title, 0 /* width */, 0 /* height */);
+ layoutParams.alpha = 0f;
+ mSystemWindows.addView(v, layoutParams, 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
+ }
+
+ void onPipTransitionFinished(boolean enterTransition) {
+ // There is a race between when this is called and when the last frame of the pip transition
+ // is drawn. To ensure that view updates are applied only when the animation has fully drawn
+ // and the menu view has been fully remeasured and relaid out, we add a small delay here by
+ // posting on the handler.
+ mMainHandler.post(() -> {
+ if (mPipMenuView != null) {
+ mPipMenuView.onPipTransitionFinished(enterTransition);
+ }
+ });
}
- void showMovementMenuOnly() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: showMovementMenuOnly()", TAG);
- }
- setInMoveMode(true);
- mCloseAfterExitMoveMenu = true;
- showMenuInternal();
+ void showMovementMenu() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: showMovementMenu()", TAG);
+ switchToMenuMode(MODE_MOVE_MENU);
}
@Override
public void showMenu() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG);
- }
- setInMoveMode(false);
- mCloseAfterExitMoveMenu = false;
- showMenuInternal();
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG);
+ switchToMenuMode(MODE_ALL_ACTIONS_MENU, true);
}
- private void showMenuInternal() {
- if (mPipMenuView == null) {
- return;
- }
- maybeCloseEduText();
- maybeUpdateMenuViewActions();
- updateExpansionState();
-
- grantPipMenuFocus(true);
- if (mInMoveMode) {
- mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
- } else {
- mPipMenuView.showButtonsMenu();
- }
- mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
- }
-
- void onPipTransitionStarted(Rect finishBounds) {
+ void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
if (mPipMenuView != null) {
- mPipMenuView.onPipTransitionStarted(finishBounds);
- }
- }
-
- private void maybeCloseEduText() {
- if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) {
- mMainHandler.removeCallbacks(mCloseEduTextRunnable);
- mCloseEduTextRunnable.run();
+ mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
}
}
- private void closeEduText() {
- mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
- mPipMenuView.hideEduText();
- mDelegate.closeEduText();
- }
-
void updateGravity(int gravity) {
- mPipMenuView.showMovementHints(gravity);
- }
-
- void updateExpansionState() {
- mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported()
- && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0);
- mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded());
+ mPipMenuView.setPipGravity(gravity);
}
private Rect calculateMenuSurfaceBounds(Rect pipBounds) {
@@ -275,138 +248,21 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
void closeMenu() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: closeMenu()", TAG);
- }
-
- if (mPipMenuView == null) {
- return;
- }
-
- mPipMenuView.hideAllUserControls();
- grantPipMenuFocus(false);
- mDelegate.onMenuClosed();
- }
-
- boolean isInMoveMode() {
- return mInMoveMode;
- }
-
- private void setInMoveMode(boolean moveMode) {
- if (mInMoveMode == moveMode) {
- return;
- }
-
- mInMoveMode = moveMode;
- if (mDelegate != null) {
- mDelegate.onInMoveModeChanged();
- }
- }
-
- @Override
- public void onEnterMoveMode() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
- mCloseAfterExitMoveMenu);
- }
- setInMoveMode(true);
- mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
- }
-
- @Override
- public boolean onExitMoveMode() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
- mCloseAfterExitMoveMenu);
- }
- if (mCloseAfterExitMoveMenu) {
- setInMoveMode(false);
- mCloseAfterExitMoveMenu = false;
- closeMenu();
- return true;
- }
- if (mInMoveMode) {
- setInMoveMode(false);
- mPipMenuView.showButtonsMenu();
- return true;
- }
- return false;
- }
-
- @Override
- public boolean onPipMovement(int keycode) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipMovement - %b", TAG, mInMoveMode);
- }
- if (mInMoveMode) {
- mDelegate.movePip(keycode);
- }
- return mInMoveMode;
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: closeMenu()", TAG);
+ switchToMenuMode(MODE_NO_MENU);
}
@Override
public void detach() {
closeMenu();
- mMainHandler.removeCallbacks(mCloseEduTextRunnable);
detachPipMenu();
mLeash = null;
}
@Override
public void setAppActions(List<RemoteAction> actions, RemoteAction closeAction) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: setAppActions()", TAG);
- }
- updateAdditionalActionsList(mAppActions, actions, closeAction);
- }
-
- private void onMediaActionsChanged(List<RemoteAction> actions) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onMediaActionsChanged()", TAG);
- }
-
- // Hide disabled actions.
- List<RemoteAction> enabledActions = new ArrayList<>();
- for (RemoteAction remoteAction : actions) {
- if (remoteAction.isEnabled()) {
- enabledActions.add(remoteAction);
- }
- }
- updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction);
- }
-
- private void updateAdditionalActionsList(List<RemoteAction> destination,
- @Nullable List<RemoteAction> source, RemoteAction closeAction) {
- final int number = source != null ? source.size() : 0;
- if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) {
- // Nothing changed.
- return;
- }
-
- mCloseAction = closeAction;
-
- destination.clear();
- if (number > 0) {
- destination.addAll(source);
- }
- maybeUpdateMenuViewActions();
- }
-
- private void maybeUpdateMenuViewActions() {
- if (mPipMenuView == null) {
- return;
- }
- if (!mAppActions.isEmpty()) {
- mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler);
- } else {
- mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler);
- }
+ // NOOP - handled via the TvPipActionsProvider
}
@Override
@@ -419,46 +275,35 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
*/
@Override
public void resizePipMenu(@Nullable SurfaceControl pipLeash,
- @Nullable SurfaceControl.Transaction t,
- Rect destinationBounds) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: resizePipMenu: %s", TAG, destinationBounds.toShortString());
- }
- if (destinationBounds.isEmpty()) {
+ @Nullable SurfaceControl.Transaction pipTx,
+ Rect pipBounds) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: resizePipMenu: %s", TAG, pipBounds.toShortString());
+
+ if (pipBounds.isEmpty()) {
return;
}
-
- if (!maybeCreateSyncApplier()) {
+ if (!isMenuAttached()) {
return;
}
- final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds);
-
final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
- final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams =
- new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface)
- .withWindowCrop(menuBounds)
- .build();
-
final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
- final SyncRtSurfaceTransactionApplier.SurfaceParams backParams =
- new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface)
- .withWindowCrop(menuBounds)
- .build();
-
- // TODO(b/226580399): switch to using SurfaceSyncer (see b/200284684) to synchronize the
- // animations of the pip surface with the content of the front and back menu surfaces
- mBackgroundApplier.scheduleApply(backParams);
- if (pipLeash != null && t != null) {
- final SyncRtSurfaceTransactionApplier.SurfaceParams
- pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
- .withMergeTransaction(t)
- .build();
- mApplier.scheduleApply(frontParams, pipParams);
- } else {
- mApplier.scheduleApply(frontParams);
+ final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds);
+ if (pipTx == null) {
+ pipTx = new SurfaceControl.Transaction();
}
+ pipTx.setWindowCrop(frontSurface, menuBounds.width(), menuBounds.height());
+ pipTx.setWindowCrop(backSurface, menuBounds.width(), menuBounds.height());
+
+ // Synchronize drawing the content in the front and back surfaces together with the pip
+ // transaction and the window crop 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();
}
private SurfaceControl getSurfaceControl(View v) {
@@ -466,127 +311,168 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
@Override
- public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction transaction,
- Rect pipDestBounds) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: movePipMenu: %s", TAG, pipDestBounds.toShortString());
- }
+ public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction pipTx,
+ Rect pipBounds, float alpha) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: movePipMenu: %s, alpha %s", TAG, pipBounds.toShortString(), alpha);
- if (pipDestBounds.isEmpty()) {
- if (transaction == null && DEBUG) {
+ if (pipBounds.isEmpty()) {
+ if (pipTx == null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: no transaction given", TAG);
}
return;
}
- if (!maybeCreateSyncApplier()) {
+ if (!isMenuAttached()) {
return;
}
- final Rect menuDestBounds = calculateMenuSurfaceBounds(pipDestBounds);
- final Rect tmpSourceBounds = new Rect();
- // If there is no pip leash supplied, that means the PiP leash is already finalized
- // resizing and the PiP menu is also resized. We then want to do a scale from the current
- // new menu bounds.
- if (pipLeash != null && transaction != null) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: tmpSourceBounds based on mPipMenuView.getBoundsOnScreen()", TAG);
- }
- mPipMenuView.getBoundsOnScreen(tmpSourceBounds);
- } else {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: tmpSourceBounds based on menu width and height", TAG);
- }
- tmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height());
- }
-
- mTmpSourceRectF.set(tmpSourceBounds);
- mTmpDestinationRectF.set(menuDestBounds);
- mMoveTransform.setTranslate(mTmpDestinationRectF.left, mTmpDestinationRectF.top);
-
final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
- final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams =
- new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface)
- .withMatrix(mMoveTransform)
- .build();
-
final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
- final SyncRtSurfaceTransactionApplier.SurfaceParams backParams =
- new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface)
- .withMatrix(mMoveTransform)
- .build();
-
- // TODO(b/226580399): switch to using SurfaceSyncer (see b/200284684) to synchronize the
- // animations of the pip surface with the content of the front and back menu surfaces
- mBackgroundApplier.scheduleApply(backParams);
- if (pipLeash != null && transaction != null) {
- final SyncRtSurfaceTransactionApplier.SurfaceParams pipParams =
- new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
- .withMergeTransaction(transaction)
- .build();
- mApplier.scheduleApply(frontParams, pipParams);
- } else {
- mApplier.scheduleApply(frontParams);
+ 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);
- updateMenuBounds(pipDestBounds);
+ 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();
}
- private boolean maybeCreateSyncApplier() {
- if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
+ private boolean isMenuAttached() {
+ final boolean ready = mPipMenuView != null && mPipMenuView.getViewRootImpl() != null
+ && mPipBackgroundView != null && mPipBackgroundView.getViewRootImpl() != null;
+ if (!ready) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
- return false;
- }
-
- if (mApplier == null) {
- mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
+ "%s: the menu surfaces are not attached.", TAG);
}
- if (mBackgroundApplier == null) {
- mBackgroundApplier = new SyncRtSurfaceTransactionApplier(mPipBackgroundView);
- }
- return true;
+ return ready;
}
private void detachPipMenu() {
if (mPipMenuView != null) {
- mApplier = null;
mSystemWindows.removeView(mPipMenuView);
mPipMenuView = null;
}
if (mPipBackgroundView != null) {
- mBackgroundApplier = null;
mSystemWindows.removeView(mPipBackgroundView);
mPipBackgroundView = null;
}
}
@Override
- public void updateMenuBounds(Rect destinationBounds) {
- final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds);
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString());
+ public void updateMenuBounds(Rect pipBounds) {
+ if (!isMenuAttached()) {
+ return;
}
+ final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds);
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString());
mSystemWindows.updateViewLayout(mPipBackgroundView,
- getPipMenuLayoutParams(BACKGROUND_WINDOW_TITLE, menuBounds.width(),
+ getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
menuBounds.height()));
mSystemWindows.updateViewLayout(mPipMenuView,
- getPipMenuLayoutParams(MENU_WINDOW_TITLE, menuBounds.width(),
+ getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
menuBounds.height()));
-
if (mPipMenuView != null) {
- mPipMenuView.updateBounds(destinationBounds);
+ mPipMenuView.setPipBounds(pipBounds);
+ }
+ }
+
+ // Start methods handling {@link TvPipMenuMode}
+
+ @VisibleForTesting
+ boolean isMenuOpen() {
+ return mCurrentMenuMode != MODE_NO_MENU;
+ }
+
+ @VisibleForTesting
+ boolean isInMoveMode() {
+ return mCurrentMenuMode == MODE_MOVE_MENU;
+ }
+
+ @VisibleForTesting
+ boolean isInAllActionsMode() {
+ return mCurrentMenuMode == MODE_ALL_ACTIONS_MENU;
+ }
+
+ private void switchToMenuMode(@TvPipMenuMode int menuMode) {
+ switchToMenuMode(menuMode, false);
+ }
+
+ private void switchToMenuMode(@TvPipMenuMode int menuMode, boolean resetMenu) {
+ // Note: we intentionally don't return early here, because the TvPipMenuView needs to
+ // refresh the Ui even if there is no menu mode change.
+ mPrevMenuMode = mCurrentMenuMode;
+ mCurrentMenuMode = menuMode;
+
+ ProtoLog.i(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: switchToMenuMode: setting mCurrentMenuMode=%s, mPrevMenuMode=%s", TAG,
+ getMenuModeString(), getMenuModeString(mPrevMenuMode));
+
+ updateUiOnNewMenuModeRequest(resetMenu);
+ updateDelegateOnNewMenuModeRequest();
+ }
+
+ private void updateUiOnNewMenuModeRequest(boolean resetMenu) {
+ if (mPipMenuView == null || mPipBackgroundView == null) return;
+
+ mPipMenuView.setPipGravity(mTvPipBoundsState.getTvPipGravity());
+ mPipMenuView.transitionToMenuMode(mCurrentMenuMode, resetMenu);
+ mPipBackgroundView.transitionToMenuMode(mCurrentMenuMode);
+ grantPipMenuFocus(mCurrentMenuMode != MODE_NO_MENU);
+ }
+
+ private void updateDelegateOnNewMenuModeRequest() {
+ if (mPrevMenuMode == mCurrentMenuMode) return;
+ if (mDelegate == null) return;
+
+ if (mPrevMenuMode == MODE_MOVE_MENU || isInMoveMode()) {
+ mDelegate.onInMoveModeChanged();
+ }
+
+ if (mCurrentMenuMode == MODE_NO_MENU) {
+ mDelegate.onMenuClosed();
}
}
+ @VisibleForTesting
+ String getMenuModeString() {
+ return getMenuModeString(mCurrentMenuMode);
+ }
+
+ static String getMenuModeString(@TvPipMenuMode int menuMode) {
+ switch(menuMode) {
+ case MODE_NO_MENU:
+ return "MODE_NO_MENU";
+ case MODE_MOVE_MENU:
+ return "MODE_MOVE_MENU";
+ case MODE_ALL_ACTIONS_MENU:
+ return "MODE_ALL_ACTIONS_MENU";
+ default:
+ return "Unknown";
+ }
+ }
+
+ // Start {@link TvPipMenuView.Delegate} methods
+
@Override
- public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onFocusTaskChanged", TAG);
+ public void onCloseEduText() {
+ mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
+ mDelegate.closeEduText();
}
@Override
@@ -597,43 +483,52 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
@Override
- public void onCloseButtonClick() {
- mDelegate.closePip();
+ public boolean onExitMoveMode() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onExitMoveMode - mCurrentMenuMode=%s", TAG, getMenuModeString());
+
+ final int saveMenuMode = mCurrentMenuMode;
+ if (isInMoveMode()) {
+ switchToMenuMode(mPrevMenuMode);
+ }
+ return saveMenuMode == MODE_MOVE_MENU;
}
@Override
- public void onFullscreenButtonClick() {
- mDelegate.movePipToFullscreen();
+ public boolean onPipMovement(int keycode) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString());
+ if (isInMoveMode()) {
+ mDelegate.movePip(keycode);
+ }
+ return isInMoveMode();
}
@Override
- public void onToggleExpandedMode() {
- mDelegate.togglePipExpansion();
+ public void onPipWindowFocusChanged(boolean focused) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onPipWindowFocusChanged - focused=%b", TAG, focused);
+ mMenuIsFocused = focused;
+ if (!focused && isMenuOpen()) {
+ closeMenu();
+ }
}
interface Delegate {
- void movePipToFullscreen();
-
void movePip(int keycode);
void onInMoveModeChanged();
- int getPipGravity();
-
- void togglePipExpansion();
-
void onMenuClosed();
void closeEduText();
-
- void closePip();
}
private void grantPipMenuFocus(boolean grantFocus) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: grantWindowFocus(%b)", TAG, grantFocus);
- }
+ if (mMenuIsFocused == grantFocus) return;
+
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: grantWindowFocus(%b)", TAG, grantFocus);
try {
WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
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
new file mode 100644
index 000000000000..f86f987039ba
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -0,0 +1,303 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER;
+import static android.view.View.GONE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.text.Annotation;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+
+import java.util.Arrays;
+
+/**
+ * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu.
+ * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same
+ * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might
+ * not be enough space to fit the whole educational text in the available space. In such cases we
+ * apply a marquee animation to the TextView inside the drawer.
+ *
+ * The drawer is shown temporarily giving the user enough time to read it, after which it slides
+ * shut. We show the text for a duration calculated based on whether the text is marqueed or not.
+ */
+class TvPipMenuEduTextDrawer extends FrameLayout {
+ private static final String TAG = "TvPipMenuEduTextDrawer";
+
+ private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND
+ private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY
+ private final float mMarqueeAnimSpeed; // pixels per ms
+
+ private final Runnable mCloseDrawerRunnable = this::closeDrawer;
+ private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText;
+
+ private final Handler mMainHandler;
+ private final Listener mListener;
+ private final TextView mEduTextView;
+
+ TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) {
+ super(context, null, 0, 0);
+
+ mListener = listener;
+ mMainHandler = mainHandler;
+
+ // Taken from TextView.Marquee calculation
+ mMarqueeAnimSpeed =
+ (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f;
+
+ mEduTextView = new TextView(mContext);
+ setupDrawer();
+ }
+
+ private void setupDrawer() {
+ final int eduTextHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.pip_menu_edu_text_view_height);
+ final int marqueeRepeatLimit = mContext.getResources()
+ .getInteger(R.integer.pip_edu_text_scroll_times);
+
+ mEduTextView.setLayoutParams(
+ new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER));
+ mEduTextView.setGravity(CENTER);
+ mEduTextView.setClickable(false);
+ mEduTextView.setText(createEduTextString());
+ mEduTextView.setSingleLine();
+ mEduTextView.setTextAppearance(R.style.TvPipEduText);
+ mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit);
+ mEduTextView.setHorizontallyScrolling(true);
+ mEduTextView.setHorizontalFadingEdgeEnabled(true);
+ mEduTextView.setSelected(false);
+ addView(mEduTextView);
+
+ setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER));
+ setClipChildren(true);
+ }
+
+ /**
+ * Initializes the edu text. Should only be called once when the PiP is entered
+ */
+ void init() {
+ ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG);
+ scheduleLifecycleEvents();
+ }
+
+ int getEduTextDrawerHeight() {
+ return getVisibility() == GONE ? 0 : getHeight();
+ }
+
+ private void scheduleLifecycleEvents() {
+ final int startScrollDelay = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_start_scroll_delay);
+ if (isEduTextMarqueed()) {
+ mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay);
+ }
+ mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration());
+ mEduTextView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ }
+
+ @Override
+ public void onWindowDetached() {
+ mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this);
+ mMainHandler.removeCallbacks(mStartScrollEduTextRunnable);
+ mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+ }
+ });
+ }
+
+ private int getEduTextShowDuration() {
+ int eduTextShowDuration;
+ if (isEduTextMarqueed()) {
+ // Calculate the time it takes to fully scroll the text once: time = distance / speed
+ final float singleMarqueeDuration =
+ getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed;
+ // The TextView adds a delay between each marquee repetition. Take that into account
+ final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY;
+ // Finally, multiply by the number of times we repeat the marquee animation
+ eduTextShowDuration =
+ (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit();
+ } else {
+ eduTextShowDuration = mContext.getResources()
+ .getInteger(R.integer.pip_edu_text_non_scroll_show_duration);
+ }
+
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d",
+ TAG, eduTextShowDuration);
+ return eduTextShowDuration;
+ }
+
+ /**
+ * Returns true if the edu text width is bigger than the width of the text view, which indicates
+ * that the edu text will be marqueed
+ */
+ private boolean isEduTextMarqueed() {
+ final int availableWidth = (int) mEduTextView.getWidth()
+ - mEduTextView.getCompoundPaddingLeft()
+ - mEduTextView.getCompoundPaddingRight();
+ return availableWidth < getEduTextWidth();
+ }
+
+ /**
+ * Returns the width of a single marquee repetition of the edu text in pixels.
+ * This is the width from the start of the edu text to the start of the next edu
+ * text when it is marqueed.
+ *
+ * This is calculated based on the TextView.Marquee#start calculations
+ */
+ private float getMarqueeAnimEduTextLineWidth() {
+ // When the TextView has a marquee animation, it puts a gap between the text end and the
+ // start of the next edu text repetition. The space is equal to a third of the TextView
+ // width
+ final float gap = mEduTextView.getWidth() / 3.0f;
+ return getEduTextWidth() + gap;
+ }
+
+ private void startScrollEduText() {
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d",
+ TAG, mEduTextView.getMarqueeRepeatLimit());
+ mEduTextView.setSelected(true);
+ }
+
+ /**
+ * Returns the width of the edu text irrespective of the TextView width
+ */
+ private int getEduTextWidth() {
+ return (int) mEduTextView.getLayout().getLineWidth(0);
+ }
+
+ /**
+ * Closes the edu text drawer if it hasn't been closed yet
+ */
+ void closeIfNeeded() {
+ if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) {
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close(), closing the edu text drawer because of user action", TAG);
+ mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+ mCloseDrawerRunnable.run();
+ } else {
+ // Do nothing, the drawer has already been closed
+ }
+ }
+
+ private void closeDrawer() {
+ ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG);
+ final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_view_exit_animation_duration);
+ final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_window_exit_animation_duration);
+
+ // Start fading out the edu text
+ mEduTextView.animate()
+ .alpha(0f)
+ .setInterpolator(TvPipInterpolators.EXIT)
+ .setDuration(eduTextFadeExitAnimationDuration)
+ .start();
+
+ // Start animation to close the drawer by animating its height to 0
+ final ValueAnimator heightAnimator = ValueAnimator.ofInt(getHeight(), 0);
+ heightAnimator.setDuration(eduTextSlideExitAnimationDuration);
+ heightAnimator.setInterpolator(TvPipInterpolators.BROWSE);
+ heightAnimator.addUpdateListener(animator -> {
+ final ViewGroup.LayoutParams params = getLayoutParams();
+ params.height = (int) animator.getAnimatedValue();
+ setLayoutParams(params);
+ });
+ heightAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(@NonNull Animator animator) {
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animator) {
+ onCloseEduTextAnimationEnd();
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animator) {
+ onCloseEduTextAnimationEnd();
+ }
+
+ @Override
+ public void onAnimationRepeat(@NonNull Animator animator) {
+ }
+ });
+ heightAnimator.start();
+
+ mListener.onCloseEduTextAnimationStart();
+ }
+
+ public void onCloseEduTextAnimationEnd() {
+ setVisibility(GONE);
+ mListener.onCloseEduTextAnimationEnd();
+ }
+
+ /**
+ * Creates the educational text that will be displayed to the user. Here we replace the
+ * HOME annotation in the String with an icon
+ */
+ private CharSequence createEduTextString() {
+ final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
+ final SpannableString spannableString = new SpannableString(eduText);
+ Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
+ .ifPresent(annotation -> {
+ final Drawable icon =
+ getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
+ if (icon != null) {
+ icon.mutate();
+ icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+ spannableString.setSpan(new CenteredImageSpan(icon),
+ eduText.getSpanStart(annotation),
+ eduText.getSpanEnd(annotation),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ });
+
+ return spannableString;
+ }
+
+ /**
+ * A listener for edu text drawer event states.
+ */
+ interface Listener {
+ void onCloseEduTextAnimationStart();
+ void onCloseEduTextAnimationEnd();
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 320c05c4a415..613791ccc062 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -25,205 +25,197 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
import static android.view.KeyEvent.KEYCODE_ENTER;
-import android.animation.ValueAnimator;
-import android.app.PendingIntent;
-import android.app.RemoteAction;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE;
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU;
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU;
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU;
+
import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
import android.os.Handler;
-import android.text.Annotation;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannedString;
-import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
-import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewRootImpl;
+import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
-import android.widget.HorizontalScrollView;
import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.widget.LinearLayoutManager;
+import com.android.internal.widget.RecyclerView;
import com.android.wm.shell.R;
+import com.android.wm.shell.common.TvWindowMenuActionButton;
import com.android.wm.shell.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
- * A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu
- * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set
- * via a {@link #setAdditionalActions(List, Handler)} call.
+ * A View that represents Pip Menu on TV. It's responsible for displaying the Pip menu actions from
+ * the TvPipActionsProvider as well as the buttons for manually moving the PiP.
*/
-public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
+public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.Listener,
+ TvPipMenuEduTextDrawer.Listener {
private static final String TAG = "TvPipMenuView";
- private static final boolean DEBUG = TvPipController.DEBUG;
- private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
+ private final TvPipMenuView.Listener mListener;
- @Nullable
- private Listener mListener;
+ private final TvPipActionsProvider mTvPipActionsProvider;
+
+ private final RecyclerView mActionButtonsRecyclerView;
+ private final LinearLayoutManager mButtonLayoutManager;
+ private final RecyclerViewAdapter mRecyclerViewAdapter;
- private final LinearLayout mActionButtonsContainer;
- private final View mMenuFrameView;
- private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>();
private final View mPipFrameView;
+ private final View mMenuFrameView;
private final View mPipView;
- private final TextView mEduTextView;
- private final View mEduTextContainerView;
- private final int mPipMenuOuterSpace;
- private final int mPipMenuBorderWidth;
- private final int mEduTextFadeExitAnimationDurationMs;
- private final int mEduTextSlideExitAnimationDurationMs;
- private int mEduTextHeight;
-
- private final ImageView mArrowUp;
- private final ImageView mArrowRight;
- private final ImageView mArrowDown;
- private final ImageView mArrowLeft;
-
- private final ScrollView mScrollView;
- private final HorizontalScrollView mHorizontalScrollView;
- private View mFocusedButton;
- private Rect mCurrentPipBounds;
- private boolean mMoveMenuIsVisible;
- private boolean mButtonMenuIsVisible;
+ private final View mPipBackground;
+ private final View mDimLayer;
- private final TvPipMenuActionButton mExpandButton;
- private final TvPipMenuActionButton mCloseButton;
+ private final TvPipMenuEduTextDrawer mEduTextDrawer;
+ private final ViewGroup mEduTextContainer;
- private boolean mSwitchingOrientation;
+ private final int mPipMenuOuterSpace;
+ private final int mPipMenuBorderWidth;
private final int mPipMenuFadeAnimationDuration;
private final int mResizeAnimationDuration;
- public TvPipMenuView(@NonNull Context context) {
- this(context, null);
- }
+ private final ImageView mArrowUp;
+ private final ImageView mArrowRight;
+ private final ImageView mArrowDown;
+ private final ImageView mArrowLeft;
+ private final TvWindowMenuActionButton mA11yDoneButton;
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
+ private final int mArrowElevation;
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
+ private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU;
+ private final Rect mCurrentPipBounds = new Rect();
+ private int mCurrentPipGravity;
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
+ private final AccessibilityManager mA11yManager;
+ private final Handler mMainHandler;
+ public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
+ @NonNull Listener listener, TvPipActionsProvider tvPipActionsProvider) {
+ super(context, null, 0, 0);
inflate(context, R.layout.tv_pip_menu, this);
- mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
- mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button)
- .setOnClickListener(this);
+ mMainHandler = mainHandler;
+ mListener = listener;
+ mA11yManager = context.getSystemService(AccessibilityManager.class);
- mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button);
- mCloseButton.setOnClickListener(this);
- mCloseButton.setIsCustomCloseAction(true);
+ mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons);
+ mButtonLayoutManager = new LinearLayoutManager(mContext);
+ mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager);
+ mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true);
- mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button)
- .setOnClickListener(this);
- mExpandButton = findViewById(R.id.tv_pip_menu_expand_button);
- mExpandButton.setOnClickListener(this);
+ mTvPipActionsProvider = tvPipActionsProvider;
+ mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList());
+ mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter);
- mScrollView = findViewById(R.id.tv_pip_menu_scroll);
- mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll);
+ tvPipActionsProvider.addListener(this);
mMenuFrameView = findViewById(R.id.tv_pip_menu_frame);
mPipFrameView = findViewById(R.id.tv_pip_border);
mPipView = findViewById(R.id.tv_pip);
+ mPipBackground = findViewById(R.id.tv_pip_menu_background);
+ mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer);
+
mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up);
mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right);
mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down);
mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
+ mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button);
+
+ final Resources res = context.getResources();
+ mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
+ mPipMenuFadeAnimationDuration =
+ res.getInteger(R.integer.tv_window_menu_fade_animation_duration);
+ mPipMenuOuterSpace = res.getDimensionPixelSize(R.dimen.pip_menu_outer_space);
+ mPipMenuBorderWidth = res.getDimensionPixelSize(R.dimen.pip_menu_border_width);
+ mArrowElevation = res.getDimensionPixelSize(R.dimen.pip_menu_arrow_elevation);
- mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
- mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
-
- mResizeAnimationDuration = context.getResources().getInteger(
- R.integer.config_pipResizeAnimationDuration);
- mPipMenuFadeAnimationDuration = context.getResources()
- .getInteger(R.integer.pip_menu_fade_animation_duration);
-
- mPipMenuOuterSpace = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_outer_space);
- mPipMenuBorderWidth = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_border_width);
- mEduTextHeight = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
- mEduTextFadeExitAnimationDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms);
- mEduTextSlideExitAnimationDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
-
- initEduText();
+ initMoveArrows();
+
+ mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, this);
+ mEduTextContainer = (ViewGroup) findViewById(R.id.tv_pip_menu_edu_text_container);
+ mEduTextContainer.addView(mEduTextDrawer);
}
- void initEduText() {
- final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
- final SpannableString spannableString = new SpannableString(eduText);
- Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
- .ifPresent(annotation -> {
- final Drawable icon =
- getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
- if (icon != null) {
- icon.mutate();
- icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
- spannableString.setSpan(new CenteredImageSpan(icon),
- eduText.getSpanStart(annotation),
- eduText.getSpanEnd(annotation),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- });
+ private void initMoveArrows() {
+ final int arrowSize =
+ mContext.getResources().getDimensionPixelSize(R.dimen.pip_menu_arrow_size);
+ final Path arrowPath = createArrowPath(arrowSize);
+
+ final ShapeDrawable arrowDrawable = new ShapeDrawable();
+ arrowDrawable.setShape(new PathShape(arrowPath, arrowSize, arrowSize));
+ arrowDrawable.setTint(mContext.getResources().getColor(R.color.tv_pip_menu_arrow_color));
+
+ final ViewOutlineProvider arrowOutlineProvider = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setPath(createArrowPath(view.getMeasuredHeight()));
+ }
+ };
- mEduTextView.setText(spannableString);
+ initArrow(mArrowRight, arrowOutlineProvider, arrowDrawable, 0);
+ initArrow(mArrowDown, arrowOutlineProvider, arrowDrawable, 90);
+ initArrow(mArrowLeft, arrowOutlineProvider, arrowDrawable, 180);
+ initArrow(mArrowUp, arrowOutlineProvider, arrowDrawable, 270);
}
- void setEduTextActive(boolean active) {
- mEduTextView.setSelected(active);
+ /**
+ * Creates a Path for a movement arrow in the MODE_MOVE_MENU. The resulting Path is a simple
+ * right-pointing triangle with its tip in the center of a size x size square:
+ * _ _ _ _ _
+ * |* |
+ * |* * |
+ * |* * |
+ * |* * |
+ * |* _ _ _ _|
+ *
+ */
+ private Path createArrowPath(int size) {
+ final Path triangle = new Path();
+ triangle.lineTo(0, size);
+ triangle.lineTo(size / 2, size / 2);
+ triangle.close();
+ return triangle;
}
- void hideEduText() {
- final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0);
- heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs);
- heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
- heightAnimation.addUpdateListener(animator -> {
- mEduTextHeight = (int) animator.getAnimatedValue();
- });
- mEduTextView.animate()
- .alpha(0f)
- .setInterpolator(TvPipInterpolators.EXIT)
- .setDuration(mEduTextFadeExitAnimationDurationMs)
- .withEndAction(() -> {
- mEduTextContainerView.setVisibility(GONE);
- }).start();
- heightAnimation.start();
+ private void initArrow(View v, ViewOutlineProvider arrowOutlineProvider, Drawable arrowDrawable,
+ int rotation) {
+ v.setOutlineProvider(arrowOutlineProvider);
+ v.setBackground(arrowDrawable);
+ v.setRotation(rotation);
+ v.setElevation(mArrowElevation);
}
- void onPipTransitionStarted(Rect finishBounds) {
+ void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
+ if (targetBounds == null) {
+ return;
+ }
+
// Fade out content by fading in view on top.
- if (mCurrentPipBounds != null && finishBounds != null) {
+ if (mCurrentPipBounds != null) {
boolean ratioChanged = PipUtils.aspectRatioChanged(
mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
- finishBounds.width() / (float) finishBounds.height());
+ targetBounds.width() / (float) targetBounds.height());
if (ratioChanged) {
- mPipView.animate()
+ mPipBackground.animate()
.alpha(1f)
.setInterpolator(TvPipInterpolators.EXIT)
.setDuration(mResizeAnimationDuration / 2)
@@ -232,172 +224,69 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
}
// Update buttons.
- final boolean vertical = finishBounds.height() > finishBounds.width();
+ final boolean vertical = targetBounds.height() > targetBounds.width();
final boolean orientationChanged =
- vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
+ vertical != (mButtonLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged);
+ "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
+ TAG, orientationChanged);
if (!orientationChanged) {
return;
}
- if (mButtonMenuIsVisible) {
- mSwitchingOrientation = true;
- mActionButtonsContainer.animate()
+ if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) {
+ // Fade out while orientation change is ongoing and fade back in once transition is
+ // finished.
+ mActionButtonsRecyclerView.animate()
.alpha(0)
.setInterpolator(TvPipInterpolators.EXIT)
- .setDuration(mResizeAnimationDuration / 2)
- .withEndAction(() -> {
- changeButtonScrollOrientation(finishBounds);
- updateButtonGravity(finishBounds);
- // Only make buttons visible again in onPipTransitionFinished to keep in
- // sync with PiP content alpha animation.
- });
+ .setDuration(mResizeAnimationDuration / 2);
} else {
- changeButtonScrollOrientation(finishBounds);
- updateButtonGravity(finishBounds);
+ mButtonLayoutManager.setOrientation(vertical
+ ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
}
}
- void onPipTransitionFinished() {
+ void onPipTransitionFinished(boolean enterTransition) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransitionFinished()", TAG);
- // Fade in content by fading out view on top.
- mPipView.animate()
+ // Fade in content by fading out view on top (faded out at every aspect ratio change).
+ mPipBackground.animate()
.alpha(0f)
.setDuration(mResizeAnimationDuration / 2)
.setInterpolator(TvPipInterpolators.ENTER)
.start();
- // Update buttons.
- if (mSwitchingOrientation) {
- mActionButtonsContainer.animate()
+ if (enterTransition) {
+ mEduTextDrawer.init();
+ }
+
+ mButtonLayoutManager.setOrientation(
+ mCurrentPipBounds.height() > mCurrentPipBounds.width()
+ ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
+ if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU
+ && mActionButtonsRecyclerView.getAlpha() != 1f) {
+ mActionButtonsRecyclerView.animate()
.alpha(1)
.setInterpolator(TvPipInterpolators.ENTER)
.setDuration(mResizeAnimationDuration / 2);
- } else {
- refocusPreviousButton();
}
- mSwitchingOrientation = false;
}
/**
* Also updates the button gravity.
*/
- void updateBounds(Rect updatedBounds) {
+ void setPipBounds(Rect updatedPipBounds) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(),
- updatedBounds.height());
- mCurrentPipBounds = updatedBounds;
- if (!mSwitchingOrientation) {
- updateButtonGravity(mCurrentPipBounds);
- }
+ "%s: updateLayout, width: %s, height: %s", TAG, updatedPipBounds.width(),
+ updatedPipBounds.height());
+ if (updatedPipBounds.equals(mCurrentPipBounds)) return;
+ mCurrentPipBounds.set(updatedPipBounds);
updatePipFrameBounds();
}
- private void changeButtonScrollOrientation(Rect bounds) {
- final boolean vertical = bounds.height() > bounds.width();
-
- final ViewGroup oldScrollView = vertical ? mHorizontalScrollView : mScrollView;
- final ViewGroup newScrollView = vertical ? mScrollView : mHorizontalScrollView;
-
- if (oldScrollView.getChildCount() == 1) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: orientation changed", TAG);
- oldScrollView.removeView(mActionButtonsContainer);
- oldScrollView.setVisibility(GONE);
- mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL
- : LinearLayout.HORIZONTAL);
- newScrollView.addView(mActionButtonsContainer);
- newScrollView.setVisibility(VISIBLE);
- if (mFocusedButton != null) {
- mFocusedButton.requestFocus();
- }
- }
- }
-
- /**
- * Change button gravity based on new dimensions
- */
- private void updateButtonGravity(Rect bounds) {
- final boolean vertical = bounds.height() > bounds.width();
- // Use Math.max since the possible orientation change might not have been applied yet.
- final int buttonsSize = Math.max(mActionButtonsContainer.getHeight(),
- mActionButtonsContainer.getWidth());
-
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: buttons container width: %s, height: %s", TAG,
- mActionButtonsContainer.getWidth(), mActionButtonsContainer.getHeight());
-
- final boolean buttonsFit =
- vertical ? buttonsSize < bounds.height()
- : buttonsSize < bounds.width();
- final int buttonGravity = buttonsFit ? Gravity.CENTER
- : (vertical ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
-
- final LayoutParams params = (LayoutParams) mActionButtonsContainer.getLayoutParams();
- params.gravity = buttonGravity;
- mActionButtonsContainer.setLayoutParams(params);
-
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: vertical: %b, buttonsFit: %b, gravity: %s", TAG, vertical, buttonsFit,
- Gravity.toString(buttonGravity));
- }
-
- private void refocusPreviousButton() {
- if (mMoveMenuIsVisible || mCurrentPipBounds == null || mFocusedButton == null) {
- return;
- }
- final boolean vertical = mCurrentPipBounds.height() > mCurrentPipBounds.width();
-
- if (!mFocusedButton.hasFocus()) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: request focus from: %s", TAG, mFocusedButton);
- mFocusedButton.requestFocus();
- } else {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: already focused: %s", TAG, mFocusedButton);
- }
-
- // Do we need to scroll?
- final Rect buttonBounds = new Rect();
- final Rect scrollBounds = new Rect();
- if (vertical) {
- mScrollView.getDrawingRect(scrollBounds);
- } else {
- mHorizontalScrollView.getDrawingRect(scrollBounds);
- }
- mFocusedButton.getHitRect(buttonBounds);
-
- if (scrollBounds.contains(buttonBounds)) {
- // Button is already completely visible, don't scroll
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: not scrolling", TAG);
- return;
- }
-
- // Scrolling so the button is visible to the user.
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: scrolling to focused button", TAG);
-
- if (vertical) {
- mScrollView.smoothScrollTo((int) mFocusedButton.getX(),
- (int) mFocusedButton.getY());
- } else {
- mHorizontalScrollView.smoothScrollTo((int) mFocusedButton.getX(),
- (int) mFocusedButton.getY());
- }
- }
-
- Rect getPipMenuContainerBounds(Rect pipBounds) {
- final Rect menuUiBounds = new Rect(pipBounds);
- menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
- menuUiBounds.bottom += mEduTextHeight;
- return menuUiBounds;
- }
-
/**
* Update mPipFrameView's bounds according to the new pip window bounds. We can't
* make mPipFrameView match_parent, because the pip menu might contain other content around
@@ -406,11 +295,13 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
* pip menu when it gains focus.
*/
private void updatePipFrameBounds() {
- final ViewGroup.LayoutParams pipFrameParams = mPipFrameView.getLayoutParams();
- if (pipFrameParams != null) {
- pipFrameParams.width = mCurrentPipBounds.width() + 2 * mPipMenuBorderWidth;
- pipFrameParams.height = mCurrentPipBounds.height() + 2 * mPipMenuBorderWidth;
- mPipFrameView.setLayoutParams(pipFrameParams);
+ if (mPipFrameView.getVisibility() == VISIBLE) {
+ final ViewGroup.LayoutParams pipFrameParams = mPipFrameView.getLayoutParams();
+ if (pipFrameParams != null) {
+ pipFrameParams.width = mCurrentPipBounds.width() + 2 * mPipMenuBorderWidth;
+ pipFrameParams.height = mCurrentPipBounds.height() + 2 * mPipMenuBorderWidth;
+ mPipFrameView.setLayoutParams(pipFrameParams);
+ }
}
final ViewGroup.LayoutParams pipViewParams = mPipView.getLayoutParams();
@@ -420,86 +311,130 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
mPipView.setLayoutParams(pipViewParams);
}
-
- }
-
- void setListener(@Nullable Listener listener) {
- mListener = listener;
+ // Keep focused button within the visible area while the PiP is changing size. Otherwise,
+ // the button would lose focus which would cause a need for scrolling and re-focusing after
+ // the animation finishes, which does not look good.
+ View focusedChild = mActionButtonsRecyclerView.getFocusedChild();
+ if (focusedChild != null) {
+ mActionButtonsRecyclerView.scrollToPosition(
+ mActionButtonsRecyclerView.getChildLayoutPosition(focusedChild));
+ }
}
- void setExpandedModeEnabled(boolean enabled) {
- mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
+ Rect getPipMenuContainerBounds(Rect pipBounds) {
+ final Rect menuUiBounds = new Rect(pipBounds);
+ menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
+ menuUiBounds.bottom += mEduTextDrawer.getEduTextDrawerHeight();
+ return menuUiBounds;
}
- void setIsExpanded(boolean expanded) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: setIsExpanded, expanded: %b", TAG, expanded);
+ void transitionToMenuMode(int menuMode, boolean resetMenu) {
+ switch (menuMode) {
+ case MODE_NO_MENU:
+ hideAllUserControls();
+ break;
+ case MODE_MOVE_MENU:
+ showMoveMenu();
+ break;
+ case MODE_ALL_ACTIONS_MENU:
+ showAllActionsMenu(resetMenu);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown TV PiP menu mode: "
+ + TvPipMenuController.getMenuModeString(mCurrentMenuMode));
}
- mExpandButton.setImageResource(
- expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand);
- mExpandButton.setTextAndDescription(
- expanded ? R.string.pip_collapse : R.string.pip_expand);
+
+ mCurrentMenuMode = menuMode;
}
- /**
- * @param gravity for the arrow hints
- */
- void showMoveMenu(int gravity) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
- }
- mButtonMenuIsVisible = false;
- mMoveMenuIsVisible = true;
- showButtonsMenu(false);
- showMovementHints(gravity);
+ private void showMoveMenu() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
+
+ if (mCurrentMenuMode == MODE_MOVE_MENU) return;
+
+ showMovementHints();
+ setMenuButtonsVisible(false);
setFrameHighlighted(true);
+
+ animateAlphaTo(mA11yManager.isEnabled() ? 1f : 0f, mDimLayer);
+
+ mEduTextDrawer.closeIfNeeded();
}
- void showButtonsMenu() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: showButtonsMenu()", TAG);
+ private void showAllActionsMenu(boolean resetMenu) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: showAllActionsMenu(), resetMenu %b", TAG, resetMenu);
+
+ if (resetMenu) {
+ scrollToFirstAction();
}
- mButtonMenuIsVisible = true;
- mMoveMenuIsVisible = false;
- showButtonsMenu(true);
+ if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) return;
+
+ setMenuButtonsVisible(true);
hideMovementHints();
setFrameHighlighted(true);
+ animateAlphaTo(1f, mDimLayer);
+ mEduTextDrawer.closeIfNeeded();
- // Always focus on the first button when opening the menu, except directly after moving.
- if (mFocusedButton == null) {
- // Focus on first button (there is a Space at position 0)
- mFocusedButton = mActionButtonsContainer.getChildAt(1);
- // Reset scroll position.
- mScrollView.scrollTo(0, 0);
- mHorizontalScrollView.scrollTo(
- isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0);
+ if (mCurrentMenuMode == MODE_MOVE_MENU) {
+ refocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE));
}
- refocusPreviousButton();
+
+ }
+
+ private void scrollToFirstAction() {
+ // Clearing the focus here is necessary to allow a smooth scroll even if the first action
+ // is currently not visible.
+ final View focusedChild = mActionButtonsRecyclerView.getFocusedChild();
+ if (focusedChild != null) {
+ focusedChild.clearFocus();
+ }
+
+ mButtonLayoutManager.scrollToPosition(0);
+ mActionButtonsRecyclerView.post(() -> refocusButton(0));
}
/**
- * Hides all menu views, including the menu frame.
+ * @return true if focus was requested, false if focus request could not be carried out due to
+ * the view for the position not being available (scrolling beforehand will be necessary).
*/
- void hideAllUserControls() {
+ private boolean refocusButton(int position) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: refocusButton, position: %d", TAG, position);
+
+ View itemToFocus = mButtonLayoutManager.findViewByPosition(position);
+ if (itemToFocus != null) {
+ itemToFocus.requestFocus();
+ itemToFocus.requestAccessibilityFocus();
+ }
+ return itemToFocus != null;
+ }
+
+ private void hideAllUserControls() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideAllUserControls()", TAG);
- mFocusedButton = null;
- mButtonMenuIsVisible = false;
- mMoveMenuIsVisible = false;
- showButtonsMenu(false);
+
+ if (mCurrentMenuMode == MODE_NO_MENU) return;
+
+ setMenuButtonsVisible(false);
hideMovementHints();
setFrameHighlighted(false);
+ animateAlphaTo(0f, mDimLayer);
+ }
+
+ void setPipGravity(int gravity) {
+ mCurrentPipGravity = gravity;
+ if (mCurrentMenuMode == MODE_MOVE_MENU) {
+ showMovementHints();
+ }
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
- if (!hasWindowFocus) {
- hideAllUserControls();
- }
+ mListener.onPipWindowFocusChanged(hasWindowFocus);
}
private void animateAlphaTo(float alpha, View view) {
@@ -522,143 +457,41 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
});
}
- /**
- * Button order:
- * - Fullscreen
- * - Close
- * - Custom actions (app or media actions)
- * - System actions
- */
- void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction,
- Handler mainHandler) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: setAdditionalActions()", TAG);
- }
-
- // Replace system close action with custom close action if available
- if (closeAction != null) {
- setActionForButton(closeAction, mCloseButton, mainHandler);
- } else {
- mCloseButton.setTextAndDescription(R.string.pip_close);
- mCloseButton.setImageResource(R.drawable.pip_ic_close_white);
- }
- mCloseButton.setIsCustomCloseAction(closeAction != null);
- // Make sure the close action is always enabled
- mCloseButton.setEnabled(true);
-
- // Make sure we exactly as many additional buttons as we have actions to display.
- final int actionsNumber = actions.size();
- int buttonsNumber = mAdditionalButtons.size();
- if (actionsNumber > buttonsNumber) {
- // Add buttons until we have enough to display all the actions.
- while (actionsNumber > buttonsNumber) {
- TvPipMenuActionButton button = new TvPipMenuActionButton(mContext);
- button.setOnClickListener(this);
-
- mActionButtonsContainer.addView(button,
- FIRST_CUSTOM_ACTION_POSITION + buttonsNumber);
- mAdditionalButtons.add(button);
-
- buttonsNumber++;
- }
- } else if (actionsNumber < buttonsNumber) {
- // Hide buttons until we as many as the actions.
- while (actionsNumber < buttonsNumber) {
- final View button = mAdditionalButtons.get(buttonsNumber - 1);
- button.setVisibility(View.GONE);
- button.setTag(null);
-
- buttonsNumber--;
- }
- }
-
- // "Assign" actions to the buttons.
- for (int index = 0; index < actionsNumber; index++) {
- final RemoteAction action = actions.get(index);
- final TvPipMenuActionButton button = mAdditionalButtons.get(index);
-
- // Remove action if it matches the custom close action.
- if (PipUtils.remoteActionsMatch(action, closeAction)) {
- button.setVisibility(GONE);
- continue;
- }
- setActionForButton(action, button, mainHandler);
- }
-
- if (mCurrentPipBounds != null) {
- updateButtonGravity(mCurrentPipBounds);
- refocusPreviousButton();
- }
- }
-
- private void setActionForButton(RemoteAction action, TvPipMenuActionButton button,
- Handler mainHandler) {
- button.setVisibility(View.VISIBLE); // Ensure the button is visible.
- if (action.getContentDescription().length() > 0) {
- button.setTextAndDescription(action.getContentDescription());
- } else {
- button.setTextAndDescription(action.getTitle());
+ @Override
+ public void onActionsChanged(int added, int updated, int startIndex) {
+ mRecyclerViewAdapter.notifyItemRangeChanged(startIndex, updated);
+ if (added > 0) {
+ mRecyclerViewAdapter.notifyItemRangeInserted(startIndex + updated, added);
+ } else if (added < 0) {
+ mRecyclerViewAdapter.notifyItemRangeRemoved(startIndex + updated, -added);
}
- button.setEnabled(action.isEnabled());
- button.setTag(action);
- action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler);
}
- @Nullable
- SurfaceControl getWindowSurfaceControl() {
- final ViewRootImpl root = getViewRootImpl();
- if (root == null) {
- return null;
- }
- final SurfaceControl out = root.getSurfaceControl();
- if (out != null && out.isValid()) {
- return out;
- }
- return null;
+ @Override
+ public void onCloseEduTextAnimationStart() {
+ mListener.onCloseEduText();
}
@Override
- public void onClick(View v) {
- if (mListener == null) return;
-
- final int id = v.getId();
- if (id == R.id.tv_pip_menu_fullscreen_button) {
- mListener.onFullscreenButtonClick();
- } else if (id == R.id.tv_pip_menu_move_button) {
- mListener.onEnterMoveMode();
- } else if (id == R.id.tv_pip_menu_close_button) {
- mListener.onCloseButtonClick();
- } else if (id == R.id.tv_pip_menu_expand_button) {
- mListener.onToggleExpandedMode();
- } else {
- // This should be an "additional action"
- final RemoteAction action = (RemoteAction) v.getTag();
- if (action != null) {
- try {
- action.getActionIntent().send();
- } catch (PendingIntent.CanceledException e) {
- ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Failed to send action, %s", TAG, e);
- }
- } else {
- ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: RemoteAction is null", TAG);
- }
- }
+ public void onCloseEduTextAnimationEnd() {
+ mPipFrameView.setVisibility(GONE);
+ mEduTextContainer.setVisibility(GONE);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
- if (mListener != null && event.getAction() == ACTION_UP) {
- if (!mMoveMenuIsVisible) {
- mFocusedButton = mActionButtonsContainer.getFocusedChild();
+ if (event.getAction() == ACTION_UP) {
+
+ if (event.getKeyCode() == KEYCODE_BACK) {
+ mListener.onBackPress();
+ return true;
+ }
+
+ if (mA11yManager.isEnabled()) {
+ return super.dispatchKeyEvent(event);
}
switch (event.getKeyCode()) {
- case KEYCODE_BACK:
- mListener.onBackPress();
- return true;
case KEYCODE_DPAD_UP:
case KEYCODE_DPAD_DOWN:
case KEYCODE_DPAD_LEFT:
@@ -678,16 +511,38 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
/**
* Shows user hints for moving the PiP, e.g. arrows.
*/
- public void showMovementHints(int gravity) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
+ public void showMovementHints() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: showMovementHints(), position: %s", TAG, Gravity.toString(mCurrentPipGravity));
+ animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
+ animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
+ animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
+ animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.LEFT) ? 1f : 0f, mArrowRight);
+
+ boolean a11yEnabled = mA11yManager.isEnabled();
+ setArrowA11yEnabled(mArrowUp, a11yEnabled, KEYCODE_DPAD_UP);
+ setArrowA11yEnabled(mArrowDown, a11yEnabled, KEYCODE_DPAD_DOWN);
+ setArrowA11yEnabled(mArrowLeft, a11yEnabled, KEYCODE_DPAD_LEFT);
+ setArrowA11yEnabled(mArrowRight, a11yEnabled, KEYCODE_DPAD_RIGHT);
+
+ animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
+ if (a11yEnabled) {
+ mA11yDoneButton.setVisibility(VISIBLE);
+ mA11yDoneButton.setOnClickListener(v -> {
+ mListener.onExitMoveMode();
+ });
+ mA11yDoneButton.requestFocus();
+ mA11yDoneButton.requestAccessibilityFocus();
}
+ }
- animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
- animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
- animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
- animateAlphaTo(checkGravity(gravity, Gravity.LEFT) ? 1f : 0f, mArrowRight);
+ private void setArrowA11yEnabled(View arrowView, boolean enabled, int keycode) {
+ arrowView.setClickable(enabled);
+ if (enabled) {
+ arrowView.setOnClickListener(v -> {
+ mListener.onPipMovement(keycode);
+ });
+ }
}
private boolean checkGravity(int gravity, int feature) {
@@ -698,41 +553,81 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
* Hides user hints for moving the PiP, e.g. arrows.
*/
public void hideMovementHints() {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: hideMovementHints()", TAG);
- }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: hideMovementHints()", TAG);
+
+ if (mCurrentMenuMode != MODE_MOVE_MENU) return;
+
animateAlphaTo(0, mArrowUp);
animateAlphaTo(0, mArrowRight);
animateAlphaTo(0, mArrowDown);
animateAlphaTo(0, mArrowLeft);
+ animateAlphaTo(0, mA11yDoneButton);
}
/**
* Show or hide the pip buttons menu.
*/
- public void showButtonsMenu(boolean show) {
- if (DEBUG) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: showUserActions: %b", TAG, show);
- }
- if (show) {
- mActionButtonsContainer.setVisibility(VISIBLE);
- refocusPreviousButton();
- }
- animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
+ private void setMenuButtonsVisible(boolean visible) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: showUserActions: %b", TAG, visible);
+ animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView);
}
private void setFrameHighlighted(boolean highlighted) {
mMenuFrameView.setActivated(highlighted);
}
+ private class RecyclerViewAdapter extends
+ RecyclerView.Adapter<RecyclerViewAdapter.ButtonViewHolder> {
+
+ private final List<TvPipAction> mActionList;
+
+ RecyclerViewAdapter(List<TvPipAction> actionList) {
+ this.mActionList = actionList;
+ }
+
+ @NonNull
+ @Override
+ public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ButtonViewHolder(new TvWindowMenuActionButton(mContext));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) {
+ TvPipAction action = mActionList.get(position);
+ action.populateButton(holder.mButton, mMainHandler);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mActionList.size();
+ }
+
+ private class ButtonViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
+ TvWindowMenuActionButton mButton;
+
+ ButtonViewHolder(@NonNull View itemView) {
+ super(itemView);
+ mButton = (TvWindowMenuActionButton) itemView;
+ mButton.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ TvPipAction action = mActionList.get(
+ mActionButtonsRecyclerView.getChildLayoutPosition(v));
+ if (action != null) {
+ action.executeAction();
+ }
+ }
+ }
+ }
+
interface Listener {
void onBackPress();
- void onEnterMoveMode();
-
/**
* Called when a button for exiting move mode was pressed.
*
@@ -746,10 +641,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
*/
boolean onPipMovement(int keycode);
- void onCloseButtonClick();
-
- void onFullscreenButtonClick();
+ /**
+ * Called when the TvPipMenuView loses focus. This also means that the TV PiP menu window
+ * has lost focus.
+ */
+ void onPipWindowFocusChanged(boolean focused);
- void onToggleExpandedMode();
+ /**
+ * The edu text closing impacts the size of the Picture-in-Picture window and influences
+ * how it is positioned on the screen.
+ */
+ void onCloseEduText();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
index 61a609d9755e..f22ee595e6c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
@@ -16,18 +16,13 @@
package com.android.wm.shell.pip.tv;
-import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE;
-import static android.app.Notification.Action.SEMANTIC_ACTION_NONE;
-
+import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
@@ -35,7 +30,6 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.session.MediaSession;
import android.os.Bundle;
-import android.os.Handler;
import android.text.TextUtils;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
@@ -47,7 +41,6 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -55,39 +48,18 @@ import java.util.List;
* <p>Once it's created, it will manage the PiP notification UI by itself except for handling
* configuration changes and user initiated expanded PiP toggling.
*/
-public class TvPipNotificationController {
- private static final String TAG = "TvPipNotification";
+public class TvPipNotificationController implements TvPipActionsProvider.Listener {
+ private static final String TAG = TvPipNotificationController.class.getSimpleName();
// Referenced in com.android.systemui.util.NotificationChannels.
public static final String NOTIFICATION_CHANNEL = "TVPIP";
private static final String NOTIFICATION_TAG = "TvPip";
- private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
-
- private static final String ACTION_SHOW_PIP_MENU =
- "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
- private static final String ACTION_CLOSE_PIP =
- "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP";
- private static final String ACTION_MOVE_PIP =
- "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
- private static final String ACTION_TOGGLE_EXPANDED_PIP =
- "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP";
- private static final String ACTION_FULLSCREEN =
- "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN";
private final Context mContext;
private final PackageManager mPackageManager;
private final NotificationManager mNotificationManager;
private final Notification.Builder mNotificationBuilder;
- private final ActionBroadcastReceiver mActionBroadcastReceiver;
- private final Handler mMainHandler;
- private Delegate mDelegate;
- private final TvPipBoundsState mTvPipBoundsState;
-
- private String mDefaultTitle;
-
- private final List<RemoteAction> mCustomActions = new ArrayList<>();
- private final List<RemoteAction> mMediaActions = new ArrayList<>();
- private RemoteAction mCustomCloseAction;
+ private TvPipActionsProvider mTvPipActionsProvider;
private MediaSession.Token mMediaSessionToken;
@@ -95,55 +67,41 @@ public class TvPipNotificationController {
private String mPackageName;
private boolean mIsNotificationShown;
+ private String mDefaultTitle;
private String mPipTitle;
private String mPipSubtitle;
+ // Saving the actions, so they don't have to be regenerated when e.g. the PiP title changes.
+ @NonNull
+ private Notification.Action[] mPipActions;
+
private Bitmap mActivityIcon;
public TvPipNotificationController(Context context, PipMediaController pipMediaController,
- PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState,
- Handler mainHandler) {
+ PipParamsChangedForwarder pipParamsChangedForwarder) {
mContext = context;
mPackageManager = context.getPackageManager();
mNotificationManager = context.getSystemService(NotificationManager.class);
- mMainHandler = mainHandler;
- mTvPipBoundsState = tvPipBoundsState;
+
+ mPipActions = new Notification.Action[0];
mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL)
.setLocalOnly(true)
.setOngoing(true)
.setCategory(Notification.CATEGORY_SYSTEM)
.setShowWhen(true)
+ .setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.pip_icon)
.setAllowSystemGeneratedContextualActions(false)
- .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN))
- .setDeleteIntent(getCloseAction().actionIntent)
- .extend(new Notification.TvExtender()
- .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU))
- .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP)));
-
- mActionBroadcastReceiver = new ActionBroadcastReceiver();
+ .setContentIntent(
+ createPendingIntent(context, TvPipController.ACTION_TO_FULLSCREEN));
+ // TvExtender and DeleteIntent set later since they might change.
- pipMediaController.addActionListener(this::onMediaActionsChanged);
pipMediaController.addTokenListener(this::onMediaSessionTokenChanged);
pipParamsChangedForwarder.addListener(
new PipParamsChangedForwarder.PipParamsChangedCallback() {
@Override
- public void onExpandedAspectRatioChanged(float ratio) {
- updateExpansionState();
- }
-
- @Override
- public void onActionsChanged(List<RemoteAction> actions,
- RemoteAction closeAction) {
- mCustomActions.clear();
- mCustomActions.addAll(actions);
- mCustomCloseAction = closeAction;
- updateNotificationContent();
- }
-
- @Override
public void onTitleChanged(String title) {
mPipTitle = title;
updateNotificationContent();
@@ -156,34 +114,33 @@ public class TvPipNotificationController {
}
});
- onConfigurationChanged(context);
+ onConfigurationChanged();
}
- void setDelegate(Delegate delegate) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s",
- TAG, delegate);
-
- if (mDelegate != null) {
- throw new IllegalStateException(
- "The delegate has already been set and should not change.");
- }
- if (delegate == null) {
- throw new IllegalArgumentException("The delegate must not be null.");
- }
+ /**
+ * Call before showing any notification.
+ */
+ void setTvPipActionsProvider(@NonNull TvPipActionsProvider tvPipActionsProvider) {
+ mTvPipActionsProvider = tvPipActionsProvider;
+ mTvPipActionsProvider.addListener(this);
+ }
- mDelegate = delegate;
+ void onConfigurationChanged() {
+ mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title);
+ updateNotificationContent();
}
void show(String packageName) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName);
- if (mDelegate == null) {
- throw new IllegalStateException("Delegate is not set.");
+ if (mTvPipActionsProvider == null) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Missing TvPipActionsProvider", TAG);
+ return;
}
mIsNotificationShown = true;
mPackageName = packageName;
mActivityIcon = getActivityIcon();
- mActionBroadcastReceiver.register();
updateNotificationContent();
}
@@ -193,151 +150,42 @@ public class TvPipNotificationController {
mIsNotificationShown = false;
mPackageName = null;
- mActionBroadcastReceiver.unregister();
-
mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
}
- private Notification.Action getToggleAction(boolean expanded) {
- if (expanded) {
- return createSystemAction(R.drawable.pip_ic_collapse,
- R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP);
- } else {
- return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand,
- ACTION_TOGGLE_EXPANDED_PIP);
- }
- }
-
- private Notification.Action createSystemAction(int iconRes, int titleRes, String action) {
- Notification.Action.Builder builder = new Notification.Action.Builder(
- Icon.createWithResource(mContext, iconRes),
- mContext.getString(titleRes),
- createPendingIntent(mContext, action));
- builder.setContextual(true);
- return builder.build();
- }
-
- private void onMediaActionsChanged(List<RemoteAction> actions) {
- mMediaActions.clear();
- mMediaActions.addAll(actions);
- if (mCustomActions.isEmpty()) {
- updateNotificationContent();
- }
- }
-
private void onMediaSessionTokenChanged(MediaSession.Token token) {
mMediaSessionToken = token;
updateNotificationContent();
}
- private Notification.Action remoteToNotificationAction(RemoteAction action) {
- return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE);
- }
-
- private Notification.Action remoteToNotificationAction(RemoteAction action,
- int semanticAction) {
- Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(),
- action.getTitle(),
- action.getActionIntent());
- if (action.getContentDescription() != null) {
- Bundle extras = new Bundle();
- extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION,
- action.getContentDescription());
- builder.addExtras(extras);
- }
- builder.setSemanticAction(semanticAction);
- builder.setContextual(true);
- return builder.build();
- }
-
- private Notification.Action[] getNotificationActions() {
- final List<Notification.Action> actions = new ArrayList<>();
-
- // 1. Fullscreen
- actions.add(getFullscreenAction());
- // 2. Close
- actions.add(getCloseAction());
- // 3. App actions
- final List<RemoteAction> appActions =
- mCustomActions.isEmpty() ? mMediaActions : mCustomActions;
- for (RemoteAction appAction : appActions) {
- if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction)
- || !appAction.isEnabled()) {
- continue;
- }
- actions.add(remoteToNotificationAction(appAction));
- }
- // 4. Move
- actions.add(getMoveAction());
- // 5. Toggle expansion (if expanded PiP enabled)
- if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0
- && mTvPipBoundsState.isTvExpandedPipSupported()) {
- actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded()));
- }
- return actions.toArray(new Notification.Action[0]);
- }
-
- private Notification.Action getCloseAction() {
- if (mCustomCloseAction == null) {
- return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close,
- ACTION_CLOSE_PIP);
- } else {
- return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE);
- }
- }
-
- private Notification.Action getFullscreenAction() {
- return createSystemAction(R.drawable.pip_ic_fullscreen_white,
- R.string.pip_fullscreen, ACTION_FULLSCREEN);
- }
-
- private Notification.Action getMoveAction() {
- return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move,
- ACTION_MOVE_PIP);
- }
-
- /**
- * Called by {@link TvPipController} when the configuration is changed.
- */
- void onConfigurationChanged(Context context) {
- mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title);
- updateNotificationContent();
- }
-
- void updateExpansionState() {
- updateNotificationContent();
- }
-
private void updateNotificationContent() {
if (mPackageManager == null || !mIsNotificationShown) {
return;
}
- Notification.Action[] actions = getNotificationActions();
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG,
- getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length);
- for (Notification.Action action : actions) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG,
- action.toString());
- }
-
+ getNotificationTitle(), mPipSubtitle, mMediaSessionToken, mPipActions.length);
mNotificationBuilder
.setWhen(System.currentTimeMillis())
.setContentTitle(getNotificationTitle())
.setContentText(mPipSubtitle)
.setSubText(getApplicationLabel(mPackageName))
- .setActions(actions);
+ .setActions(mPipActions);
setPipIcon();
Bundle extras = new Bundle();
extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken);
mNotificationBuilder.setExtras(extras);
+ PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent();
+ mNotificationBuilder.setDeleteIntent(closeIntent);
// TvExtender not recognized if not set last.
mNotificationBuilder.extend(new Notification.TvExtender()
- .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU))
- .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP)));
+ .setContentIntent(
+ createPendingIntent(mContext, TvPipController.ACTION_SHOW_PIP_MENU))
+ .setDeleteIntent(closeIntent));
+
mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP,
mNotificationBuilder.build());
}
@@ -389,68 +237,20 @@ public class TvPipNotificationController {
return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true);
}
- private static PendingIntent createPendingIntent(Context context, String action) {
+ static PendingIntent createPendingIntent(Context context, String action) {
return PendingIntent.getBroadcast(context, 0,
new Intent(action).setPackage(context.getPackageName()),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
- private class ActionBroadcastReceiver extends BroadcastReceiver {
- final IntentFilter mIntentFilter;
- {
- mIntentFilter = new IntentFilter();
- mIntentFilter.addAction(ACTION_CLOSE_PIP);
- mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
- mIntentFilter.addAction(ACTION_MOVE_PIP);
- mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
- mIntentFilter.addAction(ACTION_FULLSCREEN);
- }
- boolean mRegistered = false;
-
- void register() {
- if (mRegistered) return;
-
- mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION,
- mMainHandler);
- mRegistered = true;
- }
-
- void unregister() {
- if (!mRegistered) return;
-
- mContext.unregisterReceiver(this);
- mRegistered = false;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: on(Broadcast)Receive(), action=%s", TAG, action);
-
- if (ACTION_SHOW_PIP_MENU.equals(action)) {
- mDelegate.showPictureInPictureMenu();
- } else if (ACTION_CLOSE_PIP.equals(action)) {
- mDelegate.closePip();
- } else if (ACTION_MOVE_PIP.equals(action)) {
- mDelegate.enterPipMovementMenu();
- } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) {
- mDelegate.togglePipExpansion();
- } else if (ACTION_FULLSCREEN.equals(action)) {
- mDelegate.movePipToFullscreen();
- }
+ @Override
+ public void onActionsChanged(int added, int updated, int startIndex) {
+ List<TvPipAction> actions = mTvPipActionsProvider.getActionsList();
+ mPipActions = new Notification.Action[actions.size()];
+ for (int i = 0; i < mPipActions.length; i++) {
+ mPipActions[i] = actions.get(i).toNotificationAction(mContext);
}
+ updateNotificationContent();
}
- interface Delegate {
- void showPictureInPictureMenu();
-
- void closePip();
-
- void enterPipMovementMenu();
-
- void togglePipExpansion();
-
- void movePipToFullscreen();
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java
new file mode 100644
index 000000000000..f6dabb762c54
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE;
+import static android.app.Notification.Action.SEMANTIC_ACTION_NONE;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.StringRes;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+
+import com.android.wm.shell.common.TvWindowMenuActionButton;
+
+/**
+ * A TvPipAction for actions that the system provides, i.e. fullscreen, default close, move,
+ * expand/collapse.
+ */
+public class TvPipSystemAction extends TvPipAction {
+
+ @StringRes
+ private int mTitleResource;
+ @DrawableRes
+ private int mIconResource;
+
+ private final PendingIntent mBroadcastIntent;
+
+ TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon,
+ String broadcastAction, @NonNull Context context,
+ SystemActionsHandler systemActionsHandler) {
+ super(actionType, systemActionsHandler);
+ update(title, icon);
+ mBroadcastIntent = TvPipNotificationController.createPendingIntent(context,
+ broadcastAction);
+ }
+
+ /**
+ * @return true if the title and/or icon were changed.
+ */
+ boolean update(@StringRes int title, @DrawableRes int icon) {
+ boolean changed = title != mTitleResource || icon != mIconResource;
+ mTitleResource = title;
+ mIconResource = icon;
+ return changed;
+ }
+
+ void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) {
+ button.setTextAndDescription(mTitleResource);
+ button.setImageResource(mIconResource);
+ button.setEnabled(true);
+ button.setIsCustomCloseAction(false);
+ }
+
+ PendingIntent getPendingIntent() {
+ return mBroadcastIntent;
+ }
+
+ @Override
+ Notification.Action toNotificationAction(Context context) {
+ Notification.Action.Builder builder = new Notification.Action.Builder(
+ Icon.createWithResource(context, mIconResource),
+ context.getString(mTitleResource),
+ mBroadcastIntent);
+
+ builder.setSemanticAction(isCloseAction()
+ ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE);
+ builder.setContextual(true);
+ return builder.build();
+ }
+
+}
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 be9b9361b359..4819f665d6d3 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
@@ -28,6 +28,7 @@ import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
@@ -36,7 +37,6 @@ import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.pip.PipUtils;
-import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.util.Objects;
@@ -51,7 +51,7 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer {
@NonNull SyncTransactionQueue syncTransactionQueue,
@NonNull PipTransitionState pipTransitionState,
@NonNull PipBoundsState pipBoundsState,
- @NonNull PipSizeSpecHandler pipSizeSpecHandler,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState,
@NonNull PipBoundsAlgorithm boundsHandler,
@NonNull PipMenuController pipMenuController,
@NonNull PipAnimationController pipAnimationController,
@@ -63,8 +63,8 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer {
@NonNull PipUiEventLogger pipUiEventLogger,
@NonNull ShellTaskOrganizer shellTaskOrganizer,
ShellExecutor mainExecutor) {
- super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipSizeSpecHandler,
- boundsHandler, pipMenuController, pipAnimationController,
+ super(context, syncTransactionQueue, pipTransitionState, pipBoundsState,
+ pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController,
surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer,
mainExecutor);
@@ -85,4 +85,24 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer {
mPipParamsChangedForwarder.notifySubtitleChanged(params.getSubtitle());
}
}
+
+ /**
+ * Override for TV since the menu bounds affect the PiP location. Additionally, we want to
+ * ensure that menu is shown immediately since it should always be visible on TV as it creates
+ * a border with rounded corners around the PiP.
+ */
+ protected boolean shouldAttachMenuEarly() {
+ return true;
+ }
+
+ protected boolean shouldAlwaysFadeIn() {
+ return true;
+ }
+
+ @Override
+ protected boolean shouldSyncPipTransactionWithMenu() {
+ // We always have a menu visible and want to sync the pip transaction with the menu, even
+ // when the menu alpha is 0 (e.g. when a fade-in animation starts).
+ return true;
+ }
}
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 8ebcf63f36e9..d3253a5e4d94 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,60 +16,43 @@
package com.android.wm.shell.pip.tv;
-import android.app.TaskInfo;
-import android.graphics.Rect;
-import android.os.IBinder;
-import android.view.SurfaceControl;
-import android.window.TransitionInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerTransaction;
+import android.content.Context;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipMenuController;
-import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
+import com.android.wm.shell.pip.PipTransition;
+import com.android.wm.shell.pip.PipTransitionState;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import java.util.Optional;
+
/**
* PiP Transition for TV.
- * TODO: Implement animation once TV is using Transitions.
*/
-public class TvPipTransition extends PipTransitionController {
- public TvPipTransition(
+public class TvPipTransition extends PipTransition {
+
+ public TvPipTransition(Context context,
@NonNull ShellInit shellInit,
@NonNull ShellTaskOrganizer shellTaskOrganizer,
@NonNull Transitions transitions,
- PipBoundsState pipBoundsState,
- PipMenuController pipMenuController,
+ TvPipBoundsState tvPipBoundsState,
+ PipDisplayLayoutState pipDisplayLayoutState,
+ PipTransitionState pipTransitionState,
+ TvPipMenuController tvPipMenuController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
- PipAnimationController pipAnimationController) {
- super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
- tvPipBoundsAlgorithm, pipAnimationController);
- }
-
- @Override
- public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, int direction,
- SurfaceControl.Transaction tx) {
-
- }
-
- @Override
- public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @android.annotation.NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- return false;
+ PipAnimationController pipAnimationController,
+ PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
+ Optional<SplitScreenController> splitScreenOptional) {
+ super(context, shellInit, shellTaskOrganizer, transitions, tvPipBoundsState,
+ pipDisplayLayoutState, pipTransitionState, tvPipMenuController,
+ tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
+ splitScreenOptional);
}
- @Nullable
- @Override
- public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
- @NonNull TransitionRequestInfo request) {
- return null;
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index c9b3a1af6507..2a61445b27ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -32,6 +32,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
Consts.TAG_WM_SHELL),
WM_SHELL_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
Consts.TAG_WM_SHELL),
+ WM_SHELL_RECENTS_TRANSITION(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+ "ShellRecents"),
WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
Consts.TAG_WM_SHELL),
WM_SHELL_STARTING_WINDOW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
@@ -40,7 +42,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
"ShellBackPreview"),
WM_SHELL_RECENT_TASKS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
- WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ // TODO(b/282232877): turn logToLogcat to false.
+ WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
Consts.TAG_WM_SHELL),
WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
Consts.TAG_WM_SPLIT_SCREEN),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
index 1a6c1d65db03..4048c5b8feab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
@@ -17,6 +17,11 @@
package com.android.wm.shell.recents;
import android.app.ActivityManager.RunningTaskInfo;
+import android.app.IApplicationThread;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.IRecentsAnimationRunner;
import com.android.wm.shell.recents.IRecentTasksListener;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
@@ -45,4 +50,10 @@ interface IRecentTasks {
* Gets the set of running tasks.
*/
RunningTaskInfo[] getRunningTasks(int maxNum) = 4;
+
+ /**
+ * Starts a recents transition.
+ */
+ oneway void startRecentsTransition(in PendingIntent intent, in Intent fillIn, in Bundle options,
+ IApplicationThread appThread, IRecentsAnimationRunner listener) = 5;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 0d9faa3c6f83..f35eda6caef0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -24,13 +24,18 @@ import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RE
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
+import android.app.IApplicationThread;
+import android.app.PendingIntent;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.view.IRecentsAnimationRunner;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
@@ -79,6 +84,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
private final TaskStackListenerImpl mTaskStackListener;
private final RecentTasksImpl mImpl = new RecentTasksImpl();
private final ActivityTaskManager mActivityTaskManager;
+ private RecentsTransitionHandler mTransitionHandler = null;
private IRecentTasksListener mListener;
private final boolean mIsDesktopMode;
@@ -150,6 +156,10 @@ public class RecentTasksController implements TaskStackListenerCallback,
mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
}
+ void setTransitionHandler(RecentsTransitionHandler handler) {
+ mTransitionHandler = handler;
+ }
+
/**
* Adds a split pair. This call does not validate the taskIds, only that they are not the same.
*/
@@ -235,7 +245,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
}
@Override
- public void onActiveTasksChanged() {
+ public void onActiveTasksChanged(int displayId) {
notifyRecentTasksChanged();
}
@@ -361,7 +371,8 @@ public class RecentTasksController implements TaskStackListenerCallback,
* Find the background task that match the given component.
*/
@Nullable
- public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName) {
+ public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName,
+ int userId) {
if (componentName == null) {
return null;
}
@@ -373,7 +384,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
if (task.isVisible) {
continue;
}
- if (componentName.equals(task.baseIntent.getComponent())) {
+ if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) {
return task;
}
}
@@ -492,5 +503,18 @@ public class RecentTasksController implements TaskStackListenerCallback,
true /* blocking */);
return tasks[0];
}
+
+ @Override
+ public void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
+ IApplicationThread appThread, IRecentsAnimationRunner listener) {
+ if (mController.mTransitionHandler == null) {
+ Slog.e(TAG, "Used shell-transitions startRecentsTransition without"
+ + " shell-transitions");
+ return;
+ }
+ executeRemoteCallWithTaskPermission(mController, "startRecentsTransition",
+ (controller) -> controller.mTransitionHandler.startRecentsTransition(
+ intent, fillIn, options, appThread, listener));
+ }
}
-} \ No newline at end of file
+}
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
new file mode 100644
index 000000000000..843e5af67af9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -0,0 +1,1046 @@
+/*
+ * 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.recents;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
+import static android.view.WindowManager.TRANSIT_SLEEP;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.IApplicationThread;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.IntArray;
+import android.util.Pair;
+import android.util.Slog;
+import android.view.Display;
+import android.view.IRecentsAnimationController;
+import android.view.IRecentsAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.window.PictureInPictureSurfaceTransaction;
+import android.window.TaskSnapshot;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+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.ArrayList;
+
+/**
+ * Handles the Recents (overview) animation. Only one of these can run at a time. A recents
+ * transition must be created via {@link #startRecentsTransition}. Anything else will be ignored.
+ */
+public class RecentsTransitionHandler implements Transitions.TransitionHandler {
+ private static final String TAG = "RecentsTransitionHandler";
+
+ private final Transitions mTransitions;
+ private final ShellExecutor mExecutor;
+ private IApplicationThread mAnimApp = null;
+ private final ArrayList<RecentsController> mControllers = new ArrayList<>();
+
+ /**
+ * List of other handlers which might need to mix recents with other things. These are checked
+ * in the order they are added. Ideally there should only be one.
+ */
+ private final ArrayList<RecentsMixedHandler> mMixers = new ArrayList<>();
+
+ public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
+ @Nullable RecentTasksController recentTasksController) {
+ mTransitions = transitions;
+ mExecutor = transitions.getMainExecutor();
+ if (!Transitions.ENABLE_SHELL_TRANSITIONS) return;
+ if (recentTasksController == null) return;
+ shellInit.addInitCallback(() -> {
+ recentTasksController.setTransitionHandler(this);
+ transitions.addHandler(this);
+ }, this);
+ }
+
+ /** Register a mixer handler. {@see RecentsMixedHandler}*/
+ public void addMixer(RecentsMixedHandler mixer) {
+ mMixers.add(mixer);
+ }
+
+ /** Unregister a Mixed Handler */
+ public void removeMixer(RecentsMixedHandler mixer) {
+ mMixers.remove(mixer);
+ }
+
+ @VisibleForTesting
+ public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
+ IApplicationThread appThread, IRecentsAnimationRunner listener) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "RecentsTransitionHandler.startRecentsTransition");
+
+ // only care about latest one.
+ mAnimApp = appThread;
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.sendPendingIntent(intent, fillIn, options);
+ final RecentsController controller = new RecentsController(listener);
+ RecentsMixedHandler mixer = null;
+ Transitions.TransitionHandler mixedHandler = null;
+ for (int i = 0; i < mMixers.size(); ++i) {
+ mixedHandler = mMixers.get(i).handleRecentsRequest(wct);
+ if (mixedHandler != null) {
+ mixer = mMixers.get(i);
+ break;
+ }
+ }
+ final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct,
+ mixedHandler == null ? this : mixedHandler);
+ if (mixer != null) {
+ mixer.setRecentsTransition(transition);
+ }
+ if (transition != null) {
+ controller.setTransition(transition);
+ mControllers.add(controller);
+ } else {
+ controller.cancel("startRecentsTransition");
+ }
+ return transition;
+ }
+
+ @Override
+ public WindowContainerTransaction handleRequest(IBinder transition,
+ TransitionRequestInfo request) {
+ if (mControllers.isEmpty()) {
+ // Ignore if there is no running recents transition
+ return null;
+ }
+ final RecentsController controller = mControllers.get(mControllers.size() - 1);
+ controller.handleMidTransitionRequest(request);
+ return null;
+ }
+
+ private int findController(IBinder transition) {
+ for (int i = mControllers.size() - 1; i >= 0; --i) {
+ if (mControllers.get(i).mTransition == transition) return i;
+ }
+ return -1;
+ }
+
+ @Override
+ public boolean startAnimation(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction startTransaction,
+ SurfaceControl.Transaction finishTransaction,
+ Transitions.TransitionFinishCallback finishCallback) {
+ final int controllerIdx = findController(transition);
+ if (controllerIdx < 0) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "RecentsTransitionHandler.startAnimation: no controller found");
+ return false;
+ }
+ final RecentsController controller = mControllers.get(controllerIdx);
+ Transitions.setRunningRemoteTransitionDelegate(mAnimApp);
+ mAnimApp = null;
+ if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "RecentsTransitionHandler.startAnimation: failed to start animation");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void mergeAnimation(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction t, IBinder mergeTarget,
+ Transitions.TransitionFinishCallback finishCallback) {
+ final int targetIdx = findController(mergeTarget);
+ if (targetIdx < 0) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "RecentsTransitionHandler.mergeAnimation: no controller found");
+ return;
+ }
+ final RecentsController controller = mControllers.get(targetIdx);
+ controller.merge(info, t, finishCallback);
+ }
+
+ @Override
+ public void onTransitionConsumed(IBinder transition, boolean aborted,
+ SurfaceControl.Transaction finishTransaction) {
+ // Only one recents transition can be handled at a time, but currently the first transition
+ // will trigger a no-op in the second transition which holds the active recents animation
+ // runner on the launcher side. For now, cancel all existing animations to ensure we
+ // don't get into a broken state with an orphaned animation runner, and later we can try to
+ // merge the latest transition into the currently running one
+ for (int i = mControllers.size() - 1; i >= 0; i--) {
+ mControllers.get(i).cancel("onTransitionConsumed");
+ }
+ }
+
+ /** There is only one of these and it gets reset on finish. */
+ private class RecentsController extends IRecentsAnimationController.Stub {
+ private final int mInstanceId;
+
+ private IRecentsAnimationRunner mListener;
+ private IBinder.DeathRecipient mDeathHandler;
+ private Transitions.TransitionFinishCallback mFinishCB = null;
+ private SurfaceControl.Transaction mFinishTransaction = null;
+
+ /**
+ * List of tasks that we are switching away from via this transition. Upon finish, these
+ * pausing tasks will become invisible.
+ * These need to be ordered since the order must be restored if there is no task-switch.
+ */
+ private ArrayList<TaskState> mPausingTasks = null;
+
+ /**
+ * List of tasks that we are switching to. Upon finish, these will remain visible and
+ * on top.
+ */
+ private ArrayList<TaskState> mOpeningTasks = null;
+
+ private WindowContainerToken mPipTask = null;
+ private WindowContainerToken mRecentsTask = null;
+ private int mRecentsTaskId = -1;
+ private TransitionInfo mInfo = null;
+ private boolean mOpeningSeparateHome = false;
+ private boolean mPausingSeparateHome = false;
+ private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null;
+ private PictureInPictureSurfaceTransaction mPipTransaction = null;
+ private IBinder mTransition = null;
+ private boolean mKeyguardLocked = false;
+ private boolean mWillFinishToHome = false;
+
+ /** The animation is idle, waiting for the user to choose a task to switch to. */
+ private static final int STATE_NORMAL = 0;
+
+ /** The user chose a new task to switch to and the animation is animating to it. */
+ private static final int STATE_NEW_TASK = 1;
+
+ /** The latest state that the recents animation is operating in. */
+ private int mState = STATE_NORMAL;
+
+ // Snapshots taken when a new display change transition is requested, prior to the display
+ // change being applied. This pending set of snapshots will only be applied when cancel is
+ // next called.
+ private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel;
+
+ RecentsController(IRecentsAnimationRunner listener) {
+ mInstanceId = System.identityHashCode(this);
+ mListener = listener;
+ mDeathHandler = () -> {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
+ finish(mWillFinishToHome, false /* leaveHint */);
+ };
+ try {
+ mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RecentsController: failed to link to death", e);
+ mListener = null;
+ }
+ }
+
+ void setTransition(IBinder transition) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.setTransition: id=%s", mInstanceId, transition);
+ mTransition = transition;
+ }
+
+ void cancel(String reason) {
+ // restoring (to-home = false) involves submitting more WM changes, so by default, use
+ // toHome = true when canceling.
+ cancel(true /* toHome */, false /* withScreenshots */, reason);
+ }
+
+ void cancel(boolean toHome, boolean withScreenshots, String reason) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.cancel: toHome=%b reason=%s",
+ mInstanceId, toHome, reason);
+ if (mListener != null) {
+ if (withScreenshots) {
+ sendCancelWithSnapshots();
+ } else {
+ sendCancel(null, null);
+ }
+ }
+ if (mFinishCB != null) {
+ finishInner(toHome, false /* userLeave */);
+ } else {
+ cleanUp();
+ }
+ }
+
+ /**
+ * Sends a cancel message to the recents animation with snapshots. Used to trigger a
+ * "replace-with-screenshot" like behavior.
+ */
+ private boolean sendCancelWithSnapshots() {
+ Pair<int[], TaskSnapshot[]> snapshots = mPendingPauseSnapshotsForCancel != null
+ ? mPendingPauseSnapshotsForCancel
+ : getSnapshotsForPausingTasks();
+ return sendCancel(snapshots.first, snapshots.second);
+ }
+
+ /**
+ * Snapshots the pausing tasks and returns the mapping of the taskId -> snapshot.
+ */
+ private Pair<int[], TaskSnapshot[]> getSnapshotsForPausingTasks() {
+ int[] taskIds = null;
+ TaskSnapshot[] snapshots = null;
+ if (mPausingTasks != null && mPausingTasks.size() > 0) {
+ taskIds = new int[mPausingTasks.size()];
+ snapshots = new TaskSnapshot[mPausingTasks.size()];
+ try {
+ for (int i = 0; i < mPausingTasks.size(); ++i) {
+ TaskState state = mPausingTasks.get(0);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.sendCancel: Snapshotting task=%d",
+ mInstanceId, state.mTaskInfo.taskId);
+ snapshots[i] = ActivityTaskManager.getService().takeTaskSnapshot(
+ state.mTaskInfo.taskId, true /* updateCache */);
+ }
+ } catch (RemoteException e) {
+ taskIds = null;
+ snapshots = null;
+ }
+ }
+ return new Pair(taskIds, snapshots);
+ }
+
+ /**
+ * Sends a cancel message to the recents animation.
+ */
+ private boolean sendCancel(@Nullable int[] taskIds,
+ @Nullable TaskSnapshot[] taskSnapshots) {
+ try {
+ final String cancelDetails = taskSnapshots != null ? "with snapshots" : "";
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.cancel: calling onAnimationCanceled %s",
+ mInstanceId, cancelDetails);
+ mListener.onAnimationCanceled(taskIds, taskSnapshots);
+ return true;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error canceling recents animation", e);
+ return false;
+ }
+ }
+
+ void cleanUp() {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.cleanup", mInstanceId);
+ if (mListener != null && mDeathHandler != null) {
+ mListener.asBinder().unlinkToDeath(mDeathHandler, 0 /* flags */);
+ mDeathHandler = null;
+ }
+ mListener = null;
+ mFinishCB = null;
+ // clean-up leash surfacecontrols and anything that might reference them.
+ if (mLeashMap != null) {
+ for (int i = 0; i < mLeashMap.size(); ++i) {
+ mLeashMap.valueAt(i).release();
+ }
+ mLeashMap = null;
+ }
+ mFinishTransaction = null;
+ mPausingTasks = null;
+ mOpeningTasks = null;
+ mInfo = null;
+ mTransition = null;
+ mPendingPauseSnapshotsForCancel = null;
+ mControllers.remove(this);
+ }
+
+ boolean start(TransitionInfo info, SurfaceControl.Transaction t,
+ SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.start", mInstanceId);
+ if (mListener == null || mTransition == null) {
+ cleanUp();
+ return false;
+ }
+ // First see if this is a valid recents transition.
+ boolean hasPausingTasks = false;
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (TransitionUtil.isWallpaper(change)) continue;
+ if (TransitionUtil.isClosingType(change.getMode())) {
+ hasPausingTasks = true;
+ continue;
+ }
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_RECENTS) {
+ mRecentsTask = taskInfo.token;
+ mRecentsTaskId = taskInfo.taskId;
+ } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+ mRecentsTask = taskInfo.token;
+ mRecentsTaskId = taskInfo.taskId;
+ }
+ }
+ if (mRecentsTask == null && !hasPausingTasks) {
+ // Recents is already running apparently, so this is a no-op.
+ Slog.e(TAG, "Tried to start recents while it is already running.");
+ cleanUp();
+ return false;
+ }
+
+ mInfo = info;
+ mFinishCB = finishCB;
+ mFinishTransaction = finishT;
+ mPausingTasks = new ArrayList<>();
+ mOpeningTasks = new ArrayList<>();
+ mLeashMap = new ArrayMap<>();
+ mKeyguardLocked = (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0;
+
+ final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
+ final ArrayList<RemoteAnimationTarget> wallpapers = new ArrayList<>();
+ TransitionUtil.LeafTaskFilter leafTaskFilter = new TransitionUtil.LeafTaskFilter();
+ // About layering: we divide up the "layer space" into 3 regions (each the size of
+ // the change count). This lets us categorize things into above/below/between
+ // while maintaining their relative ordering.
+ final int belowLayers = info.getChanges().size();
+ final int middleLayers = info.getChanges().size() * 2;
+ final int aboveLayers = info.getChanges().size() * 3;
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (TransitionUtil.isWallpaper(change)) {
+ final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
+ // wallpapers go into the "below" layer space
+ belowLayers - i, info, t, mLeashMap);
+ wallpapers.add(target);
+ // Make all the wallpapers opaque since we want them visible from the start
+ t.setAlpha(target.leash, 1);
+ } else if (leafTaskFilter.test(change)) {
+ // start by putting everything into the "below" layer space.
+ final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
+ belowLayers - i, info, t, mLeashMap);
+ apps.add(target);
+ if (TransitionUtil.isClosingType(change.getMode())) {
+ mPausingTasks.add(new TaskState(change, target.leash));
+ if (taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " adding pausing leaf home taskId=%d", taskInfo.taskId);
+ // This can only happen if we have a separate recents/home (3p launcher)
+ mPausingSeparateHome = true;
+ } else {
+ final int layer = aboveLayers - i;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " adding pausing leaf taskId=%d at layer=%d",
+ taskInfo.taskId, layer);
+ // raise closing (pausing) task to "above" layer so it isn't covered
+ t.setLayer(target.leash, layer);
+ }
+ if (taskInfo.pictureInPictureParams != null
+ && taskInfo.pictureInPictureParams.isAutoEnterEnabled()) {
+ mPipTask = taskInfo.token;
+ }
+ } else if (taskInfo != null
+ && taskInfo.topActivityType == ACTIVITY_TYPE_RECENTS) {
+ final int layer = middleLayers - i;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " setting recents activity layer=%d", layer);
+ // There's a 3p launcher, so make sure recents goes above that, but under
+ // the pausing apps.
+ t.setLayer(target.leash, layer);
+ } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+ // do nothing
+ } else if (TransitionUtil.isOpeningType(change.getMode())) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " adding opening leaf taskId=%d", taskInfo.taskId);
+ mOpeningTasks.add(new TaskState(change, target.leash));
+ }
+ } else if (taskInfo != null && TransitionInfo.isIndependent(change, info)) {
+ // Root tasks
+ if (TransitionUtil.isClosingType(change.getMode())) {
+ final int layer = aboveLayers - i;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " adding pausing taskId=%d at layer=%d", taskInfo.taskId, layer);
+ // raise closing (pausing) task to "above" layer so it isn't covered
+ t.setLayer(change.getLeash(), layer);
+ mPausingTasks.add(new TaskState(change, null /* leash */));
+ } else if (TransitionUtil.isOpeningType(change.getMode())) {
+ final int layer = belowLayers - i;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " adding opening taskId=%d at layer=%d", taskInfo.taskId, layer);
+ // Put into the "below" layer space.
+ t.setLayer(change.getLeash(), layer);
+ mOpeningTasks.add(new TaskState(change, null /* leash */));
+ }
+ } else if (TransitionUtil.isDividerBar(change)) {
+ final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
+ belowLayers - i, info, t, mLeashMap);
+ // Add this as a app and we will separate them on launcher side by window type.
+ apps.add(target);
+ }
+ }
+ t.apply();
+ try {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.start: calling onAnimationStart", mInstanceId);
+ mListener.onAnimationStart(this,
+ apps.toArray(new RemoteAnimationTarget[apps.size()]),
+ wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
+ new Rect(0, 0, 0, 0), new Rect());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error starting recents animation", e);
+ cancel("onAnimationStart() failed");
+ }
+ return true;
+ }
+
+ /**
+ * Updates this controller when a new transition is requested mid-recents transition.
+ */
+ void handleMidTransitionRequest(TransitionRequestInfo request) {
+ if (request.getType() == TRANSIT_CHANGE && request.getDisplayChange() != null) {
+ final TransitionRequestInfo.DisplayChange dispChange = request.getDisplayChange();
+ if (dispChange.getStartRotation() != dispChange.getEndRotation()) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.prepareForMerge: "
+ + "Snapshot due to requested display change",
+ mInstanceId);
+ mPendingPauseSnapshotsForCancel = getSnapshotsForPausingTasks();
+ }
+ }
+ }
+
+ @SuppressLint("NewApi")
+ void merge(TransitionInfo info, SurfaceControl.Transaction t,
+ Transitions.TransitionFinishCallback finishCallback) {
+ if (mFinishCB == null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge: skip, no finish callback",
+ mInstanceId);
+ // This was no-op'd (likely a repeated start) and we've already sent finish.
+ return;
+ }
+ if (info.getType() == TRANSIT_SLEEP) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge: transit_sleep", mInstanceId);
+ // A sleep event means we need to stop animations immediately, so cancel here.
+ cancel("transit_sleep");
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge", mInstanceId);
+ // Keep all tasks in one list because order matters.
+ ArrayList<TransitionInfo.Change> openingTasks = null;
+ IntArray openingTaskIsLeafs = null;
+ ArrayList<TransitionInfo.Change> closingTasks = null;
+ mOpeningSeparateHome = false;
+ TransitionInfo.Change recentsOpening = null;
+ boolean foundRecentsClosing = false;
+ boolean hasChangingApp = false;
+ final TransitionUtil.LeafTaskFilter leafTaskFilter =
+ new TransitionUtil.LeafTaskFilter();
+ boolean hasTaskChange = false;
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo != null
+ && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) {
+ // Tasks that are always on top (e.g. bubbles), will handle their own transition
+ // as they are on top of everything else. So cancel the merge here.
+ cancel("task #" + taskInfo.taskId + " is always_on_top");
+ return;
+ }
+ final boolean isRootTask = taskInfo != null
+ && TransitionInfo.isIndependent(change, info);
+ hasTaskChange = hasTaskChange || isRootTask;
+ final boolean isLeafTask = leafTaskFilter.test(change);
+ if (TransitionUtil.isOpeningType(change.getMode())) {
+ if (mRecentsTask != null && mRecentsTask.equals(change.getContainer())) {
+ recentsOpening = change;
+ } else if (isRootTask || isLeafTask) {
+ if (isLeafTask && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+ // This is usually a 3p launcher
+ mOpeningSeparateHome = true;
+ }
+ if (openingTasks == null) {
+ openingTasks = new ArrayList<>();
+ openingTaskIsLeafs = new IntArray();
+ }
+ openingTasks.add(change);
+ openingTaskIsLeafs.add(isLeafTask ? 1 : 0);
+ }
+ } else if (TransitionUtil.isClosingType(change.getMode())) {
+ if (mRecentsTask != null && mRecentsTask.equals(change.getContainer())) {
+ foundRecentsClosing = true;
+ } else if (isRootTask || isLeafTask) {
+ if (closingTasks == null) {
+ closingTasks = new ArrayList<>();
+ }
+ closingTasks.add(change);
+ }
+ } else if (change.getMode() == TRANSIT_CHANGE) {
+ // Finish recents animation if the display is changed, so the default
+ // transition handler can play the animation such as rotation effect.
+ if (change.hasFlags(TransitionInfo.FLAG_IS_DISPLAY)) {
+ // This call to cancel will use the screenshots taken preemptively in
+ // handleMidTransitionRequest() prior to the display changing
+ cancel(mWillFinishToHome, true /* withScreenshots */, "display change");
+ return;
+ }
+ // Don't consider order-only & non-leaf changes as changing apps.
+ if (!TransitionUtil.isOrderOnly(change) && isLeafTask) {
+ hasChangingApp = true;
+ } else if (isLeafTask && taskInfo.topActivityType == ACTIVITY_TYPE_HOME
+ && !mRecentsTask.equals(change.getContainer())) {
+ // Unless it is a 3p launcher. This means that the 3p launcher was already
+ // visible (eg. the "pausing" task is translucent over the 3p launcher).
+ // Treat it as if we are "re-opening" the 3p launcher.
+ if (openingTasks == null) {
+ openingTasks = new ArrayList<>();
+ openingTaskIsLeafs = new IntArray();
+ }
+ openingTasks.add(change);
+ openingTaskIsLeafs.add(1);
+ }
+ }
+ }
+ if (hasChangingApp && foundRecentsClosing) {
+ // This happens when a visible app is expanding (usually PiP). In this case,
+ // that transition probably has a special-purpose animation, so finish recents
+ // now and let it do its animation (since recents is going to be occluded).
+ sendCancelWithSnapshots();
+ mExecutor.executeDelayed(
+ () -> finishInner(true /* toHome */, false /* userLeaveHint */), 0);
+ return;
+ }
+ if (recentsOpening != null) {
+ // the recents task re-appeared. This happens if the user gestures before the
+ // task-switch (NEW_TASK) animation finishes.
+ if (mState == STATE_NORMAL) {
+ Slog.e(TAG, "Returning to recents while recents is already idle.");
+ }
+ if (closingTasks == null || closingTasks.size() == 0) {
+ Slog.e(TAG, "Returning to recents without closing any opening tasks.");
+ }
+ // Setup may hide it initially since it doesn't know that overview was still active.
+ t.show(recentsOpening.getLeash());
+ t.setAlpha(recentsOpening.getLeash(), 1.f);
+ mState = STATE_NORMAL;
+ }
+ boolean didMergeThings = false;
+ if (closingTasks != null) {
+ // Potentially cancelling a task-switch. Move the tasks back to mPausing if they
+ // are in mOpening.
+ for (int i = 0; i < closingTasks.size(); ++i) {
+ final TransitionInfo.Change change = closingTasks.get(i);
+ final int pausingIdx = TaskState.indexOf(mPausingTasks, change);
+ if (pausingIdx >= 0) {
+ mPausingTasks.remove(pausingIdx);
+ didMergeThings = true;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " closing pausing taskId=%d", change.getTaskInfo().taskId);
+ continue;
+ }
+ int openingIdx = TaskState.indexOf(mOpeningTasks, change);
+ if (openingIdx < 0) {
+ Slog.w(TAG, "Closing a task that wasn't opening, this may be split or"
+ + " something unexpected: " + change.getTaskInfo().taskId);
+ continue;
+ }
+ final TaskState openingTask = mOpeningTasks.remove(openingIdx);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " pausing opening %staskId=%d", openingTask.isLeaf() ? "leaf " : "",
+ openingTask.mTaskInfo.taskId);
+ mPausingTasks.add(openingTask);
+ didMergeThings = true;
+ }
+ }
+ RemoteAnimationTarget[] appearedTargets = null;
+ if (openingTasks != null && openingTasks.size() > 0) {
+ // Switching to some new tasks, add to mOpening and remove from mPausing. Also,
+ // enter NEW_TASK state since this will start the switch-to animation.
+ final int layer = mInfo.getChanges().size() * 3;
+ int openingLeafCount = 0;
+ for (int i = 0; i < openingTaskIsLeafs.size(); ++i) {
+ openingLeafCount += openingTaskIsLeafs.get(i);
+ }
+ if (openingLeafCount > 0) {
+ appearedTargets = new RemoteAnimationTarget[openingLeafCount];
+ }
+ int nextTargetIdx = 0;
+ for (int i = 0; i < openingTasks.size(); ++i) {
+ final TransitionInfo.Change change = openingTasks.get(i);
+ final boolean isLeaf = openingTaskIsLeafs.get(i) == 1;
+ int pausingIdx = TaskState.indexOf(mPausingTasks, change);
+ if (pausingIdx >= 0) {
+ // Something is showing/opening a previously-pausing app.
+ if (isLeaf) {
+ appearedTargets[nextTargetIdx++] = TransitionUtil.newTarget(
+ change, layer, mPausingTasks.get(pausingIdx).mLeash);
+ }
+ final TaskState pausingTask = mPausingTasks.remove(pausingIdx);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " opening pausing %staskId=%d", isLeaf ? "leaf " : "",
+ pausingTask.mTaskInfo.taskId);
+ mOpeningTasks.add(pausingTask);
+ // Setup hides opening tasks initially, so make it visible again (since we
+ // are already showing it).
+ t.show(change.getLeash());
+ t.setAlpha(change.getLeash(), 1.f);
+ } else if (isLeaf) {
+ // We are receiving new opening leaf tasks, so convert to onTasksAppeared.
+ final RemoteAnimationTarget target = TransitionUtil.newTarget(
+ change, layer, info, t, mLeashMap);
+ appearedTargets[nextTargetIdx++] = target;
+ // reparent into the original `mInfo` since that's where we are animating.
+ final int rootIdx = TransitionUtil.rootIndexFor(change, mInfo);
+ t.reparent(target.leash, mInfo.getRoot(rootIdx).getLeash());
+ t.setLayer(target.leash, layer);
+ // Hide the animation leash, let listener show it.
+ t.hide(target.leash);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " opening new leaf taskId=%d", target.taskId);
+ mOpeningTasks.add(new TaskState(change, target.leash));
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " opening new taskId=%d", change.getTaskInfo().taskId);
+ t.setLayer(change.getLeash(), layer);
+ // Setup hides opening tasks initially, so make it visible since recents
+ // is only animating the leafs.
+ t.show(change.getLeash());
+ mOpeningTasks.add(new TaskState(change, null));
+ }
+ }
+ didMergeThings = true;
+ mState = STATE_NEW_TASK;
+ }
+ if (mPausingTasks.isEmpty()) {
+ // The pausing tasks may be removed by the incoming closing tasks.
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge: empty pausing tasks", mInstanceId);
+ }
+ if (!hasTaskChange) {
+ // Activity only transition, so consume the merge as it doesn't affect the rest of
+ // recents.
+ Slog.d(TAG, "Got an activity only transition during recents, so apply directly");
+ mergeActivityOnly(info, t);
+ } 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) {
+ mWillFinishToHome = false;
+ cancel(false /* toHome */, false /* withScreenshots */, "didn't merge");
+ }
+ return;
+ }
+ // At this point, we are accepting the merge.
+ t.apply();
+ // not using the incoming anim-only surfaces
+ info.releaseAnimSurfaces();
+ if (appearedTargets != null) {
+ try {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge: calling onTasksAppeared", mInstanceId);
+ mListener.onTasksAppeared(appearedTargets);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending appeared tasks to recents animation", e);
+ }
+ }
+ finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ }
+
+ /** For now, just set-up a jump-cut to the new activity. */
+ private void mergeActivityOnly(TransitionInfo info, SurfaceControl.Transaction t) {
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (TransitionUtil.isOpeningType(change.getMode())) {
+ t.show(change.getLeash());
+ t.setAlpha(change.getLeash(), 1.f);
+ } else if (TransitionUtil.isClosingType(change.getMode())) {
+ t.hide(change.getLeash());
+ }
+ }
+ }
+
+ @Override
+ public TaskSnapshot screenshotTask(int taskId) {
+ try {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.screenshotTask: taskId=%d", mInstanceId, taskId);
+ return ActivityTaskManager.getService().takeTaskSnapshot(taskId,
+ true /* updateCache */);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to screenshot task", e);
+ }
+ return null;
+ }
+
+ @Override
+ public void setInputConsumerEnabled(boolean enabled) {
+ mExecutor.execute(() -> {
+ if (mFinishCB == null || !enabled) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "RecentsController.setInputConsumerEnabled: skip, cb?=%b enabled?=%b",
+ mFinishCB != null, enabled);
+ return;
+ }
+ final int displayId = mInfo.getRootCount() > 0 ? mInfo.getRoot(0).getDisplayId()
+ : Display.DEFAULT_DISPLAY;
+ // transient launches don't receive focus automatically. Since we are taking over
+ // the gesture now, take focus explicitly.
+ // This also moves recents back to top if the user gestured before a switch
+ // animation finished.
+ try {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.setInputConsumerEnabled: set focus to recents",
+ mInstanceId);
+ ActivityTaskManager.getService().focusTopTask(displayId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set focused task", e);
+ }
+ });
+ }
+
+ @Override
+ public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) {
+ }
+
+ @Override
+ public void setFinishTaskTransaction(int taskId,
+ PictureInPictureSurfaceTransaction finishTransaction, SurfaceControl overlay) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.setFinishTaskTransaction: taskId=%d",
+ mInstanceId, taskId);
+ mExecutor.execute(() -> {
+ if (mFinishCB == null) return;
+ mPipTransaction = finishTransaction;
+ });
+ }
+
+ @Override
+ @SuppressLint("NewApi")
+ public void finish(boolean toHome, boolean sendUserLeaveHint) {
+ mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint));
+ }
+
+ private void finishInner(boolean toHome, boolean sendUserLeaveHint) {
+ if (mFinishCB == null) {
+ Slog.e(TAG, "Duplicate call to finish");
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.finishInner: toHome=%b userLeave=%b "
+ + "willFinishToHome=%b state=%d",
+ mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState);
+ final Transitions.TransitionFinishCallback finishCB = mFinishCB;
+ mFinishCB = null;
+
+ final SurfaceControl.Transaction t = mFinishTransaction;
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+ if (mKeyguardLocked && mRecentsTask != null) {
+ if (toHome) wct.reorder(mRecentsTask, true /* toTop */);
+ else wct.restoreTransientOrder(mRecentsTask);
+ }
+ if (!toHome
+ // If a recents gesture starts on the 3p launcher, then the 3p launcher is the
+ // live tile (pausing app). If the gesture is "cancelled" we need to return to
+ // 3p launcher instead of "task-switching" away from it.
+ && (!mWillFinishToHome || mPausingSeparateHome)
+ && mPausingTasks != null && mState == STATE_NORMAL) {
+ if (mPausingSeparateHome) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " returning to 3p home");
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " returning to app");
+ }
+ // The gesture is returning to the pausing-task(s) rather than continuing with
+ // recents, so end the transition by moving the app back to the top (and also
+ // re-showing it's task).
+ for (int i = mPausingTasks.size() - 1; i >= 0; --i) {
+ // reverse order so that index 0 ends up on top
+ wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */);
+ t.show(mPausingTasks.get(i).mTaskSurface);
+ }
+ if (!mKeyguardLocked && mRecentsTask != null) {
+ wct.restoreTransientOrder(mRecentsTask);
+ }
+ } else if (toHome && mOpeningSeparateHome && mPausingTasks != null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " 3p launching home");
+ // Special situation where 3p launcher was changed during recents (this happens
+ // during tapltests...). Here we get both "return to home" AND "home opening".
+ // This is basically going home, but we have to restore the recents and home order.
+ for (int i = 0; i < mOpeningTasks.size(); ++i) {
+ final TaskState state = mOpeningTasks.get(i);
+ if (state.mTaskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+ // Make sure it is on top.
+ wct.reorder(state.mToken, true /* onTop */);
+ }
+ t.show(state.mTaskSurface);
+ }
+ for (int i = mPausingTasks.size() - 1; i >= 0; --i) {
+ t.hide(mPausingTasks.get(i).mTaskSurface);
+ }
+ if (!mKeyguardLocked && mRecentsTask != null) {
+ wct.restoreTransientOrder(mRecentsTask);
+ }
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " normal finish");
+ // The general case: committing to recents, going home, or switching tasks.
+ for (int i = 0; i < mOpeningTasks.size(); ++i) {
+ t.show(mOpeningTasks.get(i).mTaskSurface);
+ }
+ for (int i = 0; i < mPausingTasks.size(); ++i) {
+ if (!sendUserLeaveHint && mPausingTasks.get(i).isLeaf()) {
+ // This means recents is not *actually* finishing, so of course we gotta
+ // do special stuff in WMCore to accommodate.
+ wct.setDoNotPip(mPausingTasks.get(i).mToken);
+ }
+ // Since we will reparent out of the leashes, pre-emptively hide the child
+ // surface to match the leash. Otherwise, there will be a flicker before the
+ // visibility gets committed in Core when using split-screen (in splitscreen,
+ // the leaf-tasks are not "independent" so aren't hidden by normal setup).
+ t.hide(mPausingTasks.get(i).mTaskSurface);
+ }
+ if (mPipTask != null && mPipTransaction != null && sendUserLeaveHint) {
+ t.show(mInfo.getChange(mPipTask).getLeash());
+ PictureInPictureSurfaceTransaction.apply(mPipTransaction,
+ mInfo.getChange(mPipTask).getLeash(), t);
+ mPipTask = null;
+ mPipTransaction = null;
+ }
+ }
+ cleanUp();
+ finishCB.onTransitionFinished(wct.isEmpty() ? null : wct, null /* wctCB */);
+ }
+
+ @Override
+ public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
+ }
+
+ @Override
+ public void cleanupScreenshot() {
+ }
+
+ @Override
+ public void setWillFinishToHome(boolean willFinishToHome) {
+ mExecutor.execute(() -> {
+ mWillFinishToHome = willFinishToHome;
+ });
+ }
+
+ /**
+ * @see IRecentsAnimationController#removeTask
+ */
+ @Override
+ public boolean removeTask(int taskId) {
+ return false;
+ }
+
+ /**
+ * @see IRecentsAnimationController#detachNavigationBarFromApp
+ */
+ @Override
+ public void detachNavigationBarFromApp(boolean moveHomeToTop) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.detachNavigationBarFromApp", mInstanceId);
+ mExecutor.execute(() -> {
+ if (mTransition == null) return;
+ try {
+ ActivityTaskManager.getService().detachNavigationBarFromApp(mTransition);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to detach the navigation bar from app", e);
+ }
+ });
+ }
+
+ /**
+ * @see IRecentsAnimationController#animateNavigationBarToApp(long)
+ */
+ @Override
+ public void animateNavigationBarToApp(long duration) {
+ }
+ };
+
+ /** Utility class to track the state of a task as-seen by recents. */
+ private static class TaskState {
+ WindowContainerToken mToken;
+ ActivityManager.RunningTaskInfo mTaskInfo;
+
+ /** The surface/leash of the task provided by Core. */
+ SurfaceControl mTaskSurface;
+
+ /** The (local) animation-leash created for this task. Only non-null for leafs. */
+ @Nullable
+ SurfaceControl mLeash;
+
+ TaskState(TransitionInfo.Change change, SurfaceControl leash) {
+ mToken = change.getContainer();
+ mTaskInfo = change.getTaskInfo();
+ mTaskSurface = change.getLeash();
+ mLeash = leash;
+ }
+
+ static int indexOf(ArrayList<TaskState> list, TransitionInfo.Change change) {
+ for (int i = list.size() - 1; i >= 0; --i) {
+ if (list.get(i).mToken.equals(change.getContainer())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ boolean isLeaf() {
+ return mLeash != null;
+ }
+
+ public String toString() {
+ return "" + mToken + " : " + mLeash;
+ }
+ }
+
+ /**
+ * An interface for a mixed handler to receive information about recents requests (since these
+ * come into this handler directly vs from WMCore request).
+ */
+ public interface RecentsMixedHandler {
+ /**
+ * Called when a recents request comes in. The handler can add operations to outWCT. If
+ * the handler wants to "accept" the transition, it should return itself; otherwise, it
+ * should return `null`.
+ *
+ * If a mixed-handler accepts this recents, it will be the de-facto handler for this
+ * transition and is required to call the associated {@link #startAnimation},
+ * {@link #mergeAnimation}, and {@link #onTransitionConsumed} methods.
+ */
+ Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT);
+
+ /**
+ * Reports the transition token associated with the accepted recents request. If there was
+ * a problem starting the request, this will be called with `null`.
+ */
+ void setRecentsTransition(@Nullable IBinder transition);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index 81e118a31b73..c414e708b28d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -73,8 +73,8 @@ interface ISplitScreen {
/**
* Starts an activity in a stage.
*/
- oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int position,
- in Bundle options, in InstanceId instanceId) = 9;
+ oneway void startIntent(in PendingIntent intent, int userId, in Intent fillInIntent,
+ int position, in Bundle options, in InstanceId instanceId) = 9;
/**
* Starts tasks simultaneously in one transition.
@@ -86,8 +86,8 @@ interface ISplitScreen {
/**
* Starts a pair of intent and task in one transition.
*/
- oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId,
- in Bundle options2, int sidePosition, float splitRatio,
+ oneway void startIntentAndTask(in PendingIntent pendingIntent, int userId1, in Bundle options1,
+ int taskId, in Bundle options2, int sidePosition, float splitRatio,
in RemoteTransition remoteTransition, in InstanceId instanceId) = 16;
/**
@@ -107,7 +107,7 @@ interface ISplitScreen {
/**
* Starts a pair of intent and task using legacy transition system.
*/
- oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent,
+ oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, int userId1,
in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio,
in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12;
@@ -121,16 +121,17 @@ interface ISplitScreen {
/**
* Start a pair of intents using legacy transition system.
*/
- oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1,
+ oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1, int userId1,
in ShortcutInfo shortcutInfo1, in Bundle options1, in PendingIntent pendingIntent2,
- in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition, float splitRatio,
- in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 18;
+ int userId2, in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition,
+ float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 18;
/**
* Start a pair of intents in one transition.
*/
- oneway void startIntents(in PendingIntent pendingIntent1, in Bundle options1,
- in PendingIntent pendingIntent2, in Bundle options2, int splitPosition,
+ oneway void startIntents(in PendingIntent pendingIntent1, int userId1,
+ in ShortcutInfo shortcutInfo1, in Bundle options1, in PendingIntent pendingIntent2,
+ int userId2, in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition,
float splitRatio, in RemoteTransition remoteTransition, in InstanceId instanceId) = 19;
/**
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 7d5ab8428a3e..e7a367f1992d 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
@@ -88,6 +88,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -165,7 +166,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
private final DisplayController mDisplayController;
private final DisplayImeController mDisplayImeController;
private final DisplayInsetsController mDisplayInsetsController;
- private final DragAndDropController mDragAndDropController;
+ private final Optional<DragAndDropController> mDragAndDropController;
private final Transitions mTransitions;
private final TransactionPool mTransactionPool;
private final IconProvider mIconProvider;
@@ -191,7 +192,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController,
- DragAndDropController dragAndDropController,
+ Optional<DragAndDropController> dragAndDropController,
Transitions transitions,
TransactionPool transactionPool,
IconProvider iconProvider,
@@ -253,7 +254,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
mDisplayController = displayController;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
- mDragAndDropController = dragAndDropController;
+ mDragAndDropController = Optional.of(dragAndDropController);
mTransitions = transitions;
mTransactionPool = transactionPool;
mIconProvider = iconProvider;
@@ -289,7 +290,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
// TODO: Multi-display
mStageCoordinator = createStageCoordinator();
}
- mDragAndDropController.setSplitScreenController(this);
+ mDragAndDropController.ifPresent(controller -> controller.setSplitScreenController(this));
}
protected StageCoordinator createStageCoordinator() {
@@ -332,11 +333,21 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
return mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED;
}
+ /** Get the split stage of task is under it. */
+ public @StageType int getStageOfTask(int taskId) {
+ return mStageCoordinator.getStageOfTask(taskId);
+ }
+
/** Check split is foreground and task is under split or not by taskId. */
public boolean isTaskInSplitScreenForeground(int taskId) {
return isTaskInSplitScreen(taskId) && isSplitScreenVisible();
}
+ /** Check whether the task is the single-top root or the root of one of the stages. */
+ public boolean isTaskRootOrStageRoot(int taskId) {
+ return mStageCoordinator.isRootOrStageRoot(taskId);
+ }
+
public @SplitPosition int getSplitPosition(int taskId) {
return mStageCoordinator.getSplitPosition(taskId);
}
@@ -359,6 +370,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
if (task == null) {
throw new IllegalArgumentException("Unknown taskId" + taskId);
}
+ if (isTaskInSplitScreen(taskId)) {
+ throw new IllegalArgumentException("taskId is in split" + taskId);
+ }
return mStageCoordinator.moveToStage(task, stagePosition, wct);
}
@@ -370,17 +384,35 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */);
}
- public void enterSplitScreen(int taskId, boolean leftOrTop) {
- enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction());
- }
-
+ /**
+ * Doing necessary window transaction for other transition handler need to enter split in
+ * transition.
+ */
public void prepareEnterSplitScreen(WindowContainerTransaction wct,
ActivityManager.RunningTaskInfo taskInfo, int startPosition) {
- mStageCoordinator.prepareEnterSplitScreen(wct, taskInfo, startPosition);
+ mStageCoordinator.prepareEnterSplitScreen(wct, taskInfo, startPosition,
+ false /* resizeAnim */);
}
- public void finishEnterSplitScreen(SurfaceControl.Transaction t) {
- mStageCoordinator.finishEnterSplitScreen(t);
+ /**
+ * Doing necessary surface transaction for other transition handler need to enter split in
+ * transition when finished.
+ */
+ public void finishEnterSplitScreen(SurfaceControl.Transaction finishT) {
+ mStageCoordinator.finishEnterSplitScreen(finishT);
+ }
+
+ /**
+ * Doing necessary window transaction for other transition handler need to exit split in
+ * transition.
+ */
+ public void prepareExitSplitScreen(WindowContainerTransaction wct,
+ @StageType int stageToTop) {
+ mStageCoordinator.prepareExitSplitScreen(stageToTop, wct);
+ }
+
+ public void enterSplitScreen(int taskId, boolean leftOrTop) {
+ enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction());
}
public void enterSplitScreen(int taskId, boolean leftOrTop, WindowContainerTransaction wct) {
@@ -457,7 +489,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
}
@Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ public void onAnimationCancelled() {
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
mStageCoordinator.prepareEvictInvisibleChildTasks(evictWct);
mSyncQueue.queue(evictWct);
@@ -494,7 +526,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
if (options == null) options = new Bundle();
final ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
- if (samePackage(packageName, getPackageName(reverseSplitPosition(position)))) {
+ if (samePackage(packageName, getPackageName(reverseSplitPosition(position)),
+ user.getIdentifier(), getUserId(reverseSplitPosition(position)))) {
if (supportMultiInstancesSplit(packageName)) {
activityOptions.setApplyMultipleTaskFlagForShortcut(true);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -523,7 +556,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
final String packageName1 = shortcutInfo.getPackage();
final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
- if (samePackage(packageName1, packageName2)) {
+ final int userId1 = shortcutInfo.getUserId();
+ final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
+ if (samePackage(packageName1, packageName2, userId1, userId2)) {
if (supportMultiInstancesSplit(shortcutInfo.getPackage())) {
activityOptions.setApplyMultipleTaskFlagForShortcut(true);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -541,24 +576,55 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
instanceId);
}
+ void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
+ int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+ float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ if (options1 == null) options1 = new Bundle();
+ final ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+ final String packageName1 = shortcutInfo.getPackage();
+ // NOTE: This doesn't correctly pull out packageName2 if taskId is referring to a task in
+ // recents that hasn't launched and is not being organized
+ final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
+ final int userId1 = shortcutInfo.getUserId();
+ final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
+ if (samePackage(packageName1, packageName2, userId1, userId2)) {
+ if (supportMultiInstancesSplit(packageName1)) {
+ activityOptions.setApplyMultipleTaskFlagForShortcut(true);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else {
+ if (mRecentTasksOptional.isPresent()) {
+ mRecentTasksOptional.get().removeSplitPair(taskId);
+ }
+ taskId = INVALID_TASK_ID;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
+ Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ mStageCoordinator.startShortcutAndTask(shortcutInfo, options1, taskId, options2,
+ splitPosition, splitRatio, remoteTransition, instanceId);
+ }
+
/**
- * See {@link #startIntent(PendingIntent, Intent, int, Bundle)}
+ * See {@link #startIntent(PendingIntent, int, Intent, int, Bundle)}
* @param instanceId to be used by {@link SplitscreenEventLogger}
*/
- public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
+ public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) {
mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER);
- startIntent(intent, fillInIntent, position, options);
+ startIntent(intent, userId, fillInIntent, position, options);
}
- private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
+ private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1,
@Nullable Bundle options1, int taskId, @Nullable Bundle options2,
@SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
InstanceId instanceId) {
Intent fillInIntent = null;
final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
- if (samePackage(packageName1, packageName2)) {
+ final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
+ if (samePackage(packageName1, packageName2, userId1, userId2)) {
if (supportMultiInstancesSplit(packageName1)) {
fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
@@ -575,18 +641,26 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
}
- private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
- int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
- float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ private void startIntentAndTask(PendingIntent pendingIntent, int userId1,
+ @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+ @SplitPosition int splitPosition, float splitRatio,
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
Intent fillInIntent = null;
final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
+ // NOTE: This doesn't correctly pull out packageName2 if taskId is referring to a task in
+ // recents that hasn't launched and is not being organized
final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
- if (samePackage(packageName1, packageName2)) {
+ final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
+ if (samePackage(packageName1, packageName2, userId1, userId2)) {
if (supportMultiInstancesSplit(packageName1)) {
fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
} else {
+ if (mRecentTasksOptional.isPresent()) {
+ mRecentTasksOptional.get().removeSplitPair(taskId);
+ }
+ taskId = INVALID_TASK_ID;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
"Cancel entering split as not supporting multi-instances");
Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
@@ -597,16 +671,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
options2, splitPosition, splitRatio, remoteTransition, instanceId);
}
- private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1,
+ private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1,
@Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
- PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2,
+ PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2,
@Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
RemoteAnimationAdapter adapter, InstanceId instanceId) {
Intent fillInIntent1 = null;
Intent fillInIntent2 = null;
final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
- if (samePackage(packageName1, packageName2)) {
+ if (samePackage(packageName1, packageName2, userId1, userId2)) {
if (supportMultiInstancesSplit(packageName1)) {
fillInIntent1 = new Intent();
fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
@@ -626,8 +700,37 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
splitPosition, splitRatio, adapter, instanceId);
}
+ private void startIntents(PendingIntent pendingIntent1, int userId1,
+ @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
+ PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2,
+ @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ Intent fillInIntent1 = null;
+ Intent fillInIntent2 = null;
+ final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
+ final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
+ 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);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else {
+ pendingIntent2 = null;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
+ Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ mStageCoordinator.startIntents(pendingIntent1, fillInIntent1, shortcutInfo1, options1,
+ pendingIntent2, fillInIntent2, shortcutInfo2, options2, splitPosition, splitRatio,
+ remoteTransition, instanceId);
+ }
+
@Override
- public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
+ public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options) {
// Flag this as a no-user-action launch to prevent sending user leaving event to the current
// top activity since it's going to be put into another side of the split. This prevents the
@@ -637,13 +740,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
final String packageName1 = SplitScreenUtils.getPackageName(intent);
final String packageName2 = getPackageName(reverseSplitPosition(position));
- if (SplitScreenUtils.samePackage(packageName1, packageName2)) {
+ final int userId2 = getUserId(reverseSplitPosition(position));
+ if (samePackage(packageName1, packageName2, userId1, userId2)) {
if (supportMultiInstancesSplit(packageName1)) {
// To prevent accumulating large number of instances in the background, reuse task
// in the background with priority.
final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
.map(recentTasks -> recentTasks.findTaskInBackground(
- intent.getIntent().getComponent()))
+ intent.getIntent().getComponent(), userId1))
.orElse(null);
if (taskInfo != null) {
startTask(taskInfo.taskId, position, options);
@@ -690,6 +794,24 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null;
}
+ /** Retrieve user id of a specific split position if split screen is activated, otherwise
+ * returns the user id of the top running task. */
+ private int getUserId(@SplitPosition int position) {
+ ActivityManager.RunningTaskInfo taskInfo;
+ if (isSplitScreenVisible()) {
+ taskInfo = getTaskInfo(position);
+ } else {
+ taskInfo = mRecentTasksOptional
+ .map(recentTasks -> recentTasks.getTopRunningTask())
+ .orElse(null);
+ if (!isValidToSplit(taskInfo)) {
+ return -1;
+ }
+ }
+
+ return taskInfo != null ? taskInfo.userId : -1;
+ }
+
@VisibleForTesting
boolean supportMultiInstancesSplit(String packageName) {
if (packageName != null) {
@@ -998,14 +1120,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
- public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
+ public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1,
Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio,
RemoteAnimationAdapter adapter, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController,
"startIntentAndTaskWithLegacyTransition", (controller) ->
controller.startIntentAndTaskWithLegacyTransition(pendingIntent,
- options1, taskId, options2, splitPosition, splitRatio, adapter,
- instanceId));
+ userId1, options1, taskId, options2, splitPosition, splitRatio,
+ adapter, instanceId));
}
@Override
@@ -1031,13 +1153,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
- public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
- int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
- float splitRatio, @Nullable RemoteTransition remoteTransition,
- InstanceId instanceId) {
+ public void startIntentAndTask(PendingIntent pendingIntent, int userId1,
+ @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+ @SplitPosition int splitPosition, float splitRatio,
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startIntentAndTask",
- (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId,
- options2, splitPosition, splitRatio, remoteTransition, instanceId));
+ (controller) -> controller.startIntentAndTask(pendingIntent, userId1, options1,
+ taskId, options2, splitPosition, splitRatio, remoteTransition,
+ instanceId));
}
@Override
@@ -1046,31 +1169,36 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
float splitRatio, @Nullable RemoteTransition remoteTransition,
InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startShortcutAndTask",
- (controller) -> controller.mStageCoordinator.startShortcutAndTask(shortcutInfo,
- options1, taskId, options2, splitPosition, splitRatio, remoteTransition,
- instanceId));
+ (controller) -> controller.startShortcutAndTask(shortcutInfo, options1, taskId,
+ options2, splitPosition, splitRatio, remoteTransition, instanceId));
}
@Override
- public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1,
+ public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1,
@Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
- PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2,
+ PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2,
@Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
RemoteAnimationAdapter adapter, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startIntentsWithLegacyTransition",
(controller) ->
- controller.startIntentsWithLegacyTransition(pendingIntent1, shortcutInfo1,
- options1, pendingIntent2, shortcutInfo2, options2, splitPosition,
- splitRatio, adapter, instanceId)
+ controller.startIntentsWithLegacyTransition(pendingIntent1, userId1,
+ shortcutInfo1, options1, pendingIntent2, userId2, shortcutInfo2,
+ options2, splitPosition, splitRatio, adapter, instanceId)
);
}
@Override
- public void startIntents(PendingIntent pendingIntent1, @Nullable Bundle options1,
- PendingIntent pendingIntent2, @Nullable Bundle options2,
- @SplitPosition int splitPosition, float splitRatio,
+ public void startIntents(PendingIntent pendingIntent1, int userId1,
+ @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
+ PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2,
+ @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
@Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
- // TODO(b/259368992): To be implemented.
+ executeRemoteCallWithTaskPermission(mController, "startIntents",
+ (controller) ->
+ controller.startIntents(pendingIntent1, userId1, shortcutInfo1,
+ options1, pendingIntent2, userId2, shortcutInfo2, options2,
+ splitPosition, splitRatio, remoteTransition, instanceId)
+ );
}
@Override
@@ -1082,11 +1210,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
- public void startIntent(PendingIntent intent, Intent fillInIntent, int position,
+ public void startIntent(PendingIntent intent, int userId, Intent fillInIntent, int position,
@Nullable Bundle options, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startIntent",
- (controller) -> controller.startIntent(intent, fillInIntent, position, options,
- instanceId));
+ (controller) -> controller.startIntent(intent, userId, fillInIntent, position,
+ options, instanceId));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 1cf3a896b68e..d21f8a48e62a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -20,22 +20,22 @@ import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
+import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION;
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
-import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
-import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.graphics.Rect;
import android.os.IBinder;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -51,6 +51,7 @@ import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.transition.OneShotRemoteHandler;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
@@ -62,13 +63,11 @@ class SplitScreenTransitions {
private final Transitions mTransitions;
private final Runnable mOnFinish;
- DismissTransition mPendingDismiss = null;
- TransitSession mPendingEnter = null;
- TransitSession mPendingRecent = null;
+ DismissSession mPendingDismiss = null;
+ EnterSession mPendingEnter = null;
TransitSession mPendingResize = null;
private IBinder mAnimatingTransition = null;
- OneShotRemoteHandler mPendingRemoteHandler = null;
private OneShotRemoteHandler mActiveRemoteHandler = null;
private final Transitions.TransitionFinishCallback mRemoteFinishCB = this::onFinish;
@@ -88,42 +87,55 @@ class SplitScreenTransitions {
mStageCoordinator = stageCoordinator;
}
+ private void initTransition(@NonNull IBinder transition,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ mAnimatingTransition = transition;
+ mFinishTransaction = finishTransaction;
+ mFinishCallback = finishCallback;
+ }
+
+ /** Play animation for enter transition or dismiss transition. */
void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
@NonNull WindowContainerToken topRoot) {
- mFinishCallback = finishCallback;
- mAnimatingTransition = transition;
- mFinishTransaction = finishTransaction;
- if (mPendingRemoteHandler != null) {
- mPendingRemoteHandler.startAnimation(transition, info, startTransaction,
- finishTransaction, mRemoteFinishCB);
- mActiveRemoteHandler = mPendingRemoteHandler;
- mPendingRemoteHandler = null;
- return;
+ initTransition(transition, finishTransaction, finishCallback);
+
+ final TransitSession pendingTransition = getPendingTransition(transition);
+ if (pendingTransition != null) {
+ if (pendingTransition.mCanceled) {
+ // The pending transition was canceled, so skip playing animation.
+ startTransaction.apply();
+ onFinish(null /* wct */, null /* wctCB */);
+ return;
+ }
+
+ if (pendingTransition.mRemoteHandler != null) {
+ pendingTransition.mRemoteHandler.startAnimation(transition, info, startTransaction,
+ finishTransaction, mRemoteFinishCB);
+ mActiveRemoteHandler = pendingTransition.mRemoteHandler;
+ return;
+ }
}
+
playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot, topRoot);
}
+ /** Internal funcation of playAnimation. */
private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
@NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) {
- final TransitSession pendingTransition = getPendingTransition(transition);
- if (pendingTransition != null && pendingTransition.mCanceled) {
- // The pending transition was canceled, so skip playing animation.
- t.apply();
- onFinish(null /* wct */, null /* wctCB */);
- return;
- }
-
// Play some place-holder fade animations
+ final boolean isEnter = isPendingEnter(transition);
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
final SurfaceControl leash = change.getLeash();
final int mode = info.getChanges().get(i).getMode();
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
if (mode == TRANSIT_CHANGE) {
if (change.getParent() != null) {
// This is probably reparented, so we want the parent to be immediately visible
@@ -132,62 +144,123 @@ class SplitScreenTransitions {
t.setAlpha(parentChange.getLeash(), 1.f);
// and then animate this layer outside the parent (since, for example, this is
// the home task animating from fullscreen to part-screen).
- t.reparent(leash, info.getRootLeash());
- t.setLayer(leash, info.getChanges().size() - i);
+ t.reparent(parentChange.getLeash(), info.getRoot(rootIdx).getLeash());
+ t.setLayer(parentChange.getLeash(), info.getChanges().size() - i);
// build the finish reparent/reposition
mFinishTransaction.reparent(leash, parentChange.getLeash());
mFinishTransaction.setPosition(leash,
change.getEndRelOffset().x, change.getEndRelOffset().y);
}
- // TODO(shell-transitions): screenshot here
- final Rect startBounds = new Rect(change.getStartAbsBounds());
- final Rect endBounds = new Rect(change.getEndAbsBounds());
- startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
- startExampleResizeAnimation(leash, startBounds, endBounds);
- }
- boolean isRootOrSplitSideRoot = change.getParent() == null
- || topRoot.equals(change.getParent());
- // For enter or exit, we only want to animate the side roots but not the top-root.
- if (!isRootOrSplitSideRoot || topRoot.equals(change.getContainer())) {
- continue;
}
- if (isPendingEnter(transition) && (mainRoot.equals(change.getContainer())
- || sideRoot.equals(change.getContainer()))) {
+ final boolean isTopRoot = topRoot.equals(change.getContainer());
+ final boolean isMainRoot = mainRoot.equals(change.getContainer());
+ final boolean isSideRoot = sideRoot.equals(change.getContainer());
+ final boolean isDivider = change.getFlags() == FLAG_IS_DIVIDER_BAR;
+ final boolean isMainChild = mainRoot.equals(change.getParent());
+ final boolean isSideChild = sideRoot.equals(change.getParent());
+ if (isEnter && (isMainChild || isSideChild)) {
+ // Reset child tasks bounds on finish.
+ mFinishTransaction.setPosition(leash,
+ change.getEndRelOffset().x, change.getEndRelOffset().y);
+ mFinishTransaction.setCrop(leash, null);
+ } else if (isTopRoot) {
+ // Ensure top root is visible at start.
+ t.setAlpha(leash, 1.f);
+ t.show(leash);
+ } else if (isEnter && isMainRoot || isSideRoot) {
t.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top);
t.setWindowCrop(leash, change.getEndAbsBounds().width(),
change.getEndAbsBounds().height());
+ } else if (isDivider) {
+ t.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top);
+ t.setLayer(leash, Integer.MAX_VALUE);
+ t.show(leash);
+ }
+
+ // We want to use child tasks to animate so ignore split root container and non task
+ // except divider change.
+ if (isTopRoot || isMainRoot || isSideRoot
+ || (change.getTaskInfo() == null && !isDivider)) {
+ continue;
+ }
+ if (isEnter && mPendingEnter.mResizeAnim) {
+ // We will run animation in next transition so skip anim here
+ continue;
+ } else if (isPendingDismiss(transition)
+ && mPendingDismiss.mReason == EXIT_REASON_DRAG_DIVIDER) {
+ // TODO(b/280020345): need to refine animation for this but just skip anim now.
+ continue;
}
- boolean isOpening = isOpeningTransition(info);
- if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
- // fade in
- startExampleAnimation(leash, true /* show */);
- } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
+
+ // Because cross fade might be looked more flicker during animation
+ // (surface become black in middle of animation), we only do fade-out
+ // and show opening surface directly.
+ boolean isOpening = TransitionUtil.isOpeningType(info.getType());
+ if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
// fade out
- if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
- // Dismissing via snap-to-top/bottom means that the dismissed task is already
- // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
- // and don't animate it so it doesn't pop-in when reparented.
- t.setAlpha(leash, 0.f);
- } else {
- startExampleAnimation(leash, false /* show */);
- }
+ startFadeAnimation(leash, false /* show */);
+ } else if (mode == TRANSIT_CHANGE && change.getSnapshot() != null) {
+ t.reparent(change.getSnapshot(), info.getRoot(rootIdx).getLeash());
+ // Ensure snapshot it on the top of all transition surfaces
+ t.setLayer(change.getSnapshot(), info.getChanges().size() + 1);
+ t.setPosition(change.getSnapshot(), change.getStartAbsBounds().left,
+ change.getStartAbsBounds().top);
+ t.show(change.getSnapshot());
+ startFadeAnimation(change.getSnapshot(), false /* show */);
}
}
t.apply();
onFinish(null /* wct */, null /* wctCB */);
}
- void applyResizeTransition(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ /** Play animation for drag divider dismiss transition. */
+ void playDragDismissAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull WindowContainerToken toTopRoot, @NonNull SplitDecorManager toTopDecor,
+ @NonNull WindowContainerToken topRoot) {
+ initTransition(transition, finishTransaction, finishCallback);
+
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ final SurfaceControl leash = change.getLeash();
+
+ if (toTopRoot.equals(change.getContainer())) {
+ startTransaction.setAlpha(leash, 1.f);
+ startTransaction.show(leash);
+
+ ValueAnimator va = new ValueAnimator();
+ mAnimations.add(va);
+
+ toTopDecor.onResized(startTransaction, animated -> {
+ mAnimations.remove(va);
+ if (animated) {
+ mTransitions.getMainExecutor().execute(() -> {
+ onFinish(null /* wct */, null /* wctCB */);
+ });
+ }
+ });
+ } else if (topRoot.equals(change.getContainer())) {
+ // Ensure it on top of all changes in transition.
+ startTransaction.setLayer(leash, Integer.MAX_VALUE);
+ startTransaction.setAlpha(leash, 1.f);
+ startTransaction.show(leash);
+ }
+ }
+ startTransaction.apply();
+ onFinish(null /* wct */, null /* wctCB */);
+ }
+
+ /** Play animation for resize transition. */
+ void playResizeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
@NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) {
- mFinishCallback = finishCallback;
- mAnimatingTransition = transition;
- mFinishTransaction = finishTransaction;
+ initTransition(transition, finishTransaction, finishCallback);
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
@@ -200,14 +273,19 @@ class SplitScreenTransitions {
SplitDecorManager decor = mainRoot.equals(change.getContainer())
? mainDecor : sideDecor;
+
+ // This is to ensure onFinished be called after all animations ended.
ValueAnimator va = new ValueAnimator();
mAnimations.add(va);
+
decor.setScreenshotIfNeeded(change.getSnapshot(), startTransaction);
- decor.onResized(startTransaction, () -> {
- mTransitions.getMainExecutor().execute(() -> {
- mAnimations.remove(va);
- onFinish(null /* wct */, null /* wctCB */);
- });
+ decor.onResized(startTransaction, animated -> {
+ mAnimations.remove(va);
+ if (animated) {
+ mTransitions.getMainExecutor().execute(() -> {
+ onFinish(null /* wct */, null /* wctCB */);
+ });
+ }
});
}
}
@@ -224,10 +302,6 @@ class SplitScreenTransitions {
return mPendingEnter != null && mPendingEnter.mTransition == transition;
}
- boolean isPendingRecent(IBinder transition) {
- return mPendingRecent != null && mPendingRecent.mTransition == transition;
- }
-
boolean isPendingDismiss(IBinder transition) {
return mPendingDismiss != null && mPendingDismiss.mTransition == transition;
}
@@ -240,8 +314,6 @@ class SplitScreenTransitions {
private TransitSession getPendingTransition(IBinder transition) {
if (isPendingEnter(transition)) {
return mPendingEnter;
- } else if (isPendingRecent(transition)) {
- return mPendingRecent;
} else if (isPendingDismiss(transition)) {
return mPendingDismiss;
} else if (isPendingResize(transition)) {
@@ -251,6 +323,12 @@ class SplitScreenTransitions {
return null;
}
+ void startFullscreenTransition(WindowContainerTransaction wct,
+ @Nullable RemoteTransition handler) {
+ mTransitions.startTransition(TRANSIT_OPEN, wct,
+ new OneShotRemoteHandler(mTransitions.getMainExecutor(), handler));
+ }
+
/** Starts a transition to enter split with a remote transition animator. */
IBinder startEnterTransition(
@@ -258,26 +336,23 @@ class SplitScreenTransitions {
WindowContainerTransaction wct,
@Nullable RemoteTransition remoteTransition,
Transitions.TransitionHandler handler,
- @Nullable TransitionConsumedCallback consumedCallback,
- @Nullable TransitionFinishedCallback finishedCallback) {
+ int extraTransitType, boolean resizeAnim) {
+ if (mPendingEnter != null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ + " skip to start enter split transition since it already exist. ");
+ return null;
+ }
final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
- setEnterTransition(transition, remoteTransition, consumedCallback, finishedCallback);
+ setEnterTransition(transition, remoteTransition, extraTransitType, resizeAnim);
return transition;
}
/** Sets a transition to enter split. */
void setEnterTransition(@NonNull IBinder transition,
@Nullable RemoteTransition remoteTransition,
- @Nullable TransitionConsumedCallback consumedCallback,
- @Nullable TransitionFinishedCallback finishedCallback) {
- mPendingEnter = new TransitSession(transition, consumedCallback, finishedCallback);
-
- if (remoteTransition != null) {
- // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
- mPendingRemoteHandler = new OneShotRemoteHandler(
- mTransitions.getMainExecutor(), remoteTransition);
- mPendingRemoteHandler.setTransition(transition);
- }
+ int extraTransitType, boolean resizeAnim) {
+ mPendingEnter = new EnterSession(
+ transition, remoteTransition, extraTransitType, resizeAnim);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ " deduced Enter split screen");
@@ -287,6 +362,12 @@ class SplitScreenTransitions {
IBinder startDismissTransition(WindowContainerTransaction wct,
Transitions.TransitionHandler handler, @SplitScreen.StageType int dismissTop,
@SplitScreenController.ExitReason int reason) {
+ if (mPendingDismiss != null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ + " skip to start dismiss split transition since it already exist. reason to "
+ + " dismiss = %s", exitReasonToString(reason));
+ return null;
+ }
final int type = reason == EXIT_REASON_DRAG_DIVIDER
? TRANSIT_SPLIT_DISMISS_SNAP : TRANSIT_SPLIT_DISMISS;
IBinder transition = mTransitions.startTransition(type, wct, handler);
@@ -297,7 +378,7 @@ class SplitScreenTransitions {
/** Sets a transition to dismiss split. */
void setDismissTransition(@NonNull IBinder transition, @SplitScreen.StageType int dismissTop,
@SplitScreenController.ExitReason int reason) {
- mPendingDismiss = new DismissTransition(transition, reason, dismissTop);
+ mPendingDismiss = new DismissSession(transition, reason, dismissTop);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ " deduced Dismiss due to %s. toTop=%s",
@@ -307,6 +388,12 @@ class SplitScreenTransitions {
IBinder startResizeTransition(WindowContainerTransaction wct,
Transitions.TransitionHandler handler,
@Nullable TransitionFinishedCallback finishCallback) {
+ if (mPendingResize != null) {
+ mPendingResize.cancel(null);
+ mAnimations.clear();
+ onFinish(null /* wct */, null /* wctCB */);
+ }
+
IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler);
setResizeTransition(transition, finishCallback);
return transition;
@@ -319,32 +406,10 @@ class SplitScreenTransitions {
+ " deduced Resize split screen");
}
- void setRecentTransition(@NonNull IBinder transition,
- @Nullable RemoteTransition remoteTransition,
- @Nullable TransitionFinishedCallback finishCallback) {
- mPendingRecent = new TransitSession(transition, null /* consumedCb */, finishCallback);
-
- if (remoteTransition != null) {
- // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
- mPendingRemoteHandler = new OneShotRemoteHandler(
- mTransitions.getMainExecutor(), remoteTransition);
- mPendingRemoteHandler.setTransition(transition);
- }
-
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
- + " deduced Enter recent panel");
- }
-
void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t,
IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) {
if (mergeTarget != mAnimatingTransition) return;
- if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) {
- // Since there's an entering transition merged, recent transition no longer
- // need to handle entering split screen after the transition finished.
- mPendingRecent.setFinishedCallback(null);
- }
-
if (mActiveRemoteHandler != null) {
mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
} else {
@@ -372,19 +437,13 @@ class SplitScreenTransitions {
// An entering transition got merged, appends the rest operations to finish entering
// split screen.
mStageCoordinator.finishEnterSplitScreen(finishT);
- mPendingRemoteHandler = null;
}
mPendingEnter.onConsumed(aborted);
mPendingEnter = null;
- mPendingRemoteHandler = null;
} else if (isPendingDismiss(transition)) {
mPendingDismiss.onConsumed(aborted);
mPendingDismiss = null;
- } else if (isPendingRecent(transition)) {
- mPendingRecent.onConsumed(aborted);
- mPendingRecent = null;
- mPendingRemoteHandler = null;
} else if (isPendingResize(transition)) {
mPendingResize.onConsumed(aborted);
mPendingResize = null;
@@ -398,9 +457,6 @@ class SplitScreenTransitions {
if (isPendingEnter(mAnimatingTransition)) {
mPendingEnter.onFinished(wct, mFinishTransaction);
mPendingEnter = null;
- } else if (isPendingRecent(mAnimatingTransition)) {
- mPendingRecent.onFinished(wct, mFinishTransaction);
- mPendingRecent = null;
} else if (isPendingDismiss(mAnimatingTransition)) {
mPendingDismiss.onFinished(wct, mFinishTransaction);
mPendingDismiss = null;
@@ -409,7 +465,6 @@ class SplitScreenTransitions {
mPendingResize = null;
}
- mPendingRemoteHandler = null;
mActiveRemoteHandler = null;
mAnimatingTransition = null;
@@ -420,90 +475,34 @@ class SplitScreenTransitions {
}
}
- // TODO(shell-transitions): real animations
- private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
+ private void startFadeAnimation(@NonNull SurfaceControl leash, boolean show) {
final float end = show ? 1.f : 0.f;
final float start = 1.f - end;
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
final ValueAnimator va = ValueAnimator.ofFloat(start, end);
- va.setDuration(500);
+ va.setDuration(FADE_DURATION);
+ va.setInterpolator(show ? ALPHA_IN : ALPHA_OUT);
va.addUpdateListener(animation -> {
float fraction = animation.getAnimatedFraction();
transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
transaction.apply();
});
- final Runnable finisher = () -> {
- transaction.setAlpha(leash, end);
- transaction.apply();
- mTransactionPool.release(transaction);
- mTransitions.getMainExecutor().execute(() -> {
- mAnimations.remove(va);
- onFinish(null /* wct */, null /* wctCB */);
- });
- };
- va.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- finisher.run();
- }
- });
- mAnimations.add(va);
- mTransitions.getAnimExecutor().execute(va::start);
- }
-
- // TODO(shell-transitions): real animations
- private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
- @NonNull Rect startBounds, @NonNull Rect endBounds) {
- final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
- va.setDuration(500);
- va.addUpdateListener(animation -> {
- float fraction = animation.getAnimatedFraction();
- transaction.setWindowCrop(leash,
- (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
- (int) (startBounds.height() * (1.f - fraction)
- + endBounds.height() * fraction));
- transaction.setPosition(leash,
- startBounds.left * (1.f - fraction) + endBounds.left * fraction,
- startBounds.top * (1.f - fraction) + endBounds.top * fraction);
- transaction.apply();
- });
- final Runnable finisher = () -> {
- transaction.setWindowCrop(leash, 0, 0);
- transaction.setPosition(leash, endBounds.left, endBounds.top);
- transaction.apply();
- mTransactionPool.release(transaction);
- mTransitions.getMainExecutor().execute(() -> {
- mAnimations.remove(va);
- onFinish(null /* wct */, null /* wctCB */);
- });
- };
va.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- finisher.run();
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- finisher.run();
+ transaction.setAlpha(leash, end);
+ transaction.apply();
+ mTransactionPool.release(transaction);
+ mTransitions.getMainExecutor().execute(() -> {
+ mAnimations.remove(va);
+ onFinish(null /* wct */, null /* wctCB */);
+ });
}
});
mAnimations.add(va);
mTransitions.getAnimExecutor().execute(va::start);
}
- private boolean isOpeningTransition(TransitionInfo info) {
- return Transitions.isOpeningType(info.getType())
- || info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE
- || info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
- }
-
/** Calls when the transition got consumed. */
interface TransitionConsumedCallback {
void onConsumed(boolean aborted);
@@ -515,21 +514,40 @@ class SplitScreenTransitions {
}
/** Session for a transition and its clean-up callback. */
- static class TransitSession {
+ class TransitSession {
final IBinder mTransition;
TransitionConsumedCallback mConsumedCallback;
TransitionFinishedCallback mFinishedCallback;
+ OneShotRemoteHandler mRemoteHandler;
/** Whether the transition was canceled. */
boolean mCanceled;
+ /** A note for extra transit type, to help indicate custom transition. */
+ final int mExtraTransitType;
+
TransitSession(IBinder transition,
@Nullable TransitionConsumedCallback consumedCallback,
@Nullable TransitionFinishedCallback finishedCallback) {
+ this(transition, consumedCallback, finishedCallback, null /* remoteTransition */, 0);
+ }
+
+ TransitSession(IBinder transition,
+ @Nullable TransitionConsumedCallback consumedCallback,
+ @Nullable TransitionFinishedCallback finishedCallback,
+ @Nullable RemoteTransition remoteTransition, int extraTransitType) {
mTransition = transition;
mConsumedCallback = consumedCallback;
mFinishedCallback = finishedCallback;
+ if (remoteTransition != null) {
+ // Wrapping the remote transition for ease-of-use. (OneShot handles all the binder
+ // linking/death stuff)
+ mRemoteHandler = new OneShotRemoteHandler(
+ mTransitions.getMainExecutor(), remoteTransition);
+ mRemoteHandler.setTransition(transition);
+ }
+ mExtraTransitType = extraTransitType;
}
/** Sets transition consumed callback. */
@@ -567,12 +585,25 @@ class SplitScreenTransitions {
}
}
+ /** Bundled information of enter transition. */
+ class EnterSession extends TransitSession {
+ final boolean mResizeAnim;
+
+ EnterSession(IBinder transition,
+ @Nullable RemoteTransition remoteTransition,
+ int extraTransitType, boolean resizeAnim) {
+ super(transition, null /* consumedCallback */, null /* finishedCallback */,
+ remoteTransition, extraTransitType);
+ this.mResizeAnim = resizeAnim;
+ }
+ }
+
/** Bundled information of dismiss transition. */
- static class DismissTransition extends TransitSession {
+ class DismissSession extends TransitSession {
final int mReason;
final @SplitScreen.StageType int mDismissTop;
- DismissTransition(IBinder transition, int reason, int dismissTop) {
+ DismissSession(IBinder transition, int reason, int dismissTop) {
super(transition, null /* consumedCallback */, null /* finishedCallback */);
this.mReason = reason;
this.mDismissTop = dismissTop;
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 77939c7c6964..7699e2f7c13d 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
@@ -20,7 +20,6 @@ import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED;
import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
@@ -40,6 +39,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIV
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
@@ -61,8 +61,8 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonT
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
-import static com.android.wm.shell.transition.Transitions.isClosingType;
-import static com.android.wm.shell.transition.Transitions.isOpeningType;
+import static com.android.wm.shell.util.TransitionUtil.isClosingType;
+import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -90,6 +90,9 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IntArray;
import android.util.Log;
import android.util.Slog;
import android.view.Choreographer;
@@ -118,7 +121,6 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -135,6 +137,7 @@ import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.LegacyTransitions;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.util.SplitBounds;
+import com.android.wm.shell.util.TransitionUtil;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -166,7 +169,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private final StageListenerImpl mMainStageListener = new StageListenerImpl();
private final SideStage mSideStage;
private final StageListenerImpl mSideStageListener = new StageListenerImpl();
- private final DisplayLayout mDisplayLayout;
@SplitPosition
private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -187,6 +189,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private final SplitScreenTransitions mSplitTransitions;
private final SplitscreenEventLogger mLogger;
private final ShellExecutor mMainExecutor;
+ // Cache live tile tasks while entering recents, evict them from stages in finish transaction
+ // if user is opening another task(s).
+ private final ArrayList<Integer> mPausingTasks = new ArrayList<>();
private final Optional<RecentTasksController> mRecentTasks;
private final Rect mTempRect1 = new Rect();
@@ -259,37 +264,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
};
- private final SplitScreenTransitions.TransitionFinishedCallback
- mRecentTransitionFinishedCallback =
- new SplitScreenTransitions.TransitionFinishedCallback() {
- @Override
- public void onFinished(WindowContainerTransaction finishWct,
- SurfaceControl.Transaction finishT) {
- // Check if the recent transition is finished by returning to the current
- // split, so we
- // can restore the divider bar.
- for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
- final WindowContainerTransaction.HierarchyOp op =
- finishWct.getHierarchyOps().get(i);
- final IBinder container = op.getContainer();
- if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
- && (mMainStage.containsContainer(container)
- || mSideStage.containsContainer(container))) {
- updateSurfaceBounds(mSplitLayout, finishT,
- false /* applyResizingOffset */);
- setDividerVisibility(true, finishT);
- return;
- }
- }
-
- // Dismiss the split screen if it's not returning to split.
- prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct);
- setSplitsVisible(false);
- setDividerVisibility(false, finishT);
- logExit(EXIT_REASON_UNKNOWN);
- }
- };
-
protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
ShellTaskOrganizer taskOrganizer, DisplayController displayController,
DisplayImeController displayImeController,
@@ -334,10 +308,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
this::onTransitionAnimationComplete, this);
mDisplayController.addDisplayWindowListener(this);
- mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId));
transitions.addHandler(this);
mSplitUnsupportedToast = Toast.makeText(mContext,
R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
+ // With shell transition, we should update recents tile each callback so set this to true by
+ // default.
+ mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS;
}
@VisibleForTesting
@@ -365,7 +341,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mMainExecutor = mainExecutor;
mRecentTasks = recentTasks;
mDisplayController.addDisplayWindowListener(this);
- mDisplayLayout = new DisplayLayout();
transitions.addHandler(this);
mSplitUnsupportedToast = Toast.makeText(mContext,
R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
@@ -388,6 +363,32 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return mMainStage.isActive();
}
+ /** @return whether the transition-request implies entering pip from split. */
+ public boolean requestImpliesSplitToPip(TransitionRequestInfo request) {
+ if (!isSplitActive() || !mMixedHandler.requestHasPipEnter(request)) {
+ return false;
+ }
+
+ if (request.getTriggerTask() != null && getSplitPosition(
+ request.getTriggerTask().taskId) != SPLIT_POSITION_UNDEFINED) {
+ return true;
+ }
+
+ // If one of the splitting tasks support auto-pip, wm-core might reparent the task to TDA
+ // and file a TRANSIT_PIP transition when finishing transitions.
+ // @see com.android.server.wm.RootWindowContainer#moveActivityToPinnedRootTask
+ if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Checks if `transition` is a pending enter-split transition. */
+ public boolean isPendingEnter(IBinder transition) {
+ return mSplitTransitions.isPendingEnter(transition);
+ }
+
@StageType
int getStageOfTask(int taskId) {
if (mMainStage.containsTask(taskId)) {
@@ -399,41 +400,28 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return STAGE_TYPE_UNDEFINED;
}
+ boolean isRootOrStageRoot(int taskId) {
+ if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskId) {
+ return true;
+ }
+ return mMainStage.isRootTaskId(taskId) || mSideStage.isRootTaskId(taskId);
+ }
+
boolean moveToStage(ActivityManager.RunningTaskInfo task, @SplitPosition int stagePosition,
WindowContainerTransaction wct) {
- StageTaskListener targetStage;
- int sideStagePosition;
- if (isSplitScreenVisible()) {
- // If the split screen is foreground, retrieves target stage based on position.
- targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage;
- sideStagePosition = mSideStagePosition;
+ prepareEnterSplitScreen(wct, task, stagePosition, false /* resizeAnim */);
+ if (ENABLE_SHELL_TRANSITIONS) {
+ mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct,
+ null, this,
+ isSplitScreenVisible()
+ ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN,
+ !mIsDropEntering);
} else {
- targetStage = mSideStage;
- sideStagePosition = stagePosition;
- }
-
- if (!isSplitActive()) {
- mSplitLayout.init();
- prepareEnterSplitScreen(wct, task, stagePosition);
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
});
- } else {
- setSideStagePosition(sideStagePosition, wct);
- targetStage.addTask(task, wct);
- targetStage.evictAllChildren(wct);
- if (!isSplitScreenVisible()) {
- final StageTaskListener anotherStage = targetStage == mMainStage
- ? mSideStage : mMainStage;
- anotherStage.reparentTopTask(wct);
- anotherStage.evictAllChildren(wct);
- wct.reorder(mRootTaskInfo.token, true);
- }
- setRootForceTranslucent(false, wct);
- mSyncQueue.queue(wct);
}
-
// Due to drag already pip task entering split by this method so need to reset flag here.
mIsDropEntering = false;
return true;
@@ -468,26 +456,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
RemoteAnimationTarget[] wallpapers,
RemoteAnimationTarget[] nonApps,
final IRemoteAnimationFinishedCallback finishedCallback) {
- boolean openingToSide = false;
- if (apps != null) {
- for (int i = 0; i < apps.length; ++i) {
- if (apps[i].mode == MODE_OPENING
- && mSideStage.containsTask(apps[i].taskId)) {
- openingToSide = true;
- break;
- }
- }
- } else if (mSideStage.getChildCount() != 0) {
- // There are chances the entering app transition got canceled by performing
- // rotation transition. Checks if there is any child task existed in split
- // screen before fallback to cancel entering flow.
- openingToSide = true;
- }
-
- if (isEnteringSplit && !openingToSide) {
+ if (isEnteringSplit && mSideStage.getChildCount() == 0) {
mMainExecutor.execute(() -> exitSplitScreen(
- mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
- EXIT_REASON_UNKNOWN));
+ null /* childrenToTop */, EXIT_REASON_UNKNOWN));
+ mSplitUnsupportedToast.show();
}
if (finishedCallback != null) {
@@ -505,7 +477,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
@Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ public void onAnimationCancelled() {
if (isEnteringSplit) {
mMainExecutor.execute(() -> exitSplitScreen(
mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
@@ -535,30 +507,29 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
/** Launches an activity into split. */
void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
@Nullable Bundle options) {
+ mSplitRequest = new SplitRequest(intent.getIntent(), position);
if (!ENABLE_SHELL_TRANSITIONS) {
startIntentLegacy(intent, fillInIntent, position, options);
return;
}
final WindowContainerTransaction wct = new WindowContainerTransaction();
- final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- prepareEvictChildTasks(position, evictWct);
-
options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
wct.sendPendingIntent(intent, fillInIntent, options);
+ // If this should be mixed, just send the intent to avoid split handle transition directly.
+ if (mMixedHandler != null && mMixedHandler.shouldSplitEnterMixed(intent)) {
+ mTaskOrganizer.applyTransaction(wct);
+ return;
+ }
+
// If split screen is not activated, we're expecting to open a pair of apps to split.
- final int transitType = mMainStage.isActive()
+ final int extraTransitType = mMainStage.isActive()
? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
- prepareEnterSplitScreen(wct, null /* taskInfo */, position);
+ prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering);
- mSplitTransitions.startEnterTransition(transitType, wct, null, this,
- null /* consumedCallback */,
- (finishWct, finishT) -> {
- if (!evictWct.isEmpty()) {
- finishWct.merge(evictWct, true);
- }
- } /* finishedCallback */);
+ mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this,
+ extraTransitType, !mIsDropEntering);
}
/** Launches an activity into split by legacy transition. */
@@ -572,26 +543,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback,
SurfaceControl.Transaction t) {
- boolean openingToSide = false;
- if (apps != null) {
- for (int i = 0; i < apps.length; ++i) {
- if (apps[i].mode == MODE_OPENING
- && mSideStage.containsTask(apps[i].taskId)) {
- openingToSide = true;
- break;
- }
- }
- } else if (mSideStage.getChildCount() != 0) {
- // There are chances the entering app transition got canceled by performing
- // rotation transition. Checks if there is any child task existed in split
- // screen before fallback to cancel entering flow.
- openingToSide = true;
- }
-
- if (isEnteringSplit && !openingToSide && apps != null) {
+ if (isEnteringSplit && mSideStage.getChildCount() == 0) {
mMainExecutor.execute(() -> exitSplitScreen(
- mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
- EXIT_REASON_UNKNOWN));
+ null /* childrenToTop */, EXIT_REASON_UNKNOWN));
+ mSplitUnsupportedToast.show();
}
if (apps != null) {
@@ -628,7 +583,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (isEnteringSplit && mLogger.isEnterRequestedByDrag()) {
updateWindowBounds(mSplitLayout, wct);
}
- mSplitRequest = new SplitRequest(intent.getIntent(), position);
wct.sendPendingIntent(intent, fillInIntent, options);
mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
}
@@ -638,6 +592,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
@Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (taskId2 == INVALID_TASK_ID) {
+ if (mMainStage.containsTask(taskId1) || mSideStage.containsTask(taskId1)) {
+ prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
+ }
+ if (mRecentTasks.isPresent()) {
+ mRecentTasks.get().removeSplitPair(taskId1);
+ }
+ options1 = options1 != null ? options1 : new Bundle();
+ addActivityOptions(options1, null);
+ wct.startTask(taskId1, options1);
+ mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ return;
+ }
+
setSideStagePosition(splitPosition, wct);
options1 = options1 != null ? options1 : new Bundle();
addActivityOptions(options1, mSideStage);
@@ -652,6 +620,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@SplitPosition int splitPosition, float splitRatio,
@Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (taskId == INVALID_TASK_ID) {
+ options1 = options1 != null ? options1 : new Bundle();
+ addActivityOptions(options1, null);
+ wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
+ mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ return;
+ }
+
setSideStagePosition(splitPosition, wct);
options1 = options1 != null ? options1 : new Bundle();
addActivityOptions(options1, mSideStage);
@@ -665,6 +641,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (taskId == INVALID_TASK_ID) {
+ options1 = options1 != null ? options1 : new Bundle();
+ addActivityOptions(options1, null);
+ wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
+ mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ return;
+ }
+
setSideStagePosition(splitPosition, wct);
options1 = options1 != null ? options1 : new Bundle();
addActivityOptions(options1, mSideStage);
@@ -683,10 +667,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void startWithTask(WindowContainerTransaction wct, int mainTaskId,
@Nullable Bundle mainOptions, float splitRatio,
@Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
- if (mMainStage.isActive()) {
- mMainStage.evictAllChildren(wct);
- mSideStage.evictAllChildren(wct);
- } else {
+ if (!mMainStage.isActive()) {
// Build a request WCT that will launch both apps such that task 0 is on the main stage
// while task 1 is on the side stage.
mMainStage.activate(wct, false /* reparent */);
@@ -703,8 +684,63 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Add task launch requests
wct.startTask(mainTaskId, mainOptions);
- mSplitTransitions.startEnterTransition(
- TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null, null);
+ // leave recents animation by re-start pausing tasks
+ if (mPausingTasks.contains(mainTaskId)) {
+ mPausingTasks.clear();
+ }
+ mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, remoteTransition, this,
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false);
+ setEnterInstanceId(instanceId);
+ }
+
+ void startIntents(PendingIntent pendingIntent1, Intent fillInIntent1,
+ @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
+ PendingIntent pendingIntent2, Intent fillInIntent2,
+ @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2,
+ @SplitPosition int splitPosition, float splitRatio,
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (pendingIntent2 == null) {
+ options1 = options1 != null ? options1 : new Bundle();
+ addActivityOptions(options1, null);
+ if (shortcutInfo1 != null) {
+ wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
+ } else {
+ wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+ }
+ mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ return;
+ }
+
+ if (!mMainStage.isActive()) {
+ // Build a request WCT that will launch both apps such that task 0 is on the main stage
+ // while task 1 is on the side stage.
+ mMainStage.activate(wct, false /* reparent */);
+ }
+
+ mSplitLayout.setDivideRatio(splitRatio);
+ updateWindowBounds(mSplitLayout, wct);
+ wct.reorder(mRootTaskInfo.token, true);
+ setRootForceTranslucent(false, wct);
+
+ setSideStagePosition(splitPosition, wct);
+ options1 = options1 != null ? options1 : new Bundle();
+ addActivityOptions(options1, mSideStage);
+ if (shortcutInfo1 != null) {
+ wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
+ } else {
+ wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+ }
+ options2 = options2 != null ? options2 : new Bundle();
+ addActivityOptions(options2, mMainStage);
+ if (shortcutInfo2 != null) {
+ wct.startShortcut(mContext.getPackageName(), shortcutInfo2, options2);
+ } else {
+ wct.sendPendingIntent(pendingIntent2, fillInIntent2, options2);
+ }
+
+ mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, remoteTransition, this,
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false);
setEnterInstanceId(instanceId);
}
@@ -813,7 +849,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
onRemoteAnimationFinished(apps);
t.apply();
try {
- adapter.getRunner().onAnimationCancelled(mKeyguardShowing);
+ adapter.getRunner().onAnimationCancelled();
} catch (RemoteException e) {
Slog.e(TAG, "Error starting remote animation", e);
}
@@ -921,10 +957,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSyncQueue.queue(wrapAsSplitRemoteAnimation(adapter), WindowManager.TRANSIT_OPEN, wct);
}
- mSyncQueue.runInSync(t -> {
- setDividerVisibility(true, t);
- });
-
setEnterInstanceId(instanceId);
}
@@ -961,10 +993,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
@Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ public void onAnimationCancelled() {
onRemoteAnimationFinishedOrCancelled(evictWct);
+ setDividerVisibility(true, null);
try {
- adapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
+ adapter.getRunner().onAnimationCancelled();
} catch (RemoteException e) {
Slog.e(TAG, "Error starting remote animation", e);
}
@@ -985,7 +1018,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
onRemoteAnimationFinished(apps);
t.apply();
try {
- adapter.getRunner().onAnimationCancelled(mKeyguardShowing);
+ adapter.getRunner().onAnimationCancelled();
} catch (RemoteException e) {
Slog.e(TAG, "Error starting remote animation", e);
}
@@ -1003,6 +1036,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
t.setPosition(apps[i].leash, 0, 0);
}
}
+ setDividerVisibility(true, t);
t.apply();
IRemoteAnimationFinishedCallback wrapCallback =
@@ -1034,7 +1068,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) {
mIsDividerRemoteAnimating = false;
mShouldUpdateRecents = true;
- mSplitRequest = null;
+ clearRequestIfPresented();
// If any stage has no child after animation finished, it means that split will display
// nothing, such status will happen if task and intent is same app but not support
// multi-instance, we should exit split and expand that app as full screen.
@@ -1054,7 +1088,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void onRemoteAnimationFinished(RemoteAnimationTarget[] apps) {
mIsDividerRemoteAnimating = false;
mShouldUpdateRecents = true;
- mSplitRequest = null;
+ clearRequestIfPresented();
// If any stage has no child after finished animation, that side of the split will display
// nothing. This might happen if starting the same app on the both sides while not
// supporting multi-instance. Exit the split screen and expand that app to full screen.
@@ -1066,24 +1100,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- prepareEvictNonOpeningChildTasks(SPLIT_POSITION_TOP_OR_LEFT, apps, evictWct);
- prepareEvictNonOpeningChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, apps, evictWct);
+ mMainStage.evictNonOpeningChildren(apps, evictWct);
+ mSideStage.evictNonOpeningChildren(apps, evictWct);
mSyncQueue.queue(evictWct);
}
-
- /**
- * Collects all the current child tasks of a specific split and prepares transaction to evict
- * them to display.
- */
- void prepareEvictChildTasks(@SplitPosition int position, WindowContainerTransaction wct) {
- if (position == mSideStagePosition) {
- mSideStage.evictAllChildren(wct);
- } else {
- mMainStage.evictAllChildren(wct);
- }
- }
-
void prepareEvictNonOpeningChildTasks(@SplitPosition int position, RemoteAnimationTarget[] apps,
WindowContainerTransaction wct) {
if (position == mSideStagePosition) {
@@ -1320,6 +1341,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
});
mShouldUpdateRecents = false;
mIsDividerRemoteAnimating = false;
+ mSplitRequest = null;
mSplitLayout.getInvisibleBounds(mTempRect1);
if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) {
@@ -1335,8 +1357,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mIsExiting = true;
childrenToTop.resetBounds(wct);
wct.reorder(childrenToTop.mRootTaskInfo.token, true);
- wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token,
- SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
}
wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
false /* reparentLeafTaskIfRelaunch */);
@@ -1412,6 +1432,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
+ private void clearRequestIfPresented() {
+ if (mSideStageListener.mVisible && mSideStageListener.mHasChildren
+ && mMainStageListener.mVisible && mSideStageListener.mHasChildren) {
+ mSplitRequest = null;
+ }
+ }
+
/**
* Returns whether the split pair in the recent tasks list should be broken.
*/
@@ -1443,7 +1470,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
* an existing WindowContainerTransaction (rather than applying immediately). This is intended
* to be used when exiting split might be bundled with other window operations.
*/
- private void prepareExitSplitScreen(@StageType int stageToTop,
+ void prepareExitSplitScreen(@StageType int stageToTop,
@NonNull WindowContainerTransaction wct) {
if (!mMainStage.isActive()) return;
mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
@@ -1451,7 +1478,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
private void prepareEnterSplitScreen(WindowContainerTransaction wct) {
- prepareEnterSplitScreen(wct, null /* taskInfo */, SPLIT_POSITION_UNDEFINED);
+ prepareEnterSplitScreen(wct, null /* taskInfo */, SPLIT_POSITION_UNDEFINED,
+ !mIsDropEntering);
}
/**
@@ -1459,27 +1487,87 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
* into side stage.
*/
void prepareEnterSplitScreen(WindowContainerTransaction wct,
- @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) {
- if (mMainStage.isActive()) return;
-
+ @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
+ boolean resizeAnim) {
onSplitScreenEnter();
+ if (isSplitActive()) {
+ prepareBringSplit(wct, taskInfo, startPosition, resizeAnim);
+ } else {
+ prepareActiveSplit(wct, taskInfo, startPosition, resizeAnim);
+ }
+ }
+
+ private void prepareBringSplit(WindowContainerTransaction wct,
+ @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
+ boolean resizeAnim) {
+ if (taskInfo != null) {
+ wct.startTask(taskInfo.taskId,
+ resolveStartStage(STAGE_TYPE_UNDEFINED, startPosition, null, wct));
+ }
+ // If running background, we need to reparent current top visible task to main stage.
+ if (!isSplitScreenVisible()) {
+ // Ensure to evict old splitting tasks because the new split pair might be composed by
+ // one of the splitting tasks, evicting the task when finishing entering transition
+ // won't guarantee to put the task to the indicated new position.
+ mMainStage.evictAllChildren(wct);
+ mMainStage.reparentTopTask(wct);
+ prepareSplitLayout(wct, resizeAnim);
+ }
+ }
+
+ private void prepareActiveSplit(WindowContainerTransaction wct,
+ @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
+ boolean resizeAnim) {
+ if (!ENABLE_SHELL_TRANSITIONS) {
+ // Legacy transition we need to create divider here, shell transition case we will
+ // create it on #finishEnterSplitScreen
+ mSplitLayout.init();
+ } else {
+ // We handle split visibility itself on shell transition, but sometimes we didn't
+ // reset it correctly after dismiss by some reason, so just set invisible before active.
+ setSplitsVisible(false);
+ }
if (taskInfo != null) {
setSideStagePosition(startPosition, wct);
mSideStage.addTask(taskInfo, wct);
}
mMainStage.activate(wct, true /* includingTopTask */);
+ prepareSplitLayout(wct, resizeAnim);
+ }
+
+ private void prepareSplitLayout(WindowContainerTransaction wct, boolean resizeAnim) {
+ if (resizeAnim) {
+ mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+ } else {
+ mSplitLayout.resetDividerPosition();
+ }
updateWindowBounds(mSplitLayout, wct);
+ if (resizeAnim) {
+ // Reset its smallest width dp to avoid is change layout before it actually resized to
+ // split bounds.
+ wct.setSmallestScreenWidthDp(mMainStage.mRootTaskInfo.token,
+ SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
+ }
wct.reorder(mRootTaskInfo.token, true);
setRootForceTranslucent(false, wct);
}
- void finishEnterSplitScreen(SurfaceControl.Transaction t) {
- mSplitLayout.init();
- setDividerVisibility(true, t);
- updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
- t.show(mRootTaskLeash);
+ void finishEnterSplitScreen(SurfaceControl.Transaction finishT) {
+ mSplitLayout.update(finishT);
+ mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash,
+ getMainStageBounds());
+ mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash,
+ getSideStageBounds());
+ setDividerVisibility(true, finishT);
+ // Ensure divider surface are re-parented back into the hierarchy at the end of the
+ // transition. See Transition#buildFinishTransaction for more detail.
+ finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
+
+ updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */);
+ finishT.show(mRootTaskLeash);
setSplitsVisible(true);
- mShouldUpdateRecents = true;
+ mIsDropEntering = false;
+ mSplitRequest = null;
updateRecentTasksSplitPair();
if (!mLogger.hasStartedSession()) {
mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
@@ -1572,7 +1660,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
mSplitLayout.isLandscape());
}
- if (present && visible) {
+ if (present) {
updateRecentTasksSplitPair();
}
@@ -1582,7 +1670,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
private void updateRecentTasksSplitPair() {
- if (!mShouldUpdateRecents) {
+ // Preventing from single task update while processing recents.
+ if (!mShouldUpdateRecents || !mPausingTasks.isEmpty()) {
return;
}
mRecentTasks.ifPresent(recentTasks -> {
@@ -1630,7 +1719,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (mSplitLayout == null) {
mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
mRootTaskInfo.configuration, this, mParentContainerCallbacks,
- mDisplayImeController, mTaskOrganizer,
+ mDisplayController, mDisplayImeController, mTaskOrganizer,
PARALLAX_ALIGN_CENTER /* parallaxType */);
mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
}
@@ -1702,22 +1791,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
});
}
- void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) {
+ /** Callback when split roots have child task appeared under it, this is a little different from
+ * #onStageHasChildrenChanged because this would be called every time child task appeared.
+ * NOTICE: This only be called on legacy transition. */
+ private void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) {
// Handle entering split screen while there is a split pair running in the background.
if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive()
&& mSplitRequest == null) {
- if (mIsDropEntering) {
- mSplitLayout.resetDividerPosition();
- } else {
- mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
- }
final WindowContainerTransaction wct = new WindowContainerTransaction();
- mMainStage.reparentTopTask(wct);
+ prepareEnterSplitScreen(wct);
mMainStage.evictAllChildren(wct);
mSideStage.evictOtherChildren(wct, taskId);
- updateWindowBounds(mSplitLayout, wct);
- wct.reorder(mRootTaskInfo.token, true);
- setRootForceTranslucent(false, wct);
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
@@ -1748,6 +1832,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
wct.setForceTranslucent(mRootTaskInfo.token, translucent);
}
+ /** Callback when split roots visiblility changed.
+ * NOTICE: This only be called on legacy transition. */
private void onStageVisibilityChanged(StageListenerImpl stageListener) {
// If split didn't active, just ignore this callback because we should already did these
// on #applyExitSplitScreen.
@@ -1776,6 +1862,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
true /* setReparentLeafTaskIfRelaunch */);
setRootForceTranslucent(true, wct);
} else {
+ clearRequestIfPresented();
wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
false /* setReparentLeafTaskIfRelaunch */);
setRootForceTranslucent(false, wct);
@@ -1785,6 +1872,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
setDividerVisibility(mainStageVisible, null);
}
+ // Set divider visibility flag and try to apply it, the param transaction is used to apply.
+ // See applyDividerVisibility for more detail.
private void setDividerVisibility(boolean visible, @Nullable SurfaceControl.Transaction t) {
if (visible == mDividerVisible) {
return;
@@ -1811,14 +1900,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return;
}
- if (t != null) {
- applyDividerVisibility(t);
- } else {
- mSyncQueue.runInSync(transaction -> applyDividerVisibility(transaction));
- }
+ applyDividerVisibility(t);
}
- private void applyDividerVisibility(SurfaceControl.Transaction t) {
+ // Apply divider visibility by current visibility flag. If param transaction is non-null, it
+ // will apply by that transaction, if it is null and visible, it will run a fade-in animation,
+ // otherwise hide immediately.
+ private void applyDividerVisibility(@Nullable SurfaceControl.Transaction t) {
final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
if (dividerLeash == null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
@@ -1835,7 +1923,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDividerFadeInAnimator.cancel();
}
- if (mDividerVisible) {
+ mSplitLayout.getRefDividerBounds(mTempRect1);
+ if (t != null) {
+ t.setVisibility(dividerLeash, mDividerVisible);
+ t.setLayer(dividerLeash, Integer.MAX_VALUE);
+ t.setPosition(dividerLeash, mTempRect1.left, mTempRect1.top);
+ } else if (mDividerVisible) {
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
mDividerFadeInAnimator = ValueAnimator.ofFloat(0f, 1f);
mDividerFadeInAnimator.addUpdateListener(animation -> {
@@ -1875,10 +1968,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDividerFadeInAnimator.start();
} else {
- t.hide(dividerLeash);
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ transaction.hide(dividerLeash);
+ transaction.apply();
+ mTransactionPool.release(transaction);
}
}
+ /** Callback when split roots have child or haven't under it.
+ * NOTICE: This only be called on legacy transition. */
private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
final boolean hasChildren = stageListener.mHasChildren;
final boolean isSideStage = stageListener == mSideStageListener;
@@ -1898,20 +1996,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED);
}
} else if (isSideStage && hasChildren && !mMainStage.isActive()) {
- mSplitLayout.init();
-
final WindowContainerTransaction wct = new WindowContainerTransaction();
- if (mIsDropEntering) {
- prepareEnterSplitScreen(wct);
- } else {
- // TODO (b/238697912) : Add the validation to prevent entering non-recovered status
- onSplitScreenEnter();
- mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
- mMainStage.activate(wct, true /* includingTopTask */);
- updateWindowBounds(mSplitLayout, wct);
- wct.reorder(mRootTaskInfo.token, true);
- setRootForceTranslucent(false, wct);
- }
+ prepareEnterSplitScreen(wct);
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
@@ -1926,7 +2012,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
mShouldUpdateRecents = true;
- mSplitRequest = null;
+ clearRequestIfPresented();
updateRecentTasksSplitPair();
if (!mLogger.hasStartedSession()) {
@@ -1946,13 +2032,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final boolean mainStageToTop =
bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
: mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
+ final StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage;
if (!ENABLE_SHELL_TRANSITIONS) {
- exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, reason);
+ exitSplitScreen(toTopStage, reason);
return;
}
final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
final WindowContainerTransaction wct = new WindowContainerTransaction();
+ toTopStage.resetBounds(wct);
prepareExitSplitScreen(dismissTop, wct);
if (mRootTaskInfo != null) {
wct.setDoNotPip(mRootTaskInfo.token);
@@ -1992,21 +2080,30 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Reset this flag every time onLayoutSizeChanged.
mShowDecorImmediately = false;
- if (!ENABLE_SHELL_TRANSITIONS) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ boolean sizeChanged = updateWindowBounds(layout, wct);
+ if (!sizeChanged) {
+ // We still need to resize on decor for ensure all current status clear.
+ final SurfaceControl.Transaction t = mTransactionPool.acquire();
+ mMainStage.onResized(t);
+ mSideStage.onResized(t);
+ mTransactionPool.release(t);
+ return;
+ }
+
+ sendOnBoundsChanged();
+ if (ENABLE_SHELL_TRANSITIONS) {
+ mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart");
+ mSplitTransitions.startResizeTransition(wct, this, (finishWct, t) ->
+ mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish"));
+ } else {
// Only need screenshot for legacy case because shell transition should screenshot
// itself during transition.
final SurfaceControl.Transaction startT = mTransactionPool.acquire();
mMainStage.screenshotIfNeeded(startT);
mSideStage.screenshotIfNeeded(startT);
mTransactionPool.release(startT);
- }
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- updateWindowBounds(layout, wct);
- sendOnBoundsChanged();
- if (ENABLE_SHELL_TRANSITIONS) {
- mSplitTransitions.startResizeTransition(wct, this, null /* callback */);
- } else {
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
@@ -2024,13 +2121,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
/**
* Populates `wct` with operations that match the split windows to the current layout.
* To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
+ *
+ * @return true if stage bounds actually .
*/
- private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
+ private boolean updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
final StageTaskListener topLeftStage =
mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
final StageTaskListener bottomRightStage =
mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
- layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
+ return layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo,
+ bottomRightStage.mRootTaskInfo);
}
void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t,
@@ -2083,7 +2183,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (displayId != DEFAULT_DISPLAY) {
return;
}
- mDisplayLayout.set(mDisplayController.getDisplayLayout(displayId));
+ if (mSplitLayout != null && mSplitLayout.isDensityChanged(newConfig.densityDpi)
+ && mMainStage.isActive()
+ && mSplitLayout.updateConfiguration(newConfig)
+ && ENABLE_SHELL_TRANSITIONS) {
+ mSplitLayout.update(null /* t */);
+ onLayoutSizeChanged(mSplitLayout);
+ }
}
void updateSurfaces(SurfaceControl.Transaction transaction) {
@@ -2093,10 +2199,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void onDisplayChange(int displayId, int fromRotation, int toRotation,
@Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
- if (!mMainStage.isActive()) return;
+ if (displayId != DEFAULT_DISPLAY || !mMainStage.isActive()) return;
- mDisplayLayout.rotateTo(mContext.getResources(), toRotation);
- mSplitLayout.rotateTo(toRotation, mDisplayLayout.stableInsets());
+ mSplitLayout.rotateTo(toRotation);
if (newDisplayAreaInfo != null) {
mSplitLayout.updateConfiguration(newDisplayAreaInfo.configuration);
}
@@ -2220,42 +2325,61 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
out = new WindowContainerTransaction();
final StageTaskListener stage = getStageOfTask(triggerTask);
if (stage != null) {
- // Dismiss split if the last task in one of the stages is going away
if (isClosingType(type) && stage.getChildCount() == 1) {
+ // Dismiss split if the last task in one of the stages is going away
// The top should be the opposite side that is closing:
- int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN ? STAGE_TYPE_SIDE
- : STAGE_TYPE_MAIN;
+ int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN
+ ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+ prepareExitSplitScreen(dismissTop, out);
+ mSplitTransitions.setDismissTransition(transition, dismissTop,
+ EXIT_REASON_APP_FINISHED);
+ } else if (isOpening && !mPausingTasks.isEmpty()) {
+ // One of the splitting task is opening while animating the split pair in
+ // recents, which means to dismiss the split pair to this task.
+ int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN
+ ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
prepareExitSplitScreen(dismissTop, out);
mSplitTransitions.setDismissTransition(transition, dismissTop,
EXIT_REASON_APP_FINISHED);
+ } else if (!isSplitScreenVisible() && isOpening) {
+ // If split is running in the background and the trigger task is appearing into
+ // split, prepare to enter split screen.
+ prepareEnterSplitScreen(out);
+ mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering);
}
} else if (isOpening && inFullscreen) {
final int activityType = triggerTask.getActivityType();
- if (activityType == ACTIVITY_TYPE_ASSISTANT) {
- // We don't want assistant panel to dismiss split screen, so do nothing.
- } else if (activityType == ACTIVITY_TYPE_HOME
- || activityType == ACTIVITY_TYPE_RECENTS) {
- // Enter overview panel, so start recent transition.
- mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(),
- mRecentTransitionFinishedCallback);
- } else if (mSplitTransitions.mPendingRecent == null) {
- // If split-task is not controlled by recents animation
- // and occluded by the other fullscreen task, dismiss both.
+ if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) {
+ // starting recents/home, so don't handle this and let it fall-through to
+ // the remote handler.
+ return null;
+ }
+
+ if ((mMainStage.containsTask(triggerTask.taskId)
+ && mMainStage.getChildCount() == 1)
+ || (mSideStage.containsTask(triggerTask.taskId)
+ && mSideStage.getChildCount() == 1)) {
+ // A splitting task is opening to fullscreen causes one side of the split empty,
+ // so appends operations to exit split.
prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out);
- mSplitTransitions.setDismissTransition(
- transition, STAGE_TYPE_UNDEFINED, EXIT_REASON_UNKNOWN);
}
}
+
+ // When split in the background, it should be only opening/dismissing transition and
+ // would keep out not empty. Prevent intercepting all transitions for split screen when
+ // it is in the background and not identify to handle it.
+ return (!out.isEmpty() || isSplitScreenVisible()) ? out : null;
} else {
if (isOpening && getStageOfTask(triggerTask) != null) {
// One task is appearing into split, prepare to enter split screen.
out = new WindowContainerTransaction();
prepareEnterSplitScreen(out);
mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
- null /* consumedCallback */, null /* finishedCallback */);
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering);
}
+ return out;
}
- return out;
}
/**
@@ -2277,6 +2401,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
+ " so make sure split-screen state is cleaned-up. "
+ "mainStageCount=%d sideStageCount=%d", mMainStage.getChildCount(),
mSideStage.getChildCount());
+ if (triggerTask != null) {
+ mRecentTasks.ifPresent(
+ recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId));
+ }
prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, outWCT);
}
}
@@ -2312,6 +2440,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (!mMainStage.isActive()) return false;
mSplitLayout.setFreezeDividerWindow(false);
+ final StageChangeRecord record = new StageChangeRecord();
+ final int transitType = info.getType();
+ boolean hasEnteringPip = false;
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
if (change.getMode() == TRANSIT_CHANGE
@@ -2319,26 +2450,85 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout.update(startTransaction);
}
+ if (mMixedHandler.isEnteringPip(change, transitType)) {
+ hasEnteringPip = true;
+ }
+
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (taskInfo == null || !taskInfo.hasParentTask()) continue;
+ if (taskInfo == null) continue;
+ if (taskInfo.token.equals(mRootTaskInfo.token)) {
+ if (isOpeningType(change.getMode())) {
+ // Split is opened by someone so set it as visible.
+ setSplitsVisible(true);
+ // TODO(b/275664132): Find a way to integrate this with finishWct.
+ // This is setting the flag to a task and not interfering with the
+ // transition.
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+ false /* reparentLeafTaskIfRelaunch */);
+ mTaskOrganizer.applyTransaction(wct);
+ } else if (isClosingType(change.getMode())) {
+ // Split is closed by someone so set it as invisible.
+ setSplitsVisible(false);
+ // TODO(b/275664132): Find a way to integrate this with finishWct.
+ // This is setting the flag to a task and not interfering with the
+ // transition.
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+ true /* reparentLeafTaskIfRelaunch */);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ continue;
+ }
final StageTaskListener stage = getStageOfTask(taskInfo);
- if (stage == null) continue;
+ if (stage == null) {
+ if (change.getParent() == null && !isClosingType(change.getMode())
+ && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ record.mContainShowFullscreenChange = true;
+ }
+ continue;
+ }
if (isOpeningType(change.getMode())) {
if (!stage.containsTask(taskInfo.taskId)) {
Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called"
+ " with " + taskInfo.taskId + " before startAnimation().");
+ record.addRecord(stage, true, taskInfo.taskId);
}
} else if (isClosingType(change.getMode())) {
if (stage.containsTask(taskInfo.taskId)) {
+ record.addRecord(stage, false, taskInfo.taskId);
Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
+ " with " + taskInfo.taskId + " before startAnimation().");
}
}
}
- if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
- // TODO(shell-transitions): Implement a fallback behavior for now.
- throw new IllegalStateException("Somehow removed the last task in a stage"
- + " outside of a proper transition");
+
+ if (hasEnteringPip) {
+ mMixedHandler.animatePendingEnterPipFromSplit(transition, info,
+ startTransaction, finishTransaction, finishCallback);
+ return true;
+ }
+
+ final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
+ if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0
+ || dismissStages.size() == 1) {
+ // If the size of dismissStages == 1, one of the task is closed without prepare
+ // pending transition, which could happen if all activities were finished after
+ // finish top activity in a task, so the trigger task is null when handleRequest.
+ // Note if the size of dismissStages == 2, it's starting a new task,
+ // so don't handle it.
+ Log.e(TAG, "Somehow removed the last task in a stage outside of a proper "
+ + "transition.");
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final int dismissTop = (dismissStages.size() == 1
+ && getStageType(dismissStages.valueAt(0)) == STAGE_TYPE_MAIN)
+ || mMainStage.getChildCount() == 0 ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+ // If there is a fullscreen opening change, we should not bring stage to top.
+ prepareExitSplitScreen(
+ !record.mContainShowFullscreenChange && isSplitScreenVisible()
+ ? dismissTop : STAGE_TYPE_UNDEFINED, wct);
+ mSplitTransitions.startDismissTransition(wct, this, dismissTop,
+ EXIT_REASON_APP_FINISHED);
// This can happen in some pathological cases. For example:
// 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C]
// 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time
@@ -2346,14 +2536,18 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// TODO(b/184679596): Find a way to either include task-org information in
// the transition, or synchronize task-org callbacks.
}
-
// Use normal animations.
return false;
- } else if (mMixedHandler != null && hasDisplayChange(info)) {
+ } else if (mMixedHandler != null && TransitionUtil.hasDisplayChange(info)) {
// A display-change has been un-expectedly inserted into the transition. Redirect
// handling to the mixed-handler to deal with splitting it up.
if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info,
startTransaction, finishTransaction, finishCallback)) {
+ if (mSplitTransitions.isPendingResize(transition)) {
+ // Only need to update in resize because divider exist before transition.
+ mSplitLayout.update(startTransaction);
+ startTransaction.apply();
+ }
return true;
}
}
@@ -2362,6 +2556,58 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
finishCallback);
}
+ static class StageChangeRecord {
+ boolean mContainShowFullscreenChange = false;
+ static class StageChange {
+ final StageTaskListener mStageTaskListener;
+ final IntArray mAddedTaskId = new IntArray();
+ final IntArray mRemovedTaskId = new IntArray();
+ StageChange(StageTaskListener stage) {
+ mStageTaskListener = stage;
+ }
+
+ boolean shouldDismissStage() {
+ if (mAddedTaskId.size() > 0 || mRemovedTaskId.size() == 0) {
+ return false;
+ }
+ int removeChildTaskCount = 0;
+ for (int i = mRemovedTaskId.size() - 1; i >= 0; --i) {
+ if (mStageTaskListener.containsTask(mRemovedTaskId.get(i))) {
+ ++removeChildTaskCount;
+ }
+ }
+ return removeChildTaskCount == mStageTaskListener.getChildCount();
+ }
+ }
+ private final ArrayMap<StageTaskListener, StageChange> mChanges = new ArrayMap<>();
+
+ void addRecord(StageTaskListener stage, boolean open, int taskId) {
+ final StageChange next;
+ if (!mChanges.containsKey(stage)) {
+ next = new StageChange(stage);
+ mChanges.put(stage, next);
+ } else {
+ next = mChanges.get(stage);
+ }
+ if (open) {
+ next.mAddedTaskId.add(taskId);
+ } else {
+ next.mRemovedTaskId.add(taskId);
+ }
+ }
+
+ ArraySet<StageTaskListener> getShouldDismissedStage() {
+ final ArraySet<StageTaskListener> dismissTarget = new ArraySet<>();
+ for (int i = mChanges.size() - 1; i >= 0; --i) {
+ final StageChange change = mChanges.valueAt(i);
+ if (change.shouldDismissStage()) {
+ dismissTarget.add(change.mStageTaskListener);
+ }
+ }
+ return dismissTarget;
+ }
+ }
+
/** Starts the pending transition animation. */
public boolean startPendingAnimation(@NonNull IBinder transition,
@NonNull TransitionInfo info,
@@ -2371,14 +2617,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
boolean shouldAnimate = true;
if (mSplitTransitions.isPendingEnter(transition)) {
shouldAnimate = startPendingEnterAnimation(
- transition, info, startTransaction, finishTransaction);
- } else if (mSplitTransitions.isPendingRecent(transition)) {
- shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction);
+ mSplitTransitions.mPendingEnter, info, startTransaction, finishTransaction);
} else if (mSplitTransitions.isPendingDismiss(transition)) {
+ final SplitScreenTransitions.DismissSession dismiss = mSplitTransitions.mPendingDismiss;
shouldAnimate = startPendingDismissAnimation(
- mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction);
+ dismiss, info, startTransaction, finishTransaction);
+ if (shouldAnimate && dismiss.mReason == EXIT_REASON_DRAG_DIVIDER) {
+ final StageTaskListener toTopStage =
+ dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage;
+ mSplitTransitions.playDragDismissAnimation(transition, info, startTransaction,
+ finishTransaction, finishCallback, toTopStage.mRootTaskInfo.token,
+ toTopStage.getSplitDecorManager(), mRootTaskInfo.token);
+ return true;
+ }
} else if (mSplitTransitions.isPendingResize(transition)) {
- mSplitTransitions.applyResizeTransition(transition, info, startTransaction,
+ mSplitTransitions.playResizeAnimation(transition, info, startTransaction,
finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token,
mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(),
mSideStage.getSplitDecorManager());
@@ -2392,15 +2645,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return true;
}
- private boolean hasDisplayChange(TransitionInfo info) {
- boolean has = false;
- for (int iC = 0; iC < info.getChanges().size() && !has; ++iC) {
- final TransitionInfo.Change change = info.getChanges().get(iC);
- has = change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0;
- }
- return has;
- }
-
/** Called to clean-up state and do house-keeping after the animation is done. */
public void onTransitionAnimationComplete() {
// If still playing, let it finish.
@@ -2411,28 +2655,42 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
- private boolean startPendingEnterAnimation(@NonNull IBinder transition,
+ private boolean startPendingEnterAnimation(
+ @NonNull SplitScreenTransitions.EnterSession enterTransition,
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
@NonNull SurfaceControl.Transaction finishT) {
// First, verify that we actually have opened apps in both splits.
TransitionInfo.Change mainChild = null;
TransitionInfo.Change sideChild = null;
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (taskInfo == null || !taskInfo.hasParentTask()) continue;
+ if (mPausingTasks.contains(taskInfo.taskId)) {
+ continue;
+ }
final @StageType int stageType = getStageType(getStageOfTask(taskInfo));
- if (stageType == STAGE_TYPE_MAIN) {
+ if (mainChild == null && stageType == STAGE_TYPE_MAIN
+ && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) {
+ // Includes TRANSIT_CHANGE to cover reparenting top-most task to split.
mainChild = change;
- } else if (stageType == STAGE_TYPE_SIDE) {
+ } else if (sideChild == null && stageType == STAGE_TYPE_SIDE
+ && isOpeningType(change.getMode())) {
sideChild = change;
+ } else if (stageType != STAGE_TYPE_UNDEFINED && change.getMode() == TRANSIT_TO_BACK) {
+ // Collect all to back task's and evict them when transition finished.
+ evictWct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
}
}
- if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
+ if (mSplitTransitions.mPendingEnter.mExtraTransitType
+ == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
+ // Open to side should only be used when split already active and foregorund.
if (mainChild == null && sideChild == null) {
Log.w(TAG, "Launched a task in split, but didn't receive any task in transition.");
- mSplitTransitions.mPendingEnter.cancel(null /* finishedCb */);
+ // This should happen when the target app is already on front, so just cancel.
+ mSplitTransitions.mPendingEnter.cancel(null);
return true;
}
} else {
@@ -2443,6 +2701,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
(sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED);
mSplitTransitions.mPendingEnter.cancel(
(cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct));
+ mSplitUnsupportedToast.show();
return true;
}
}
@@ -2451,21 +2710,53 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// transitions locally, but remotes (like Launcher) may get confused if they were
// depending on listener callbacks. This can happen because task-organizer callbacks
// aren't serialized with transition callbacks.
+ // This usually occurred on app use trampoline launch new task and finish itself.
// TODO(b/184679596): Find a way to either include task-org information in
// the transition, or synchronize task-org callbacks.
- if (mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId)) {
+ final boolean mainNotContainOpenTask =
+ mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId);
+ final boolean sideNotContainOpenTask =
+ sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId);
+ if (mainNotContainOpenTask) {
Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
+ " to have been called with " + mainChild.getTaskInfo().taskId
+ " before startAnimation().");
}
- if (sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId)) {
+ if (sideNotContainOpenTask) {
Log.w(TAG, "Expected onTaskAppeared on " + mSideStage
+ " to have been called with " + sideChild.getTaskInfo().taskId
+ " before startAnimation().");
}
+ final TransitionInfo.Change finalMainChild = mainChild;
+ final TransitionInfo.Change finalSideChild = sideChild;
+ enterTransition.setFinishedCallback((callbackWct, callbackT) -> {
+ if (finalMainChild != null) {
+ if (!mainNotContainOpenTask) {
+ mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId);
+ } else {
+ mMainStage.evictInvisibleChildren(callbackWct);
+ }
+ }
+ if (finalSideChild != null) {
+ if (!sideNotContainOpenTask) {
+ mSideStage.evictOtherChildren(callbackWct, finalSideChild.getTaskInfo().taskId);
+ } else {
+ mSideStage.evictInvisibleChildren(callbackWct);
+ }
+ }
+ if (!evictWct.isEmpty()) {
+ callbackWct.merge(evictWct, true);
+ }
+ if (enterTransition.mResizeAnim) {
+ mShowDecorImmediately = true;
+ mSplitLayout.flingDividerToCenter();
+ }
+ callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
+ mPausingTasks.clear();
+ });
finishEnterSplitScreen(finishT);
- addDividerBarToTransition(info, finishT, true /* show */);
+ addDividerBarToTransition(info, true /* show */);
return true;
}
@@ -2530,41 +2821,51 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// aren't serialized with transition callbacks.
// TODO(b/184679596): Find a way to either include task-org information in
// the transition, or synchronize task-org callbacks.
- if (mMainStage.getChildCount() != 0) {
- final StringBuilder tasksLeft = new StringBuilder();
- for (int i = 0; i < mMainStage.getChildCount(); ++i) {
- tasksLeft.append(i != 0 ? ", " : "");
- tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i));
+ if (toStage == STAGE_TYPE_UNDEFINED) {
+ if (mMainStage.getChildCount() != 0) {
+ final StringBuilder tasksLeft = new StringBuilder();
+ for (int i = 0; i < mMainStage.getChildCount(); ++i) {
+ tasksLeft.append(i != 0 ? ", " : "");
+ tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i));
+ }
+ Log.w(TAG, "Expected onTaskVanished on " + mMainStage
+ + " to have been called with [" + tasksLeft.toString()
+ + "] before startAnimation().");
}
- Log.w(TAG, "Expected onTaskVanished on " + mMainStage
- + " to have been called with [" + tasksLeft.toString()
- + "] before startAnimation().");
- }
- if (mSideStage.getChildCount() != 0) {
- final StringBuilder tasksLeft = new StringBuilder();
- for (int i = 0; i < mSideStage.getChildCount(); ++i) {
- tasksLeft.append(i != 0 ? ", " : "");
- tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i));
+ if (mSideStage.getChildCount() != 0) {
+ final StringBuilder tasksLeft = new StringBuilder();
+ for (int i = 0; i < mSideStage.getChildCount(); ++i) {
+ tasksLeft.append(i != 0 ? ", " : "");
+ tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i));
+ }
+ Log.w(TAG, "Expected onTaskVanished on " + mSideStage
+ + " to have been called with [" + tasksLeft.toString()
+ + "] before startAnimation().");
}
- Log.w(TAG, "Expected onTaskVanished on " + mSideStage
- + " to have been called with [" + tasksLeft.toString()
- + "] before startAnimation().");
}
- mRecentTasks.ifPresent(recentTasks -> {
+ final ArrayMap<Integer, SurfaceControl> dismissingTasks = new ArrayMap<>();
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null) continue;
+ if (getStageOfTask(taskInfo) != null
+ || getSplitItemPosition(change.getLastParent()) != SPLIT_POSITION_UNDEFINED) {
+ dismissingTasks.put(taskInfo.taskId, change.getLeash());
+ }
+ }
+
+
+ if (shouldBreakPairedTaskInRecents(dismissReason)) {
// Notify recents if we are exiting in a way that breaks the pair, and disable further
// updates to splits in the recents until we enter split again
- if (shouldBreakPairedTaskInRecents(dismissReason) && mShouldUpdateRecents) {
- for (TransitionInfo.Change change : info.getChanges()) {
- final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (taskInfo != null
- && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
- recentTasks.removeSplitPair(taskInfo.taskId);
- }
+ mRecentTasks.ifPresent(recentTasks -> {
+ for (int i = dismissingTasks.keySet().size() - 1; i >= 0; --i) {
+ recentTasks.removeSplitPair(dismissingTasks.keyAt(i));
}
- }
- });
- mShouldUpdateRecents = false;
+ });
+ }
+ mSplitRequest = null;
// Update local states.
setSplitsVisible(false);
@@ -2573,6 +2874,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Reset crops so they don't interfere with subsequent launches
t.setCrop(mMainStage.mRootLeash, null);
t.setCrop(mSideStage.mRootLeash, null);
+ // Hide the non-top stage and set the top one to the fullscreen position.
+ if (toStage != STAGE_TYPE_UNDEFINED) {
+ t.hide(toStage == STAGE_TYPE_MAIN ? mSideStage.mRootLeash : mMainStage.mRootLeash);
+ t.setPosition(toStage == STAGE_TYPE_MAIN
+ ? mMainStage.mRootLeash : mSideStage.mRootLeash, 0, 0);
+ } else {
+ for (int i = dismissingTasks.keySet().size() - 1; i >= 0; --i) {
+ finishT.hide(dismissingTasks.valueAt(i));
+ }
+ }
if (toStage == STAGE_TYPE_UNDEFINED) {
logExit(dismissReason);
@@ -2581,13 +2892,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
// Hide divider and dim layer on transition finished.
- setDividerVisibility(false, finishT);
+ setDividerVisibility(false, t);
finishT.hide(mMainStage.mDimLayer);
finishT.hide(mSideStage.mDimLayer);
}
private boolean startPendingDismissAnimation(
- @NonNull SplitScreenTransitions.DismissTransition dismissTransition,
+ @NonNull SplitScreenTransitions.DismissSession dismissTransition,
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
@NonNull SurfaceControl.Transaction finishT) {
prepareDismissAnimation(dismissTransition.mDismissTop, dismissTransition.mReason, info,
@@ -2595,27 +2906,107 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (dismissTransition.mDismissTop == STAGE_TYPE_UNDEFINED) {
// TODO: Have a proper remote for this. Until then, though, reset state and use the
// normal animation stuff (which falls back to the normal launcher remote).
- t.hide(mSplitLayout.getDividerLeash());
+ setDividerVisibility(false, t);
mSplitLayout.release(t);
mSplitTransitions.mPendingDismiss = null;
return false;
}
-
- addDividerBarToTransition(info, finishT, false /* show */);
+ dismissTransition.setFinishedCallback((callbackWct, callbackT) -> {
+ mMainStage.getSplitDecorManager().release(callbackT);
+ mSideStage.getSplitDecorManager().release(callbackT);
+ callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
+ });
return true;
}
- private boolean startPendingRecentAnimation(@NonNull IBinder transition,
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
- setDividerVisibility(false, t);
- return true;
+ /** Call this when starting the open-recents animation while split-screen is active. */
+ public void onRecentsInSplitAnimationStart(TransitionInfo info) {
+ if (isSplitScreenVisible()) {
+ // Cache tasks on live tile.
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (TransitionUtil.isClosingType(change.getMode())
+ && change.getTaskInfo() != null) {
+ final int taskId = change.getTaskInfo().taskId;
+ if (mMainStage.getTopVisibleChildTaskId() == taskId
+ || mSideStage.getTopVisibleChildTaskId() == taskId) {
+ mPausingTasks.add(taskId);
+ }
+ }
+ }
+ }
+
+ addDividerBarToTransition(info, false /* show */);
+ }
+
+ /** Call this when the recents animation canceled during split-screen. */
+ public void onRecentsInSplitAnimationCanceled() {
+ mPausingTasks.clear();
+ setSplitsVisible(false);
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+ true /* reparentLeafTaskIfRelaunch */);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+
+ /** Call this when the recents animation during split-screen finishes. */
+ public void onRecentsInSplitAnimationFinish(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {
+ mPausingTasks.clear();
+ // Check if the recent transition is finished by returning to the current
+ // split, so we can restore the divider bar.
+ for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
+ final WindowContainerTransaction.HierarchyOp op =
+ finishWct.getHierarchyOps().get(i);
+ final IBinder container = op.getContainer();
+ if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
+ && (mMainStage.containsContainer(container)
+ || mSideStage.containsContainer(container))) {
+ updateSurfaceBounds(mSplitLayout, finishT,
+ false /* applyResizingOffset */);
+ finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
+ setDividerVisibility(true, finishT);
+ return;
+ }
+ }
+
+ setSplitsVisible(false);
+ finishWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+ true /* reparentLeafTaskIfRelaunch */);
+ }
+
+ /** Call this when the recents animation finishes by doing pair-to-pair switch. */
+ public void onRecentsPairToPairAnimationFinish(WindowContainerTransaction finishWct) {
+ // Pair-to-pair switch happened so here should evict the live tile from its stage.
+ // Otherwise, the task will remain in stage, and occluding the new task when next time
+ // user entering recents.
+ for (int i = mPausingTasks.size() - 1; i >= 0; --i) {
+ final int taskId = mPausingTasks.get(i);
+ if (mMainStage.containsTask(taskId)) {
+ mMainStage.evictChildren(finishWct, taskId);
+ } else if (mSideStage.containsTask(taskId)) {
+ mSideStage.evictChildren(finishWct, taskId);
+ }
+ }
+ // If pending enter hasn't consumed, the mix handler will invoke start pending
+ // animation within following transition.
+ if (mSplitTransitions.mPendingEnter == null) {
+ mPausingTasks.clear();
+ updateRecentTasksSplitPair();
+ }
}
- private void addDividerBarToTransition(@NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction finishT, boolean show) {
+ private void addDividerBarToTransition(@NonNull TransitionInfo info, boolean show) {
final SurfaceControl leash = mSplitLayout.getDividerLeash();
+ if (leash == null || !leash.isValid()) {
+ Slog.w(TAG, "addDividerBarToTransition but leash was released or not be created");
+ return;
+ }
+
final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
mSplitLayout.getRefDividerBounds(mTempRect1);
+ barChange.setParent(mRootTaskInfo.token);
barChange.setStartAbsBounds(mTempRect1);
barChange.setEndAbsBounds(mTempRect1);
barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
@@ -2623,15 +3014,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Technically this should be order-0, but this is running after layer assignment
// and it's a special case, so just add to end.
info.addChange(barChange);
-
- if (show) {
- finishT.setLayer(leash, Integer.MAX_VALUE);
- finishT.setPosition(leash, mTempRect1.left, mTempRect1.top);
- finishT.show(leash);
- // Ensure divider surface are re-parented back into the hierarchy at the end of the
- // transition. See Transition#buildFinishTransaction for more detail.
- finishT.reparent(leash, mRootTaskLeash);
- }
}
RemoteAnimationTarget getDividerBarLegacyTarget() {
@@ -2650,17 +3032,26 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final String childPrefix = innerPrefix + " ";
pw.println(prefix + TAG + " mDisplayId=" + mDisplayId);
pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
+ pw.println(innerPrefix + "isSplitActive=" + isSplitActive());
+ pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible());
pw.println(innerPrefix + "MainStage");
- pw.println(childPrefix + "stagePosition=" + getMainStagePosition());
+ pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition()));
pw.println(childPrefix + "isActive=" + mMainStage.isActive());
+ mMainStage.dump(pw, childPrefix);
+ pw.println(innerPrefix + "MainStageListener");
mMainStageListener.dump(pw, childPrefix);
pw.println(innerPrefix + "SideStage");
- pw.println(childPrefix + "stagePosition=" + getSideStagePosition());
+ pw.println(childPrefix + "stagePosition=" + splitPositionToString(getSideStagePosition()));
+ mSideStage.dump(pw, childPrefix);
+ pw.println(innerPrefix + "SideStageListener");
mSideStageListener.dump(pw, childPrefix);
if (mMainStage.isActive()) {
pw.println(innerPrefix + "SplitLayout");
mSplitLayout.dump(pw, childPrefix);
}
+ if (!mPausingTasks.isEmpty()) {
+ pw.println(childPrefix + "mPausingTasks=" + mPausingTasks);
+ }
}
/**
@@ -2680,8 +3071,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (!isSplitScreenVisible()) {
mIsDropEntering = true;
}
- if (!isSplitScreenVisible()) {
+ if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) {
// If split running background, exit split first.
+ // Skip this on shell transition due to we could evict existing tasks on transition
+ // finished.
exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
}
mLogger.enterRequestedByDrag(position, dragSessionId);
@@ -2691,8 +3084,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
* Sets info to be logged when splitscreen is next entered.
*/
public void onRequestToSplit(InstanceId sessionId, int enterReason) {
- if (!isSplitScreenVisible()) {
+ if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) {
// If split running background, exit split first.
+ // Skip this on shell transition due to we could evict existing tasks on transition
+ // finished.
exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
}
mLogger.enterRequested(sessionId, enterReason);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index ead0bcd15c73..a01eddbc9b9f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.splitscreen;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
@@ -126,7 +127,8 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
* Returns the top visible child task's id.
*/
int getTopVisibleChildTaskId() {
- final ActivityManager.RunningTaskInfo taskInfo = getChildTaskInfo(t -> t.isVisible);
+ final ActivityManager.RunningTaskInfo taskInfo = getChildTaskInfo(t -> t.isVisible
+ && t.isVisibleRequested);
return taskInfo != null ? taskInfo.taskId : INVALID_TASK_ID;
}
@@ -182,12 +184,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
final int taskId = taskInfo.taskId;
mChildrenLeashes.put(taskId, leash);
mChildrenTaskInfo.put(taskId, taskInfo);
- updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
- mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible);
+ mCallbacks.onChildTaskStatusChanged(taskId, true /* present */,
+ taskInfo.isVisible && taskInfo.isVisibleRequested);
if (ENABLE_SHELL_TRANSITIONS) {
// Status is managed/synchronized by the transition lifecycle.
return;
}
+ updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
mCallbacks.onChildTaskAppeared(taskId);
sendStatusChanged();
} else {
@@ -201,7 +204,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
if (mRootTaskInfo.taskId == taskInfo.taskId) {
// Inflates split decor view only when the root task is visible.
- if (mRootTaskInfo.isVisible != taskInfo.isVisible) {
+ if (!ENABLE_SHELL_TRANSITIONS && mRootTaskInfo.isVisible != taskInfo.isVisible) {
if (taskInfo.isVisible) {
mSplitDecorManager.inflate(mContext, mRootLeash,
taskInfo.configuration.windowConfiguration.getBounds());
@@ -220,20 +223,12 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
mCallbacks.onNoLongerSupportMultiWindow();
return;
}
- if (taskInfo.topActivity == null && mChildrenTaskInfo.contains(taskInfo.taskId)
- && mChildrenTaskInfo.get(taskInfo.taskId).topActivity != null) {
- // If top activity become null, it means the task is about to vanish, we use this
- // signal to remove it from children list earlier for smooth dismiss transition.
- mChildrenTaskInfo.remove(taskInfo.taskId);
- mChildrenLeashes.remove(taskInfo.taskId);
- } else {
- mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
- }
+ mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
- taskInfo.isVisible);
- if (!ENABLE_SHELL_TRANSITIONS && mChildrenLeashes.contains(taskInfo.taskId)) {
- updateChildTaskSurface(taskInfo, mChildrenLeashes.get(taskInfo.taskId),
- false /* firstAppeared */);
+ taskInfo.isVisible && taskInfo.isVisibleRequested);
+ if (!ENABLE_SHELL_TRANSITIONS) {
+ updateChildTaskSurface(
+ taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
}
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
@@ -267,6 +262,9 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
return;
}
sendStatusChanged();
+ } else {
+ throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ + "\n mRootTaskInfo: " + mRootTaskInfo);
}
}
@@ -291,6 +289,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
+ boolean isRootTaskId(int taskId) {
+ return mRootTaskInfo != null && mRootTaskInfo.taskId == taskId;
+ }
+
void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX,
int offsetY, boolean immediately) {
if (mSplitDecorManager != null && mRootTaskInfo != null) {
@@ -377,6 +379,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
+ void evictChildren(WindowContainerTransaction wct, int taskId) {
+ final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.get(taskId);
+ if (taskInfo != null) {
+ wct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
+ }
+ }
+
void reparentTopTask(WindowContainerTransaction wct) {
wct.reparentTasks(null /* currentParent */, mRootTaskInfo.token,
CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES,
@@ -386,6 +395,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
void resetBounds(WindowContainerTransaction wct) {
wct.setBounds(mRootTaskInfo.token, null);
wct.setAppBounds(mRootTaskInfo.token, null);
+ wct.setSmallestScreenWidthDp(mRootTaskInfo.token, SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
}
void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
@@ -409,7 +419,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
t.setCrop(leash, null);
t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
- if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) {
+ if (firstAppeared) {
t.setAlpha(leash, 1f);
t.setMatrix(leash, 1, 0, 0, 1);
t.show(leash);
@@ -426,6 +436,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
final String childPrefix = innerPrefix + " ";
- pw.println(prefix + this);
+ if (mChildrenTaskInfo.size() > 0) {
+ pw.println(prefix + "Children list:");
+ for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+ final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
+ pw.println(childPrefix + "Task#" + i + " taskID=" + taskInfo.taskId
+ + " baseActivity=" + taskInfo.baseActivity);
+ }
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS
new file mode 100644
index 000000000000..28be0efc38f6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS
@@ -0,0 +1,3 @@
+# WM shell sub-module TV splitscreen owner
+galinap@google.com
+bronger@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java
new file mode 100644
index 000000000000..1d8a8d506c5c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java
@@ -0,0 +1,218 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.splitscreen.tv;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+import static android.view.WindowManager.SHELL_ROOT_LAYER_DIVIDER;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.view.LayoutInflater;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.split.SplitScreenConstants;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+/**
+ * Handles the interaction logic with the {@link TvSplitMenuView}.
+ * A bridge between {@link TvStageCoordinator} and {@link TvSplitMenuView}.
+ */
+public class TvSplitMenuController implements TvSplitMenuView.Listener {
+
+ private static final String TAG = TvSplitMenuController.class.getSimpleName();
+ private static final String ACTION_SHOW_MENU = "com.android.wm.shell.splitscreen.SHOW_MENU";
+ private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
+
+ private final Context mContext;
+ private final StageController mStageController;
+ private final SystemWindows mSystemWindows;
+ private final Handler mMainHandler;
+
+ private final TvSplitMenuView mSplitMenuView;
+
+ private final ActionBroadcastReceiver mActionBroadcastReceiver;
+
+ private final int mTvButtonFadeAnimationDuration;
+
+ public TvSplitMenuController(Context context, StageController stageController,
+ SystemWindows systemWindows, Handler mainHandler) {
+ mContext = context;
+ mMainHandler = mainHandler;
+ mStageController = stageController;
+ mSystemWindows = systemWindows;
+
+ mTvButtonFadeAnimationDuration = context.getResources()
+ .getInteger(R.integer.tv_window_menu_fade_animation_duration);
+
+ mSplitMenuView = (TvSplitMenuView) LayoutInflater.from(context)
+ .inflate(R.layout.tv_split_menu_view, null);
+ mSplitMenuView.setListener(this);
+
+ mActionBroadcastReceiver = new ActionBroadcastReceiver();
+ }
+
+ /**
+ * Adds the menu view for the splitscreen to SystemWindows.
+ */
+ void addSplitMenuViewToSystemWindows() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ mContext.getResources().getDisplayMetrics().widthPixels,
+ mContext.getResources().getDisplayMetrics().heightPixels,
+ TYPE_DOCK_DIVIDER,
+ FLAG_NOT_TOUCHABLE,
+ PixelFormat.TRANSLUCENT);
+ lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
+ mSplitMenuView.setAlpha(0);
+ mSystemWindows.addView(mSplitMenuView, lp, DEFAULT_DISPLAY, SHELL_ROOT_LAYER_DIVIDER);
+ }
+
+ /**
+ * Removes the menu view for the splitscreen from SystemWindows.
+ */
+ void removeSplitMenuViewFromSystemWindows() {
+ mSystemWindows.removeView(mSplitMenuView);
+ }
+
+ /**
+ * Registers BroadcastReceiver when split screen mode is entered.
+ */
+ void registerBroadcastReceiver() {
+ mActionBroadcastReceiver.register();
+ }
+
+ /**
+ * Unregisters BroadcastReceiver when split screen mode is entered.
+ */
+ void unregisterBroadcastReceiver() {
+ mActionBroadcastReceiver.unregister();
+ }
+
+ @Override
+ public void onBackPress() {
+ setMenuVisibility(false);
+ }
+
+ @Override
+ public void onFocusStage(@SplitScreenConstants.SplitPosition int stageToFocus) {
+ setMenuVisibility(false);
+ mStageController.grantFocusToStage(stageToFocus);
+ }
+
+ @Override
+ public void onCloseStage(@SplitScreenConstants.SplitPosition int stageToClose) {
+ setMenuVisibility(false);
+ mStageController.exitStage(stageToClose);
+ }
+
+ @Override
+ public void onSwapPress() {
+ mStageController.swapStages();
+ }
+
+ private void setMenuVisibility(boolean visible) {
+ applyMenuVisibility(visible);
+ setMenuFocus(visible);
+ }
+
+ private void applyMenuVisibility(boolean visible) {
+ float alphaTarget = visible ? 1F : 0F;
+
+ if (mSplitMenuView.getAlpha() == alphaTarget) {
+ return;
+ }
+
+ mSplitMenuView.animate()
+ .alpha(alphaTarget)
+ .setDuration(mTvButtonFadeAnimationDuration)
+ .withStartAction(() -> {
+ if (alphaTarget != 0) {
+ mSplitMenuView.setVisibility(VISIBLE);
+ }
+ })
+ .withEndAction(() -> {
+ if (alphaTarget == 0) {
+ mSplitMenuView.setVisibility(INVISIBLE);
+ }
+ });
+
+ }
+
+ private void setMenuFocus(boolean focused) {
+ try {
+ WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null,
+ mSystemWindows.getFocusGrantToken(mSplitMenuView), focused);
+ } catch (RemoteException e) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "%s: Unable to update focus, %s", TAG, e);
+ }
+ }
+
+ interface StageController {
+ void grantFocusToStage(@SplitScreenConstants.SplitPosition int stageToFocus);
+ void exitStage(@SplitScreenConstants.SplitPosition int stageToClose);
+ void swapStages();
+ }
+
+ private class ActionBroadcastReceiver extends BroadcastReceiver {
+
+ final IntentFilter mIntentFilter;
+ {
+ mIntentFilter = new IntentFilter();
+ mIntentFilter.addAction(ACTION_SHOW_MENU);
+ }
+ boolean mRegistered = false;
+
+ void register() {
+ if (mRegistered) return;
+
+ mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION,
+ mMainHandler);
+ mRegistered = true;
+ }
+
+ void unregister() {
+ if (!mRegistered) return;
+
+ mContext.unregisterReceiver(this);
+ mRegistered = false;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ if (ACTION_SHOW_MENU.equals(action)) {
+ setMenuVisibility(true);
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java
new file mode 100644
index 000000000000..88e9757a9b31
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.splitscreen.tv;
+
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.KeyEvent.KEYCODE_BACK;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.split.SplitScreenConstants;
+
+/**
+ * A View for the Menu Window.
+ */
+public class TvSplitMenuView extends LinearLayout implements View.OnClickListener {
+
+ private Listener mListener;
+
+ public TvSplitMenuView(Context context) {
+ super(context);
+ }
+
+ public TvSplitMenuView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TvSplitMenuView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ initButtons();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mListener == null) return;
+
+ final int id = v.getId();
+ if (id == R.id.tv_split_main_menu_focus_button) {
+ mListener.onFocusStage(SPLIT_POSITION_TOP_OR_LEFT);
+ } else if (id == R.id.tv_split_main_menu_close_button) {
+ mListener.onCloseStage(SPLIT_POSITION_TOP_OR_LEFT);
+ } else if (id == R.id.tv_split_side_menu_focus_button) {
+ mListener.onFocusStage(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ } else if (id == R.id.tv_split_side_menu_close_button) {
+ mListener.onCloseStage(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ } else if (id == R.id.tv_split_menu_swap_stages) {
+ mListener.onSwapPress();
+ }
+ }
+
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getAction() == ACTION_DOWN) {
+ if (event.getKeyCode() == KEYCODE_BACK) {
+ if (mListener != null) {
+ mListener.onBackPress();
+ return true;
+ }
+ }
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+ private void initButtons() {
+ findViewById(R.id.tv_split_main_menu_focus_button).setOnClickListener(this);
+ findViewById(R.id.tv_split_main_menu_close_button).setOnClickListener(this);
+ findViewById(R.id.tv_split_side_menu_focus_button).setOnClickListener(this);
+ findViewById(R.id.tv_split_side_menu_close_button).setOnClickListener(this);
+ findViewById(R.id.tv_split_menu_swap_stages).setOnClickListener(this);
+ }
+
+ void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ interface Listener {
+ /** "Back" button from the remote control */
+ void onBackPress();
+
+ /** Menu Action Buttons */
+
+ void onFocusStage(@SplitScreenConstants.SplitPosition int stageToFocus);
+
+ void onCloseStage(@SplitScreenConstants.SplitPosition int stageToClose);
+
+ void onSwapPress();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
new file mode 100644
index 000000000000..27d520d81c41
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.splitscreen.tv;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.Optional;
+
+/**
+ * Class inherits from {@link SplitScreenController} and provides {@link TvStageCoordinator}
+ * for Split Screen on TV.
+ */
+public class TvSplitScreenController extends SplitScreenController {
+ private final ShellTaskOrganizer mTaskOrganizer;
+ private final SyncTransactionQueue mSyncQueue;
+ private final Context mContext;
+ private final ShellExecutor mMainExecutor;
+ private final DisplayController mDisplayController;
+ private final DisplayImeController mDisplayImeController;
+ private final DisplayInsetsController mDisplayInsetsController;
+ private final Transitions mTransitions;
+ private final TransactionPool mTransactionPool;
+ private final IconProvider mIconProvider;
+ private final Optional<RecentTasksController> mRecentTasksOptional;
+
+ private final Handler mMainHandler;
+ private final SystemWindows mSystemWindows;
+
+ public TvSplitScreenController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ ShellTaskOrganizer shellTaskOrganizer,
+ SyncTransactionQueue syncQueue,
+ RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+ DisplayController displayController,
+ DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController,
+ Optional<DragAndDropController> dragAndDropController,
+ Transitions transitions,
+ TransactionPool transactionPool,
+ IconProvider iconProvider,
+ Optional<RecentTasksController> recentTasks,
+ ShellExecutor mainExecutor,
+ Handler mainHandler,
+ SystemWindows systemWindows) {
+ super(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer,
+ syncQueue, rootTDAOrganizer, displayController, displayImeController,
+ displayInsetsController, dragAndDropController, transitions, transactionPool,
+ iconProvider, recentTasks, mainExecutor);
+
+ mTaskOrganizer = shellTaskOrganizer;
+ mSyncQueue = syncQueue;
+ mContext = context;
+ mMainExecutor = mainExecutor;
+ mDisplayController = displayController;
+ mDisplayImeController = displayImeController;
+ mDisplayInsetsController = displayInsetsController;
+ mTransitions = transitions;
+ mTransactionPool = transactionPool;
+ mIconProvider = iconProvider;
+ mRecentTasksOptional = recentTasks;
+
+ mMainHandler = mainHandler;
+ mSystemWindows = systemWindows;
+ }
+
+ /**
+ * Provides Tv-specific StageCoordinator.
+ * @return {@link TvStageCoordinator}
+ */
+ @Override
+ protected StageCoordinator createStageCoordinator() {
+ return new TvStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
+ mTaskOrganizer, mDisplayController, mDisplayImeController,
+ mDisplayInsetsController, mTransitions, mTransactionPool,
+ mIconProvider, mMainExecutor, mMainHandler,
+ mRecentTasksOptional, mSystemWindows);
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
new file mode 100644
index 000000000000..4d563fbb7f04
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.splitscreen.tv;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitScreenConstants;
+import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.Optional;
+
+/**
+ * Expands {@link StageCoordinator} functionality with Tv-specific methods.
+ */
+public class TvStageCoordinator extends StageCoordinator
+ implements TvSplitMenuController.StageController {
+
+ private final TvSplitMenuController mTvSplitMenuController;
+
+ public TvStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
+ ShellTaskOrganizer taskOrganizer, DisplayController displayController,
+ DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController, Transitions transitions,
+ TransactionPool transactionPool,
+ IconProvider iconProvider, ShellExecutor mainExecutor,
+ Handler mainHandler,
+ Optional<RecentTasksController> recentTasks,
+ SystemWindows systemWindows) {
+ super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController,
+ displayInsetsController, transitions, transactionPool, iconProvider,
+ mainExecutor, recentTasks);
+
+ mTvSplitMenuController = new TvSplitMenuController(context, this,
+ systemWindows, mainHandler);
+
+ }
+
+ @Override
+ protected void onSplitScreenEnter() {
+ mTvSplitMenuController.addSplitMenuViewToSystemWindows();
+ mTvSplitMenuController.registerBroadcastReceiver();
+ }
+
+ @Override
+ protected void onSplitScreenExit() {
+ mTvSplitMenuController.unregisterBroadcastReceiver();
+ mTvSplitMenuController.removeSplitMenuViewFromSystemWindows();
+ }
+
+ @Override
+ public void grantFocusToStage(@SplitScreenConstants.SplitPosition int stageToFocus) {
+ super.grantFocusToStage(stageToFocus);
+ }
+
+ @Override
+ public void exitStage(@SplitScreenConstants.SplitPosition int stageToClose) {
+ super.exitStage(stageToClose);
+ }
+
+ /**
+ * Swaps the stages inside the SplitLayout.
+ */
+ @Override
+ public void swapStages() {
+ onDoubleTappedDivider();
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java
new file mode 100644
index 000000000000..1ddd8f9a3a14
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java
@@ -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.startingsurface;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+
+import com.android.wm.shell.common.ShellExecutor;
+
+// abstract class to create splash screen window(or windowless window)
+abstract class AbsSplashWindowCreator {
+ protected static final String TAG = StartingWindowController.TAG;
+ protected final SplashscreenContentDrawer mSplashscreenContentDrawer;
+ protected final Context mContext;
+ protected final DisplayManager mDisplayManager;
+ protected final ShellExecutor mSplashScreenExecutor;
+ protected final StartingSurfaceDrawer.StartingWindowRecordManager mStartingWindowRecordManager;
+
+ private StartingSurface.SysuiProxy mSysuiProxy;
+
+ AbsSplashWindowCreator(SplashscreenContentDrawer contentDrawer, Context context,
+ ShellExecutor splashScreenExecutor, DisplayManager displayManager,
+ StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) {
+ mSplashscreenContentDrawer = contentDrawer;
+ mContext = context;
+ mSplashScreenExecutor = splashScreenExecutor;
+ mDisplayManager = displayManager;
+ mStartingWindowRecordManager = startingWindowRecordManager;
+ }
+
+ int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) {
+ return splashScreenThemeResId != 0
+ ? splashScreenThemeResId
+ : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource()
+ : com.android.internal.R.style.Theme_DeviceDefault_DayNight;
+ }
+
+ protected Display getDisplay(int displayId) {
+ return mDisplayManager.getDisplay(displayId);
+ }
+
+ void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) {
+ mSysuiProxy = sysuiProxy;
+ }
+
+ protected void requestTopUi(boolean requestTopUi) {
+ if (mSysuiProxy != null) {
+ mSysuiProxy.requestTopUi(requestTopUi, TAG);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java
new file mode 100644
index 000000000000..20c4d5ae5f58
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java
@@ -0,0 +1,70 @@
+/*
+ * 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.startingsurface;
+
+import android.window.StartingWindowInfo;
+import android.window.TaskSnapshot;
+
+import com.android.wm.shell.common.ShellExecutor;
+
+class SnapshotWindowCreator {
+ private final ShellExecutor mMainExecutor;
+ private final StartingSurfaceDrawer.StartingWindowRecordManager
+ mStartingWindowRecordManager;
+
+ SnapshotWindowCreator(ShellExecutor mainExecutor,
+ StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) {
+ mMainExecutor = mainExecutor;
+ mStartingWindowRecordManager = startingWindowRecordManager;
+ }
+
+ void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, TaskSnapshot snapshot) {
+ final int taskId = startingWindowInfo.taskInfo.taskId;
+ // Remove any existing starting window for this task before adding.
+ mStartingWindowRecordManager.removeWindow(taskId, true);
+ final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo,
+ startingWindowInfo.appToken, snapshot, mMainExecutor,
+ () -> mStartingWindowRecordManager.removeWindow(taskId, true));
+ if (surface != null) {
+ final SnapshotWindowRecord tView = new SnapshotWindowRecord(surface,
+ startingWindowInfo.taskInfo.topActivityType, mMainExecutor);
+ mStartingWindowRecordManager.addRecord(taskId, tView);
+ }
+ }
+
+ private static class SnapshotWindowRecord extends StartingSurfaceDrawer.SnapshotRecord {
+ private final TaskSnapshotWindow mTaskSnapshotWindow;
+
+ SnapshotWindowRecord(TaskSnapshotWindow taskSnapshotWindow,
+ int activityType, ShellExecutor removeExecutor) {
+ super(activityType, removeExecutor);
+ mTaskSnapshotWindow = taskSnapshotWindow;
+ mBGColor = mTaskSnapshotWindow.getBackgroundColor();
+ }
+
+ @Override
+ protected void removeImmediately() {
+ super.removeImmediately();
+ mTaskSnapshotWindow.removeImmediately();
+ }
+
+ @Override
+ protected boolean hasImeSurface() {
+ return mTaskSnapshotWindow.hasImeSurface();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 839d56a43222..dc91a11dc64f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -16,19 +16,19 @@
package com.android.wm.shell.startingsurface;
+import static android.content.Context.CONTEXT_RESTRICTED;
import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
-import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION;
-import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION;
-
import android.annotation.ColorInt;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManager;
import android.app.ActivityThread;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -48,9 +48,11 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
+import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.IBinder;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
@@ -58,7 +60,9 @@ import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.Slog;
import android.view.ContextThemeWrapper;
+import android.view.Display;
import android.view.SurfaceControl;
+import android.view.WindowManager;
import android.window.SplashScreenView;
import android.window.StartingWindowInfo;
import android.window.StartingWindowInfo.StartingWindowType;
@@ -89,6 +93,25 @@ import java.util.function.UnaryOperator;
public class SplashscreenContentDrawer {
private static final String TAG = StartingWindowController.TAG;
+ /**
+ * The minimum duration during which the splash screen is shown when the splash screen icon is
+ * animated.
+ */
+ static final long MINIMAL_ANIMATION_DURATION = 400L;
+
+ /**
+ * Allow the icon style splash screen to be displayed for longer to give time for the animation
+ * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly
+ * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration.
+ */
+ static final long TIME_WINDOW_DURATION = 100L;
+
+ /**
+ * The maximum duration during which the splash screen will be shown if the application is ready
+ * to show before the icon animation finishes.
+ */
+ static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION;
+
// The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
// icon which it's non-transparent foreground area is similar to it's background area, then
// do not enlarge the foreground drawable.
@@ -134,6 +157,144 @@ public class SplashscreenContentDrawer {
}
/**
+ * Help method to create a layout parameters for a window.
+ */
+ static Context createContext(Context initContext, StartingWindowInfo windowInfo,
+ int theme, @StartingWindowInfo.StartingWindowType int suggestType,
+ DisplayManager displayManager) {
+ final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
+ final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
+ ? windowInfo.targetActivityInfo
+ : taskInfo.topActivityInfo;
+ if (activityInfo == null || activityInfo.packageName == null) {
+ return null;
+ }
+
+ final int displayId = taskInfo.displayId;
+ final int taskId = taskInfo.taskId;
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d",
+ activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType);
+ final Display display = displayManager.getDisplay(displayId);
+ if (display == null) {
+ // Can't show splash screen on requested display, so skip showing at all.
+ return null;
+ }
+ Context context = displayId == DEFAULT_DISPLAY
+ ? initContext : initContext.createDisplayContext(display);
+ if (context == null) {
+ return null;
+ }
+ if (theme != context.getThemeResId()) {
+ try {
+ context = context.createPackageContextAsUser(activityInfo.packageName,
+ CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId));
+ context.setTheme(theme);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(TAG, "Failed creating package context with package name "
+ + activityInfo.packageName + " for user " + taskInfo.userId, e);
+ return null;
+ }
+ }
+
+ final Configuration taskConfig = taskInfo.getConfiguration();
+ if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "addSplashScreen: creating context based on task Configuration %s",
+ taskConfig);
+ final Context overrideContext = context.createConfigurationContext(taskConfig);
+ overrideContext.setTheme(theme);
+ final TypedArray typedArray = overrideContext.obtainStyledAttributes(
+ com.android.internal.R.styleable.Window);
+ final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
+ try {
+ if (resId != 0 && overrideContext.getDrawable(resId) != null) {
+ // We want to use the windowBackground for the override context if it is
+ // available, otherwise we use the default one to make sure a themed starting
+ // window is displayed for the app.
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "addSplashScreen: apply overrideConfig %s",
+ taskConfig);
+ context = overrideContext;
+ }
+ } catch (Resources.NotFoundException e) {
+ Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: "
+ + taskId, e);
+ return null;
+ }
+ typedArray.recycle();
+ }
+ return context;
+ }
+
+ /**
+ * Creates the window layout parameters for splashscreen window.
+ */
+ static WindowManager.LayoutParams createLayoutParameters(Context context,
+ StartingWindowInfo windowInfo,
+ @StartingWindowInfo.StartingWindowType int suggestType,
+ CharSequence title, int pixelFormat, IBinder appToken) {
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
+ params.setFitInsetsSides(0);
+ params.setFitInsetsTypes(0);
+ params.format = pixelFormat;
+ int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+ final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
+ if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
+ windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+ }
+ if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
+ if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) {
+ windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ }
+ } else {
+ windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ }
+ params.layoutInDisplayCutoutMode = a.getInt(
+ R.styleable.Window_windowLayoutInDisplayCutoutMode,
+ params.layoutInDisplayCutoutMode);
+ params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
+ a.recycle();
+
+ final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
+ final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
+ ? windowInfo.targetActivityInfo
+ : taskInfo.topActivityInfo;
+ final int displayId = taskInfo.displayId;
+ // Assumes it's safe to show starting windows of launched apps while
+ // the keyguard is being hidden. This is okay because starting windows never show
+ // secret information.
+ // TODO(b/113840485): Occluded may not only happen on default display
+ if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) {
+ windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+ }
+
+ // Force the window flags: this is a fake window, so it is not really
+ // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM
+ // flag because we do know that the next window will take input
+ // focus, so we want to get the IME window up on top of us right away.
+ // Touches will only pass through to the host activity window and will be blocked from
+ // passing to any other windows.
+ windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ params.flags = windowFlags;
+ params.token = appToken;
+ params.packageName = activityInfo.packageName;
+ params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+
+ if (!context.getResources().getCompatibilityInfo().supportsScreen()) {
+ params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
+ }
+
+ params.setTitle("Splash Screen " + title);
+ return params;
+ }
+ /**
* Create a SplashScreenView object.
*
* In order to speed up the splash screen view to show on first frame, preparing the
@@ -223,10 +384,10 @@ public class SplashscreenContentDrawer {
private static int estimateWindowBGColor(Drawable themeBGDrawable) {
final DrawableColorTester themeBGTester = new DrawableColorTester(
- themeBGDrawable, DrawableColorTester.TRANSPARENT_FILTER /* filterType */);
- if (themeBGTester.passFilterRatio() == 0) {
- // the window background is transparent, unable to draw
- Slog.w(TAG, "Window background is transparent, fill background with black color");
+ themeBGDrawable, DrawableColorTester.TRANSLUCENT_FILTER /* filterType */);
+ if (themeBGTester.passFilterRatio() != 1) {
+ // the window background is translucent, unable to draw
+ Slog.w(TAG, "Window background is translucent, fill background with black color");
return getSystemBGColor();
} else {
return themeBGTester.getDominateColor();
@@ -248,6 +409,26 @@ public class SplashscreenContentDrawer {
return null;
}
+ /**
+ * Creates a SplashScreenView without read animatable icon and branding image.
+ */
+ SplashScreenView makeSimpleSplashScreenContentView(Context context,
+ StartingWindowInfo info, int themeBGColor) {
+ updateDensity();
+ mTmpAttrs.reset();
+ final ActivityInfo ai = info.targetActivityInfo != null
+ ? info.targetActivityInfo
+ : info.taskInfo.topActivityInfo;
+
+ final SplashViewBuilder builder = new SplashViewBuilder(context, ai);
+ final SplashScreenView view = builder
+ .setWindowBGColor(themeBGColor)
+ .chooseStyle(STARTING_WINDOW_TYPE_SPLASH_SCREEN)
+ .build();
+ view.setNotCopyable();
+ return view;
+ }
+
private SplashScreenView makeSplashScreenContentView(Context context, StartingWindowInfo info,
@StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) {
updateDensity();
@@ -263,7 +444,8 @@ public class SplashscreenContentDrawer {
final int themeBGColor = legacyDrawable != null
? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable))
: getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs));
- return new StartingWindowViewBuilder(context, ai)
+
+ return new SplashViewBuilder(context, ai)
.setWindowBGColor(themeBGColor)
.overlayDrawable(legacyDrawable)
.chooseStyle(suggestType)
@@ -322,6 +504,14 @@ public class SplashscreenContentDrawer {
private Drawable mSplashScreenIcon = null;
private Drawable mBrandingImage = null;
private int mIconBgColor = Color.TRANSPARENT;
+
+ void reset() {
+ mWindowBgResId = 0;
+ mWindowBgColor = Color.TRANSPARENT;
+ mSplashScreenIcon = null;
+ mBrandingImage = null;
+ mIconBgColor = Color.TRANSPARENT;
+ }
}
/**
@@ -351,7 +541,7 @@ public class SplashscreenContentDrawer {
return appReadyDuration;
}
- private class StartingWindowViewBuilder {
+ private class SplashViewBuilder {
private final Context mContext;
private final ActivityInfo mActivityInfo;
@@ -364,27 +554,28 @@ public class SplashscreenContentDrawer {
/** @see #setAllowHandleSolidColor(boolean) **/
private boolean mAllowHandleSolidColor;
- StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) {
+ SplashViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) {
mContext = context;
mActivityInfo = aInfo;
}
- StartingWindowViewBuilder setWindowBGColor(@ColorInt int background) {
+ SplashViewBuilder setWindowBGColor(@ColorInt int background) {
mThemeColor = background;
return this;
}
- StartingWindowViewBuilder overlayDrawable(Drawable overlay) {
+ SplashViewBuilder overlayDrawable(Drawable overlay) {
mOverlayDrawable = overlay;
return this;
}
- StartingWindowViewBuilder chooseStyle(int suggestType) {
+ SplashViewBuilder chooseStyle(int suggestType) {
mSuggestType = suggestType;
return this;
}
- StartingWindowViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) {
+ // Set up the UI thread for the View.
+ SplashViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) {
mUiThreadInitTask = uiThreadInitTask;
return this;
}
@@ -395,7 +586,7 @@ public class SplashscreenContentDrawer {
* android.window.SplashScreen.OnExitAnimationListener#onSplashScreenExit(SplashScreenView)}
* callback, effectively copying the {@link SplashScreenView} into the client process.
*/
- StartingWindowViewBuilder setAllowHandleSolidColor(boolean allowHandleSolidColor) {
+ SplashViewBuilder setAllowHandleSolidColor(boolean allowHandleSolidColor) {
mAllowHandleSolidColor = allowHandleSolidColor;
return this;
}
@@ -679,7 +870,7 @@ public class SplashscreenContentDrawer {
@Override
public float passFilterRatio() {
final int alpha = mColorDrawable.getAlpha();
- return (float) (alpha / 255);
+ return alpha / 255.0f;
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
index 7f6bfd23f72b..e419462012e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
@@ -62,7 +62,7 @@ public class SplashscreenIconDrawableFactory {
*/
static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor,
@NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize,
- boolean loadInDetail, Handler splashscreenWorkerHandler) {
+ boolean loadInDetail, Handler preDrawHandler) {
Drawable foreground;
Drawable background = null;
boolean drawBackground =
@@ -74,13 +74,13 @@ public class SplashscreenIconDrawableFactory {
// If the icon is Adaptive, we already use the icon background.
drawBackground = false;
foreground = new ImmobileIconDrawable(foregroundDrawable,
- srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler);
+ srcIconSize, iconSize, loadInDetail, preDrawHandler);
} else {
// Adaptive icon don't handle transparency so we draw the background of the adaptive
// icon with the same color as the window background color instead of using two layers
foreground = new ImmobileIconDrawable(
new AdaptiveForegroundDrawable(foregroundDrawable),
- srcIconSize, iconSize, loadInDetail, splashscreenWorkerHandler);
+ srcIconSize, iconSize, loadInDetail, preDrawHandler);
}
if (drawBackground) {
@@ -91,9 +91,9 @@ public class SplashscreenIconDrawableFactory {
}
static Drawable[] makeLegacyIconDrawable(@NonNull Drawable iconDrawable, int srcIconSize,
- int iconSize, boolean loadInDetail, Handler splashscreenWorkerHandler) {
+ int iconSize, boolean loadInDetail, Handler preDrawHandler) {
return new Drawable[]{new ImmobileIconDrawable(iconDrawable, srcIconSize, iconSize,
- loadInDetail, splashscreenWorkerHandler)};
+ loadInDetail, preDrawHandler)};
}
/**
@@ -107,14 +107,14 @@ public class SplashscreenIconDrawableFactory {
private Bitmap mIconBitmap;
ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, boolean loadInDetail,
- Handler splashscreenWorkerHandler) {
+ Handler preDrawHandler) {
// This icon has lower density, don't scale it.
if (loadInDetail) {
- splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, iconSize));
+ preDrawHandler.post(() -> preDrawIcon(drawable, iconSize));
} else {
final float scale = (float) iconSize / srcIconSize;
mMatrix.setScale(scale, scale);
- splashscreenWorkerHandler.post(() -> preDrawIcon(drawable, srcIconSize));
+ preDrawHandler.post(() -> preDrawIcon(drawable, srcIconSize));
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java
new file mode 100644
index 000000000000..4cfbbd971fe3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java
@@ -0,0 +1,513 @@
+/*
+ * 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.startingsurface;
+
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION;
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.ActivityThread;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.hardware.display.DisplayManager;
+import android.os.IBinder;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Choreographer;
+import android.view.Display;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.widget.FrameLayout;
+import android.window.SplashScreenView;
+import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
+
+import com.android.internal.R;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.ContrastColorUtil;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.util.function.Supplier;
+
+/**
+ * A class which able to draw splash screen as the starting window for a task.
+ *
+ * In order to speed up, there will use two threads to creating a splash screen in parallel.
+ * Right now we are still using PhoneWindow to create splash screen window, so the view is added to
+ * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call
+ * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view
+ * can synchronize on each frame.
+ *
+ * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing
+ * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background
+ * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after
+ * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very
+ * quickly.
+ *
+ * So basically we are using the spare time to prepare the SplashScreenView while splash screen
+ * thread is waiting for
+ * 1. WindowManager#addView(binder call to WM),
+ * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device),
+ * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will
+ * always happen before #draw).
+ * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on
+ * splash-screen background tread can make they execute in parallel, which ensure it is faster then
+ * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame.
+ *
+ * Here is the sequence to compare the difference between using single and two thread.
+ *
+ * Single thread:
+ * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout
+ * -> draw -> AdaptiveIconDrawable#draw
+ *
+ * Two threads:
+ * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw)
+ * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint
+ * directly).
+ */
+class SplashscreenWindowCreator extends AbsSplashWindowCreator {
+ private static final int LIGHT_BARS_MASK =
+ WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+ | WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+
+ private final WindowManagerGlobal mWindowManagerGlobal;
+ private Choreographer mChoreographer;
+
+ /**
+ * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is
+ * rendered and that have not yet been removed by their client.
+ */
+ private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts =
+ new SparseArray<>(1);
+
+ SplashscreenWindowCreator(SplashscreenContentDrawer contentDrawer, Context context,
+ ShellExecutor splashScreenExecutor, DisplayManager displayManager,
+ StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager) {
+ super(contentDrawer, context, splashScreenExecutor, displayManager,
+ startingWindowRecordManager);
+ mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
+ mWindowManagerGlobal = WindowManagerGlobal.getInstance();
+ }
+
+ void addSplashScreenStartingWindow(StartingWindowInfo windowInfo,
+ @StartingWindowInfo.StartingWindowType int suggestType) {
+ final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
+ final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
+ ? windowInfo.targetActivityInfo
+ : taskInfo.topActivityInfo;
+ if (activityInfo == null || activityInfo.packageName == null) {
+ return;
+ }
+ // replace with the default theme if the application didn't set
+ final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo);
+ final Context context = SplashscreenContentDrawer.createContext(mContext, windowInfo, theme,
+ suggestType, mDisplayManager);
+ if (context == null) {
+ return;
+ }
+ final WindowManager.LayoutParams params = SplashscreenContentDrawer.createLayoutParameters(
+ context, windowInfo, suggestType, activityInfo.packageName,
+ suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
+ ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT, windowInfo.appToken);
+
+ final int displayId = taskInfo.displayId;
+ final int taskId = taskInfo.taskId;
+ final Display display = getDisplay(displayId);
+
+ // TODO(b/173975965) tracking performance
+ // Prepare the splash screen content view on splash screen worker thread in parallel, so the
+ // content view won't be blocked by binder call like addWindow and relayout.
+ // 1. Trigger splash screen worker thread to create SplashScreenView before/while
+ // Session#addWindow.
+ // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start
+ // traversal, which will call Session#relayout on splash screen thread.
+ // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at
+ // the same time the splash screen thread should be executing Session#relayout. Blocking the
+ // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready.
+
+ // Record whether create splash screen view success, notify to current thread after
+ // create splash screen view finished.
+ final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
+ final FrameLayout rootLayout = new FrameLayout(
+ mSplashscreenContentDrawer.createViewContextWrapper(context));
+ rootLayout.setPadding(0, 0, 0, 0);
+ rootLayout.setFitsSystemWindows(false);
+ final Runnable setViewSynchronized = () -> {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
+ // waiting for setContentView before relayoutWindow
+ SplashScreenView contentView = viewSupplier.get();
+ final StartingSurfaceDrawer.StartingWindowRecord sRecord =
+ mStartingWindowRecordManager.getRecord(taskId);
+ final SplashWindowRecord record = sRecord instanceof SplashWindowRecord
+ ? (SplashWindowRecord) sRecord : null;
+ // If record == null, either the starting window added fail or removed already.
+ // Do not add this view if the token is mismatch.
+ if (record != null && windowInfo.appToken == record.mAppToken) {
+ // if view == null then creation of content view was failed.
+ if (contentView != null) {
+ try {
+ rootLayout.addView(contentView);
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "failed set content view to starting window "
+ + "at taskId: " + taskId, e);
+ contentView = null;
+ }
+ }
+ record.setSplashScreenView(contentView);
+ }
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ };
+ requestTopUi(true);
+ mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo,
+ viewSupplier::setView, viewSupplier::setUiThreadInitTask);
+ try {
+ if (addWindow(taskId, windowInfo.appToken, rootLayout, display, params, suggestType)) {
+ // We use the splash screen worker thread to create SplashScreenView while adding
+ // the window, as otherwise Choreographer#doFrame might be delayed on this thread.
+ // And since Choreographer#doFrame won't happen immediately after adding the window,
+ // if the view is not added to the PhoneWindow on the first #doFrame, the view will
+ // not be rendered on the first frame. So here we need to synchronize the view on
+ // the window before first round relayoutWindow, which will happen after insets
+ // animation.
+ mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null);
+ final SplashWindowRecord record =
+ (SplashWindowRecord) mStartingWindowRecordManager.getRecord(taskId);
+ if (record != null) {
+ record.parseAppSystemBarColor(context);
+ // Block until we get the background color.
+ final SplashScreenView contentView = viewSupplier.get();
+ if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
+ contentView.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ final int lightBarAppearance =
+ ContrastColorUtil.isColorLight(
+ contentView.getInitBackgroundColor())
+ ? LIGHT_BARS_MASK : 0;
+ contentView.getWindowInsetsController()
+ .setSystemBarsAppearance(
+ lightBarAppearance, LIGHT_BARS_MASK);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ }
+ });
+ }
+ }
+ } else {
+ // release the icon view host
+ final SplashScreenView contentView = viewSupplier.get();
+ if (contentView.getSurfaceHost() != null) {
+ SplashScreenView.releaseIconHost(contentView.getSurfaceHost());
+ }
+ }
+ } catch (RuntimeException e) {
+ // don't crash if something else bad happens, for example a
+ // failure loading resources because we are loading from an app
+ // on external storage that has been unmounted.
+ Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e);
+ }
+ }
+
+ int estimateTaskBackgroundColor(TaskInfo taskInfo) {
+ if (taskInfo.topActivityInfo == null) {
+ return Color.TRANSPARENT;
+ }
+ final ActivityInfo activityInfo = taskInfo.topActivityInfo;
+ final String packageName = activityInfo.packageName;
+ final int userId = taskInfo.userId;
+ final Context windowContext;
+ try {
+ windowContext = mContext.createPackageContextAsUser(
+ packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId));
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(TAG, "Failed creating package context with package name "
+ + packageName + " for user " + taskInfo.userId, e);
+ return Color.TRANSPARENT;
+ }
+ try {
+ final IPackageManager packageManager = ActivityThread.getPackageManager();
+ final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName,
+ userId);
+ final int splashScreenThemeId = splashScreenThemeName != null
+ ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null)
+ : 0;
+
+ final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo);
+
+ if (theme != windowContext.getThemeResId()) {
+ windowContext.setTheme(theme);
+ }
+ return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext);
+ } catch (RuntimeException | RemoteException e) {
+ Slog.w(TAG, "failed get starting window background color at taskId: "
+ + taskInfo.taskId, e);
+ }
+ return Color.TRANSPARENT;
+ }
+
+ /**
+ * Called when the Task wants to copy the splash screen.
+ */
+ public void copySplashScreenView(int taskId) {
+ final StartingSurfaceDrawer.StartingWindowRecord record =
+ mStartingWindowRecordManager.getRecord(taskId);
+ final SplashWindowRecord preView = record instanceof SplashWindowRecord
+ ? (SplashWindowRecord) record : null;
+ SplashScreenView.SplashScreenViewParcelable parcelable;
+ SplashScreenView splashScreenView = preView != null ? preView.mSplashView : null;
+ if (splashScreenView != null && splashScreenView.isCopyable()) {
+ parcelable = new SplashScreenView.SplashScreenViewParcelable(splashScreenView);
+ parcelable.setClientCallback(
+ new RemoteCallback((bundle) -> mSplashScreenExecutor.execute(
+ () -> onAppSplashScreenViewRemoved(taskId, false))));
+ splashScreenView.onCopied();
+ mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost());
+ } else {
+ parcelable = null;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "Copying splash screen window view for task: %d with parcelable %b",
+ taskId, parcelable != null);
+ ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable);
+ }
+
+ /**
+ * Called when the {@link SplashScreenView} is removed from the client Activity view's hierarchy
+ * or when the Activity is clean up.
+ *
+ * @param taskId The Task id on which the splash screen was attached
+ */
+ public void onAppSplashScreenViewRemoved(int taskId) {
+ onAppSplashScreenViewRemoved(taskId, true /* fromServer */);
+ }
+
+ /**
+ * @param fromServer If true, this means the removal was notified by the server. This is only
+ * used for debugging purposes.
+ * @see #onAppSplashScreenViewRemoved(int)
+ */
+ private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) {
+ SurfaceControlViewHost viewHost =
+ mAnimatedSplashScreenSurfaceHosts.get(taskId);
+ if (viewHost == null) {
+ return;
+ }
+ mAnimatedSplashScreenSurfaceHosts.remove(taskId);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "%s the splash screen. Releasing SurfaceControlViewHost for task: %d",
+ fromServer ? "Server cleaned up" : "App removed", taskId);
+ SplashScreenView.releaseIconHost(viewHost);
+ }
+
+ protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
+ WindowManager.LayoutParams params,
+ @StartingWindowInfo.StartingWindowType int suggestType) {
+ boolean shouldSaveView = true;
+ final Context context = view.getContext();
+ try {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
+ mWindowManagerGlobal.addView(view, params, display,
+ null /* parentWindow */, context.getUserId());
+ } catch (WindowManager.BadTokenException e) {
+ // ignore
+ Slog.w(TAG, appToken + " already running, starting window not displayed. "
+ + e.getMessage());
+ shouldSaveView = false;
+ } finally {
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ if (view.getParent() == null) {
+ Slog.w(TAG, "view not successfully added to wm, removing view");
+ mWindowManagerGlobal.removeView(view, true /* immediate */);
+ shouldSaveView = false;
+ }
+ }
+ if (shouldSaveView) {
+ mStartingWindowRecordManager.removeWindow(taskId, true);
+ saveSplashScreenRecord(appToken, taskId, view, suggestType);
+ }
+ return shouldSaveView;
+ }
+
+ private void saveSplashScreenRecord(IBinder appToken, int taskId, View view,
+ @StartingWindowInfo.StartingWindowType int suggestType) {
+ final SplashWindowRecord tView =
+ new SplashWindowRecord(appToken, view, suggestType);
+ mStartingWindowRecordManager.addRecord(taskId, tView);
+ }
+
+ private void removeWindowInner(@NonNull View decorView, boolean hideView) {
+ requestTopUi(false);
+ if (!decorView.isAttachedToWindow()) {
+ return;
+ }
+ if (hideView) {
+ decorView.setVisibility(View.GONE);
+ }
+ mWindowManagerGlobal.removeView(decorView, false /* immediate */);
+ }
+
+ private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> {
+ private SplashScreenView mView;
+ private boolean mIsViewSet;
+ private Runnable mUiThreadInitTask;
+ void setView(SplashScreenView view) {
+ synchronized (this) {
+ mView = view;
+ mIsViewSet = true;
+ notify();
+ }
+ }
+
+ void setUiThreadInitTask(Runnable initTask) {
+ synchronized (this) {
+ mUiThreadInitTask = initTask;
+ }
+ }
+
+ @Override
+ @Nullable
+ public SplashScreenView get() {
+ synchronized (this) {
+ while (!mIsViewSet) {
+ try {
+ wait();
+ } catch (InterruptedException ignored) {
+ }
+ }
+ if (mUiThreadInitTask != null) {
+ mUiThreadInitTask.run();
+ mUiThreadInitTask = null;
+ }
+ return mView;
+ }
+ }
+ }
+
+ private class SplashWindowRecord extends StartingSurfaceDrawer.StartingWindowRecord {
+ private final IBinder mAppToken;
+ private final View mRootView;
+ @StartingWindowInfo.StartingWindowType private final int mSuggestType;
+ private final long mCreateTime;
+
+ private boolean mSetSplashScreen;
+ private SplashScreenView mSplashView;
+ private int mSystemBarAppearance;
+ private boolean mDrawsSystemBarBackgrounds;
+
+ SplashWindowRecord(IBinder appToken, View decorView,
+ @StartingWindowInfo.StartingWindowType int suggestType) {
+ mAppToken = appToken;
+ mRootView = decorView;
+ mSuggestType = suggestType;
+ mCreateTime = SystemClock.uptimeMillis();
+ }
+
+ void setSplashScreenView(@Nullable SplashScreenView splashScreenView) {
+ if (mSetSplashScreen) {
+ return;
+ }
+ mSplashView = splashScreenView;
+ mBGColor = mSplashView != null ? mSplashView.getInitBackgroundColor()
+ : Color.TRANSPARENT;
+ mSetSplashScreen = true;
+ }
+
+ void parseAppSystemBarColor(Context context) {
+ final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
+ mDrawsSystemBarBackgrounds = a.getBoolean(
+ R.styleable.Window_windowDrawsSystemBarBackgrounds, false);
+ if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
+ mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+ }
+ if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) {
+ mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+ }
+ a.recycle();
+ }
+
+ // Reset the system bar color which set by splash screen, make it align to the app.
+ void clearSystemBarColor() {
+ if (mRootView == null || !mRootView.isAttachedToWindow()) {
+ return;
+ }
+ if (mRootView.getLayoutParams() instanceof WindowManager.LayoutParams) {
+ final WindowManager.LayoutParams lp =
+ (WindowManager.LayoutParams) mRootView.getLayoutParams();
+ if (mDrawsSystemBarBackgrounds) {
+ lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ } else {
+ lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ }
+ mRootView.setLayoutParams(lp);
+ }
+ mRootView.getWindowInsetsController().setSystemBarsAppearance(
+ mSystemBarAppearance, LIGHT_BARS_MASK);
+ }
+
+ @Override
+ public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
+ if (mRootView == null) {
+ return true;
+ }
+ if (mSplashView == null) {
+ // shouldn't happen, the app window may be drawn earlier than starting window?
+ Slog.e(TAG, "Found empty splash screen, remove!");
+ removeWindowInner(mRootView, false);
+ return true;
+ }
+ clearSystemBarColor();
+ if (immediately
+ || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
+ removeWindowInner(mRootView, false);
+ } else {
+ if (info.playRevealAnimation) {
+ mSplashscreenContentDrawer.applyExitAnimation(mSplashView,
+ info.windowAnimationLeash, info.mainFrame,
+ () -> removeWindowInner(mRootView, true),
+ mCreateTime, info.roundedCornerRadius);
+ } else {
+ // the SplashScreenView has been copied to client, hide the view to skip
+ // default exit animation
+ removeWindowInner(mRootView, true);
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index 22e804547d5c..7cbf263f7cb1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -16,172 +16,82 @@
package com.android.wm.shell.startingsurface;
-import static android.content.Context.CONTEXT_RESTRICTED;
-import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
-import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT;
+import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL;
+import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION;
-import android.annotation.Nullable;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.app.ActivityTaskManager;
-import android.app.ActivityThread;
+import android.annotation.CallSuper;
import android.app.TaskInfo;
+import android.app.WindowConfiguration;
import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.IPackageManager;
-import android.content.pm.PackageManager;
import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
import android.graphics.Color;
-import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
-import android.os.IBinder;
-import android.os.RemoteCallback;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.util.Slog;
import android.util.SparseArray;
-import android.view.Choreographer;
-import android.view.Display;
-import android.view.SurfaceControlViewHost;
-import android.view.View;
-import android.view.WindowInsetsController;
+import android.view.IWindow;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
import android.view.WindowManager;
-import android.view.WindowManagerGlobal;
-import android.widget.FrameLayout;
+import android.view.WindowlessWindowManager;
import android.window.SplashScreenView;
-import android.window.SplashScreenView.SplashScreenViewParcelable;
import android.window.StartingWindowInfo;
import android.window.StartingWindowInfo.StartingWindowType;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskSnapshot;
-import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
-import com.android.internal.util.ContrastColorUtil;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import java.util.function.Supplier;
-
/**
* A class which able to draw splash screen or snapshot as the starting window for a task.
- *
- * In order to speed up, there will use two threads to creating a splash screen in parallel.
- * Right now we are still using PhoneWindow to create splash screen window, so the view is added to
- * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call
- * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view
- * can synchronize on each frame.
- *
- * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing
- * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background
- * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after
- * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very
- * quickly.
- *
- * So basically we are using the spare time to prepare the SplashScreenView while splash screen
- * thread is waiting for
- * 1. WindowManager#addView(binder call to WM),
- * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device),
- * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will
- * always happen before #draw).
- * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on
- * splash-screen background tread can make they execute in parallel, which ensure it is faster then
- * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame.
- *
- * Here is the sequence to compare the difference between using single and two thread.
- *
- * Single thread:
- * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout
- * -> draw -> AdaptiveIconDrawable#draw
- *
- * Two threads:
- * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw)
- * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint
- * directly).
*/
@ShellSplashscreenThread
public class StartingSurfaceDrawer {
- private static final String TAG = StartingWindowController.TAG;
- private final Context mContext;
- private final DisplayManager mDisplayManager;
private final ShellExecutor mSplashScreenExecutor;
@VisibleForTesting
final SplashscreenContentDrawer mSplashscreenContentDrawer;
- private Choreographer mChoreographer;
- private final WindowManagerGlobal mWindowManagerGlobal;
- private StartingSurface.SysuiProxy mSysuiProxy;
- private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo();
-
- private static final int LIGHT_BARS_MASK =
- WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
- | WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
- /**
- * The minimum duration during which the splash screen is shown when the splash screen icon is
- * animated.
- */
- static final long MINIMAL_ANIMATION_DURATION = 400L;
-
- /**
- * Allow the icon style splash screen to be displayed for longer to give time for the animation
- * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly
- * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration.
- */
- static final long TIME_WINDOW_DURATION = 100L;
-
- /**
- * The maximum duration during which the splash screen will be shown if the application is ready
- * to show before the icon animation finishes.
- */
- static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION;
+ @VisibleForTesting
+ final SplashscreenWindowCreator mSplashscreenWindowCreator;
+ private final SnapshotWindowCreator mSnapshotWindowCreator;
+ private final WindowlessSplashWindowCreator mWindowlessSplashWindowCreator;
+ private final WindowlessSnapshotWindowCreator mWindowlessSnapshotWindowCreator;
+ @VisibleForTesting
+ final StartingWindowRecordManager mWindowRecords = new StartingWindowRecordManager();
+ // Windowless surface could co-exist with starting window in a task.
+ @VisibleForTesting
+ final StartingWindowRecordManager mWindowlessRecords = new StartingWindowRecordManager();
/**
* @param splashScreenExecutor The thread used to control add and remove starting window.
*/
public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
IconProvider iconProvider, TransactionPool pool) {
- mContext = context;
- mDisplayManager = mContext.getSystemService(DisplayManager.class);
mSplashScreenExecutor = splashScreenExecutor;
- mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool);
- mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
- mWindowManagerGlobal = WindowManagerGlobal.getInstance();
- mDisplayManager.getDisplay(DEFAULT_DISPLAY);
- }
-
- @VisibleForTesting
- final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>();
-
- /**
- * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is
- * rendered and that have not yet been removed by their client.
- */
- private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts =
- new SparseArray<>(1);
-
- private Display getDisplay(int displayId) {
- return mDisplayManager.getDisplay(displayId);
- }
-
- int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) {
- return splashScreenThemeResId != 0
- ? splashScreenThemeResId
- : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource()
- : com.android.internal.R.style.Theme_DeviceDefault_DayNight;
+ final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+ mSplashscreenContentDrawer = new SplashscreenContentDrawer(context, iconProvider, pool);
+ displayManager.getDisplay(DEFAULT_DISPLAY);
+
+ mSplashscreenWindowCreator = new SplashscreenWindowCreator(mSplashscreenContentDrawer,
+ context, splashScreenExecutor, displayManager, mWindowRecords);
+ mSnapshotWindowCreator = new SnapshotWindowCreator(splashScreenExecutor,
+ mWindowRecords);
+ mWindowlessSplashWindowCreator = new WindowlessSplashWindowCreator(
+ mSplashscreenContentDrawer, context, splashScreenExecutor, displayManager,
+ mWindowlessRecords, pool);
+ mWindowlessSnapshotWindowCreator = new WindowlessSnapshotWindowCreator(
+ mWindowlessRecords, context, displayManager, mSplashscreenContentDrawer, pool);
}
void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) {
- mSysuiProxy = sysuiProxy;
+ mSplashscreenWindowCreator.setSysuiProxy(sysuiProxy);
+ mWindowlessSplashWindowCreator.setSysuiProxy(sysuiProxy);
}
/**
@@ -189,328 +99,55 @@ public class StartingSurfaceDrawer {
*
* @param suggestType The suggestion type to draw the splash screen.
*/
- void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
+ void addSplashScreenStartingWindow(StartingWindowInfo windowInfo,
@StartingWindowType int suggestType) {
- final RunningTaskInfo taskInfo = windowInfo.taskInfo;
- final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
- ? windowInfo.targetActivityInfo
- : taskInfo.topActivityInfo;
- if (activityInfo == null || activityInfo.packageName == null) {
- return;
- }
-
- final int displayId = taskInfo.displayId;
- final int taskId = taskInfo.taskId;
-
- // replace with the default theme if the application didn't set
- final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo);
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d",
- activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType);
- final Display display = getDisplay(displayId);
- if (display == null) {
- // Can't show splash screen on requested display, so skip showing at all.
- return;
- }
- Context context = displayId == DEFAULT_DISPLAY
- ? mContext : mContext.createDisplayContext(display);
- if (context == null) {
- return;
- }
- if (theme != context.getThemeResId()) {
- try {
- context = context.createPackageContextAsUser(activityInfo.packageName,
- CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId));
- context.setTheme(theme);
- } catch (PackageManager.NameNotFoundException e) {
- Slog.w(TAG, "Failed creating package context with package name "
- + activityInfo.packageName + " for user " + taskInfo.userId, e);
- return;
- }
- }
-
- final Configuration taskConfig = taskInfo.getConfiguration();
- if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "addSplashScreen: creating context based on task Configuration %s",
- taskConfig);
- final Context overrideContext = context.createConfigurationContext(taskConfig);
- overrideContext.setTheme(theme);
- final TypedArray typedArray = overrideContext.obtainStyledAttributes(
- com.android.internal.R.styleable.Window);
- final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
- try {
- if (resId != 0 && overrideContext.getDrawable(resId) != null) {
- // We want to use the windowBackground for the override context if it is
- // available, otherwise we use the default one to make sure a themed starting
- // window is displayed for the app.
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "addSplashScreen: apply overrideConfig %s",
- taskConfig);
- context = overrideContext;
- }
- } catch (Resources.NotFoundException e) {
- Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: "
- + taskId, e);
- return;
- }
- typedArray.recycle();
- }
-
- final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
- WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
- params.setFitInsetsSides(0);
- params.setFitInsetsTypes(0);
- params.format = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
- ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT;
- int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
- | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
- final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
- if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
- windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
- }
- if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
- if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) {
- windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
- }
- } else {
- windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
- }
- params.layoutInDisplayCutoutMode = a.getInt(
- R.styleable.Window_windowLayoutInDisplayCutoutMode,
- params.layoutInDisplayCutoutMode);
- params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
- a.recycle();
-
- // Assumes it's safe to show starting windows of launched apps while
- // the keyguard is being hidden. This is okay because starting windows never show
- // secret information.
- // TODO(b/113840485): Occluded may not only happen on default display
- if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) {
- windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
- }
-
- // Force the window flags: this is a fake window, so it is not really
- // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM
- // flag because we do know that the next window will take input
- // focus, so we want to get the IME window up on top of us right away.
- // Touches will only pass through to the host activity window and will be blocked from
- // passing to any other windows.
- windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
- params.flags = windowFlags;
- params.token = appToken;
- params.packageName = activityInfo.packageName;
- params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
-
- if (!context.getResources().getCompatibilityInfo().supportsScreen()) {
- params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
- }
-
- params.setTitle("Splash Screen " + activityInfo.packageName);
-
- // TODO(b/173975965) tracking performance
- // Prepare the splash screen content view on splash screen worker thread in parallel, so the
- // content view won't be blocked by binder call like addWindow and relayout.
- // 1. Trigger splash screen worker thread to create SplashScreenView before/while
- // Session#addWindow.
- // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start
- // traversal, which will call Session#relayout on splash screen thread.
- // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at
- // the same time the splash screen thread should be executing Session#relayout. Blocking the
- // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready.
-
- // Record whether create splash screen view success, notify to current thread after
- // create splash screen view finished.
- final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
- final FrameLayout rootLayout = new FrameLayout(
- mSplashscreenContentDrawer.createViewContextWrapper(context));
- rootLayout.setPadding(0, 0, 0, 0);
- rootLayout.setFitsSystemWindows(false);
- final Runnable setViewSynchronized = () -> {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
- // waiting for setContentView before relayoutWindow
- SplashScreenView contentView = viewSupplier.get();
- final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
- // If record == null, either the starting window added fail or removed already.
- // Do not add this view if the token is mismatch.
- if (record != null && appToken == record.mAppToken) {
- // if view == null then creation of content view was failed.
- if (contentView != null) {
- try {
- rootLayout.addView(contentView);
- } catch (RuntimeException e) {
- Slog.w(TAG, "failed set content view to starting window "
- + "at taskId: " + taskId, e);
- contentView = null;
- }
- }
- record.setSplashScreenView(contentView);
- }
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- };
- if (mSysuiProxy != null) {
- mSysuiProxy.requestTopUi(true, TAG);
- }
- mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo,
- viewSupplier::setView, viewSupplier::setUiThreadInitTask);
- try {
- if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
- // We use the splash screen worker thread to create SplashScreenView while adding
- // the window, as otherwise Choreographer#doFrame might be delayed on this thread.
- // And since Choreographer#doFrame won't happen immediately after adding the window,
- // if the view is not added to the PhoneWindow on the first #doFrame, the view will
- // not be rendered on the first frame. So here we need to synchronize the view on
- // the window before first round relayoutWindow, which will happen after insets
- // animation.
- mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null);
- final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
- record.parseAppSystemBarColor(context);
- // Block until we get the background color.
- final SplashScreenView contentView = viewSupplier.get();
- if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
- contentView.addOnAttachStateChangeListener(
- new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- final int lightBarAppearance = ContrastColorUtil.isColorLight(
- contentView.getInitBackgroundColor())
- ? LIGHT_BARS_MASK : 0;
- contentView.getWindowInsetsController().setSystemBarsAppearance(
- lightBarAppearance, LIGHT_BARS_MASK);
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- }
- });
- }
- record.mBGColor = contentView.getInitBackgroundColor();
- } else {
- // release the icon view host
- final SplashScreenView contentView = viewSupplier.get();
- if (contentView.getSurfaceHost() != null) {
- SplashScreenView.releaseIconHost(contentView.getSurfaceHost());
- }
- }
- } catch (RuntimeException e) {
- // don't crash if something else bad happens, for example a
- // failure loading resources because we are loading from an app
- // on external storage that has been unmounted.
- Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e);
- }
+ mSplashscreenWindowCreator.addSplashScreenStartingWindow(windowInfo, suggestType);
}
int getStartingWindowBackgroundColorForTask(int taskId) {
- final StartingWindowRecord startingWindowRecord = mStartingWindowRecords.get(taskId);
+ final StartingWindowRecord startingWindowRecord = mWindowRecords.getRecord(taskId);
if (startingWindowRecord == null) {
return Color.TRANSPARENT;
}
- return startingWindowRecord.mBGColor;
- }
-
- private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> {
- private SplashScreenView mView;
- private boolean mIsViewSet;
- private Runnable mUiThreadInitTask;
- void setView(SplashScreenView view) {
- synchronized (this) {
- mView = view;
- mIsViewSet = true;
- notify();
- }
- }
-
- void setUiThreadInitTask(Runnable initTask) {
- synchronized (this) {
- mUiThreadInitTask = initTask;
- }
- }
-
- @Override
- @Nullable
- public SplashScreenView get() {
- synchronized (this) {
- while (!mIsViewSet) {
- try {
- wait();
- } catch (InterruptedException ignored) {
- }
- }
- if (mUiThreadInitTask != null) {
- mUiThreadInitTask.run();
- mUiThreadInitTask = null;
- }
- return mView;
- }
- }
+ return startingWindowRecord.getBGColor();
}
int estimateTaskBackgroundColor(TaskInfo taskInfo) {
- if (taskInfo.topActivityInfo == null) {
- return Color.TRANSPARENT;
- }
- final ActivityInfo activityInfo = taskInfo.topActivityInfo;
- final String packageName = activityInfo.packageName;
- final int userId = taskInfo.userId;
- final Context windowContext;
- try {
- windowContext = mContext.createPackageContextAsUser(
- packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId));
- } catch (PackageManager.NameNotFoundException e) {
- Slog.w(TAG, "Failed creating package context with package name "
- + packageName + " for user " + taskInfo.userId, e);
- return Color.TRANSPARENT;
- }
- try {
- final IPackageManager packageManager = ActivityThread.getPackageManager();
- final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName,
- userId);
- final int splashScreenThemeId = splashScreenThemeName != null
- ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null)
- : 0;
-
- final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo);
-
- if (theme != windowContext.getThemeResId()) {
- windowContext.setTheme(theme);
- }
- return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext);
- } catch (RuntimeException | RemoteException e) {
- Slog.w(TAG, "failed get starting window background color at taskId: "
- + taskInfo.taskId, e);
- }
- return Color.TRANSPARENT;
+ return mSplashscreenWindowCreator.estimateTaskBackgroundColor(taskInfo);
}
/**
* Called when a task need a snapshot starting window.
*/
- void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, IBinder appToken,
- TaskSnapshot snapshot) {
- final int taskId = startingWindowInfo.taskInfo.taskId;
- // Remove any existing starting window for this task before adding.
- removeWindowNoAnimate(taskId);
- final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken,
- snapshot, mSplashScreenExecutor, () -> removeWindowNoAnimate(taskId));
- if (surface == null) {
- return;
- }
- final StartingWindowRecord tView = new StartingWindowRecord(appToken,
- null/* decorView */, surface, STARTING_WINDOW_TYPE_SNAPSHOT);
- mStartingWindowRecords.put(taskId, tView);
+ void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, TaskSnapshot snapshot) {
+ mSnapshotWindowCreator.makeTaskSnapshotWindow(startingWindowInfo, snapshot);
}
/**
* Called when the content of a task is ready to show, starting window can be removed.
*/
public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "Task start finish, remove starting surface for task: %d",
- removalInfo.taskId);
- removeWindowSynced(removalInfo, false /* immediately */);
+ if (removalInfo.windowlessSurface) {
+ mWindowlessRecords.removeWindow(removalInfo, removalInfo.removeImmediately);
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "Task start finish, remove starting surface for task: %d",
+ removalInfo.taskId);
+ mWindowRecords.removeWindow(removalInfo, removalInfo.removeImmediately);
+ }
+ }
+
+ /**
+ * Create a windowless starting surface and attach to the root surface.
+ */
+ void addWindowlessStartingSurface(StartingWindowInfo windowInfo) {
+ if (windowInfo.taskSnapshot != null) {
+ mWindowlessSnapshotWindowCreator.makeTaskSnapshotWindow(windowInfo,
+ windowInfo.rootSurface, windowInfo.taskSnapshot, mSplashScreenExecutor);
+ } else {
+ mWindowlessSplashWindowCreator.addSplashScreenStartingWindow(
+ windowInfo, windowInfo.rootSurface);
+ }
}
/**
@@ -519,37 +156,15 @@ public class StartingSurfaceDrawer {
public void clearAllWindows() {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"Clear all starting windows immediately");
- final int taskSize = mStartingWindowRecords.size();
- final int[] taskIds = new int[taskSize];
- for (int i = taskSize - 1; i >= 0; --i) {
- taskIds[i] = mStartingWindowRecords.keyAt(i);
- }
- for (int i = taskSize - 1; i >= 0; --i) {
- removeWindowNoAnimate(taskIds[i]);
- }
+ mWindowRecords.clearAllWindows();
+ mWindowlessRecords.clearAllWindows();
}
/**
* Called when the Task wants to copy the splash screen.
*/
public void copySplashScreenView(int taskId) {
- final StartingWindowRecord preView = mStartingWindowRecords.get(taskId);
- SplashScreenViewParcelable parcelable;
- SplashScreenView splashScreenView = preView != null ? preView.mContentView : null;
- if (splashScreenView != null && splashScreenView.isCopyable()) {
- parcelable = new SplashScreenViewParcelable(splashScreenView);
- parcelable.setClientCallback(
- new RemoteCallback((bundle) -> mSplashScreenExecutor.execute(
- () -> onAppSplashScreenViewRemoved(taskId, false))));
- splashScreenView.onCopied();
- mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost());
- } else {
- parcelable = null;
- }
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "Copying splash screen window view for task: %d with parcelable %b",
- taskId, parcelable != null);
- ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable);
+ mSplashscreenWindowCreator.copySplashScreenView(taskId);
}
/**
@@ -559,195 +174,179 @@ public class StartingSurfaceDrawer {
* @param taskId The Task id on which the splash screen was attached
*/
public void onAppSplashScreenViewRemoved(int taskId) {
- onAppSplashScreenViewRemoved(taskId, true /* fromServer */);
+ mSplashscreenWindowCreator.onAppSplashScreenViewRemoved(taskId);
}
- /**
- * @param fromServer If true, this means the removal was notified by the server. This is only
- * used for debugging purposes.
- * @see #onAppSplashScreenViewRemoved(int)
- */
- private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) {
- SurfaceControlViewHost viewHost =
- mAnimatedSplashScreenSurfaceHosts.get(taskId);
- if (viewHost == null) {
- return;
+ void onImeDrawnOnTask(int taskId) {
+ onImeDrawnOnTask(mWindowRecords, taskId);
+ onImeDrawnOnTask(mWindowlessRecords, taskId);
+ }
+
+ private void onImeDrawnOnTask(StartingWindowRecordManager records, int taskId) {
+ final StartingSurfaceDrawer.StartingWindowRecord sRecord =
+ records.getRecord(taskId);
+ final SnapshotRecord record = sRecord instanceof SnapshotRecord
+ ? (SnapshotRecord) sRecord : null;
+ if (record != null && record.hasImeSurface()) {
+ records.removeWindow(taskId, true);
}
- mAnimatedSplashScreenSurfaceHosts.remove(taskId);
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "%s the splash screen. Releasing SurfaceControlViewHost for task: %d",
- fromServer ? "Server cleaned up" : "App removed", taskId);
- SplashScreenView.releaseIconHost(viewHost);
}
- protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
- WindowManager.LayoutParams params, @StartingWindowType int suggestType) {
- boolean shouldSaveView = true;
- final Context context = view.getContext();
- try {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
- mWindowManagerGlobal.addView(view, params, display,
- null /* parentWindow */, context.getUserId());
- } catch (WindowManager.BadTokenException e) {
- // ignore
- Slog.w(TAG, appToken + " already running, starting window not displayed. "
- + e.getMessage());
- shouldSaveView = false;
- } finally {
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- if (view.getParent() == null) {
- Slog.w(TAG, "view not successfully added to wm, removing view");
- mWindowManagerGlobal.removeView(view, true /* immediate */);
- shouldSaveView = false;
+ static class WindowlessStartingWindow extends WindowlessWindowManager {
+ SurfaceControl mChildSurface;
+
+ WindowlessStartingWindow(Configuration c, SurfaceControl rootSurface) {
+ super(c, rootSurface, null /* hostInputToken */);
+ }
+
+ @Override
+ protected SurfaceControl getParentSurface(IWindow window,
+ WindowManager.LayoutParams attrs) {
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ .setContainerLayer()
+ .setName("Windowless window")
+ .setHidden(false)
+ .setParent(mRootSurface)
+ .setCallsite("WindowlessStartingWindow#attachToParentSurface");
+ mChildSurface = builder.build();
+ try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) {
+ t.setLayer(mChildSurface, Integer.MAX_VALUE);
+ t.apply();
}
+ return mChildSurface;
}
- if (shouldSaveView) {
- removeWindowNoAnimate(taskId);
- saveSplashScreenRecord(appToken, taskId, view, suggestType);
+ }
+ abstract static class StartingWindowRecord {
+ protected int mBGColor;
+
+ /**
+ * Remove the starting window with the given {@link StartingWindowRemovalInfo} if possible.
+ * @param info The removal info sent from the task organizer controller in the WM core.
+ * @param immediately {@code true} means removing the starting window immediately,
+ * {@code false} otherwise.
+ * @return {@code true} means {@link StartingWindowRecordManager} can safely remove the
+ * record itself. {@code false} means {@link StartingWindowRecordManager} requires
+ * to manage the record reference and remove it later.
+ */
+ abstract boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately);
+ int getBGColor() {
+ return mBGColor;
}
- return shouldSaveView;
}
- @VisibleForTesting
- void saveSplashScreenRecord(IBinder appToken, int taskId, View view,
- @StartingWindowType int suggestType) {
- final StartingWindowRecord tView = new StartingWindowRecord(appToken, view,
- null/* TaskSnapshotWindow */, suggestType);
- mStartingWindowRecords.put(taskId, tView);
- }
+ abstract static class SnapshotRecord extends StartingWindowRecord {
+ private static final long DELAY_REMOVAL_TIME_GENERAL = 100;
+ /**
+ * The max delay time in milliseconds for removing the task snapshot window with IME
+ * visible.
+ * Ideally the delay time will be shorter when receiving
+ * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}.
+ */
+ private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600;
- private void removeWindowNoAnimate(int taskId) {
- mTmpRemovalInfo.taskId = taskId;
- removeWindowSynced(mTmpRemovalInfo, true /* immediately */);
- }
+ /**
+ * The max delay time in milliseconds for removing the task snapshot window with IME
+ * visible after the fixed rotation finished.
+ * Ideally the delay time will be shorter when receiving
+ * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}.
+ */
+ private static final long MAX_DELAY_REMOVAL_TIME_FIXED_ROTATION = 3000;
- void onImeDrawnOnTask(int taskId) {
- final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
- if (record != null && record.mTaskSnapshotWindow != null
- && record.mTaskSnapshotWindow.hasImeSurface()) {
- removeWindowNoAnimate(taskId);
+ private final Runnable mScheduledRunnable = this::removeImmediately;
+
+ @WindowConfiguration.ActivityType protected final int mActivityType;
+ protected final ShellExecutor mRemoveExecutor;
+
+ SnapshotRecord(int activityType, ShellExecutor removeExecutor) {
+ mActivityType = activityType;
+ mRemoveExecutor = removeExecutor;
}
- }
- protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, boolean immediately) {
- final int taskId = removalInfo.taskId;
- final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
- if (record != null) {
- if (record.mDecorView != null) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "Removing splash screen window for task: %d", taskId);
- if (record.mContentView != null) {
- record.clearSystemBarColor();
- if (immediately
- || record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
- removeWindowInner(record.mDecorView, false);
- } else {
- if (removalInfo.playRevealAnimation) {
- mSplashscreenContentDrawer.applyExitAnimation(record.mContentView,
- removalInfo.windowAnimationLeash, removalInfo.mainFrame,
- () -> removeWindowInner(record.mDecorView, true),
- record.mCreateTime, removalInfo.roundedCornerRadius);
- } else {
- // the SplashScreenView has been copied to client, hide the view to skip
- // default exit animation
- removeWindowInner(record.mDecorView, true);
- }
- }
- } else {
- // shouldn't happen
- Slog.e(TAG, "Found empty splash screen, remove!");
- removeWindowInner(record.mDecorView, false);
- }
+ @Override
+ public final boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
+ if (immediately) {
+ removeImmediately();
+ } else {
+ scheduleRemove(info.deferRemoveForImeMode);
+ return false;
+ }
+ return true;
+ }
+ void scheduleRemove(@StartingWindowRemovalInfo.DeferMode int deferRemoveForImeMode) {
+ // Show the latest content as soon as possible for unlocking to home.
+ if (mActivityType == ACTIVITY_TYPE_HOME) {
+ removeImmediately();
+ return;
}
- if (record.mTaskSnapshotWindow != null) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "Removing task snapshot window for %d", taskId);
- if (immediately) {
- record.mTaskSnapshotWindow.removeImmediately();
- } else {
- record.mTaskSnapshotWindow.scheduleRemove(removalInfo.deferRemoveForIme);
- }
+ mRemoveExecutor.removeCallbacks(mScheduledRunnable);
+ final long delayRemovalTime;
+ switch (deferRemoveForImeMode) {
+ case DEFER_MODE_ROTATION:
+ delayRemovalTime = MAX_DELAY_REMOVAL_TIME_FIXED_ROTATION;
+ break;
+ case DEFER_MODE_NORMAL:
+ delayRemovalTime = MAX_DELAY_REMOVAL_TIME_IME_VISIBLE;
+ break;
+ default:
+ delayRemovalTime = DELAY_REMOVAL_TIME_GENERAL;
}
- mStartingWindowRecords.remove(taskId);
+ mRemoveExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "Defer removing snapshot surface in %d", delayRemovalTime);
}
- }
- private void removeWindowInner(View decorView, boolean hideView) {
- if (mSysuiProxy != null) {
- mSysuiProxy.requestTopUi(false, TAG);
- }
- if (hideView) {
- decorView.setVisibility(View.GONE);
+ protected abstract boolean hasImeSurface();
+
+ @CallSuper
+ protected void removeImmediately() {
+ mRemoveExecutor.removeCallbacks(mScheduledRunnable);
}
- mWindowManagerGlobal.removeView(decorView, false /* immediate */);
}
- /**
- * Record the view or surface for a starting window.
- */
- private static class StartingWindowRecord {
- private final IBinder mAppToken;
- private final View mDecorView;
- private final TaskSnapshotWindow mTaskSnapshotWindow;
- private SplashScreenView mContentView;
- private boolean mSetSplashScreen;
- @StartingWindowType private int mSuggestType;
- private int mBGColor;
- private final long mCreateTime;
- private int mSystemBarAppearance;
- private boolean mDrawsSystemBarBackgrounds;
-
- StartingWindowRecord(IBinder appToken, View decorView,
- TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType) {
- mAppToken = appToken;
- mDecorView = decorView;
- mTaskSnapshotWindow = taskSnapshotWindow;
- if (mTaskSnapshotWindow != null) {
- mBGColor = mTaskSnapshotWindow.getBackgroundColor();
- }
- mSuggestType = suggestType;
- mCreateTime = SystemClock.uptimeMillis();
- }
+ static class StartingWindowRecordManager {
+ private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo();
+ private final SparseArray<StartingWindowRecord> mStartingWindowRecords =
+ new SparseArray<>();
- private void setSplashScreenView(SplashScreenView splashScreenView) {
- if (mSetSplashScreen) {
- return;
+ void clearAllWindows() {
+ final int taskSize = mStartingWindowRecords.size();
+ final int[] taskIds = new int[taskSize];
+ for (int i = taskSize - 1; i >= 0; --i) {
+ taskIds[i] = mStartingWindowRecords.keyAt(i);
+ }
+ for (int i = taskSize - 1; i >= 0; --i) {
+ removeWindow(taskIds[i], true);
}
- mContentView = splashScreenView;
- mSetSplashScreen = true;
}
- private void parseAppSystemBarColor(Context context) {
- final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
- mDrawsSystemBarBackgrounds = a.getBoolean(
- R.styleable.Window_windowDrawsSystemBarBackgrounds, false);
- if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
- mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
- }
- if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) {
- mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
- }
- a.recycle();
+ void addRecord(int taskId, StartingWindowRecord record) {
+ mStartingWindowRecords.put(taskId, record);
}
- // Reset the system bar color which set by splash screen, make it align to the app.
- private void clearSystemBarColor() {
- if (mDecorView == null || !mDecorView.isAttachedToWindow()) {
- return;
- }
- if (mDecorView.getLayoutParams() instanceof WindowManager.LayoutParams) {
- final WindowManager.LayoutParams lp =
- (WindowManager.LayoutParams) mDecorView.getLayoutParams();
- if (mDrawsSystemBarBackgrounds) {
- lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
- } else {
- lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ void removeWindow(StartingWindowRemovalInfo removeInfo, boolean immediately) {
+ final int taskId = removeInfo.taskId;
+ final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
+ if (record != null) {
+ final boolean canRemoveRecord = record.removeIfPossible(removeInfo, immediately);
+ if (canRemoveRecord) {
+ mStartingWindowRecords.remove(taskId);
}
- mDecorView.setLayoutParams(lp);
}
- mDecorView.getWindowInsetsController().setSystemBarsAppearance(
- mSystemBarAppearance, LIGHT_BARS_MASK);
+ }
+
+ void removeWindow(int taskId, boolean immediately) {
+ mTmpRemovalInfo.taskId = taskId;
+ removeWindow(mTmpRemovalInfo, immediately);
+ }
+
+ StartingWindowRecord getRecord(int taskId) {
+ return mStartingWindowRecords.get(taskId);
+ }
+
+ @VisibleForTesting
+ int recordSize() {
+ return mStartingWindowRecords.size();
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index be2e79342d07..bec4ba3bf0d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -21,6 +21,7 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
@@ -29,7 +30,6 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
import android.content.Context;
import android.graphics.Color;
-import android.os.IBinder;
import android.os.Trace;
import android.util.SparseIntArray;
import android.window.StartingWindowInfo;
@@ -152,22 +152,23 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo
/**
* Called when a task need a starting window.
*/
- public void addStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) {
+ public void addStartingWindow(StartingWindowInfo windowInfo) {
mSplashScreenExecutor.execute(() -> {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addStartingWindow");
final int suggestionType = mStartingWindowTypeAlgorithm.getSuggestedWindowType(
windowInfo);
final RunningTaskInfo runningTaskInfo = windowInfo.taskInfo;
- if (isSplashScreenType(suggestionType)) {
- mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, appToken,
- suggestionType);
+ if (suggestionType == STARTING_WINDOW_TYPE_WINDOWLESS) {
+ mStartingSurfaceDrawer.addWindowlessStartingSurface(windowInfo);
+ } else if (isSplashScreenType(suggestionType)) {
+ mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, suggestionType);
} else if (suggestionType == STARTING_WINDOW_TYPE_SNAPSHOT) {
final TaskSnapshot snapshot = windowInfo.taskSnapshot;
- mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, appToken,
- snapshot);
+ mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, snapshot);
}
- if (suggestionType != STARTING_WINDOW_TYPE_NONE) {
+ if (suggestionType != STARTING_WINDOW_TYPE_NONE
+ && suggestionType != STARTING_WINDOW_TYPE_WINDOWLESS) {
int taskId = runningTaskInfo.taskId;
int color = mStartingSurfaceDrawer
.getStartingWindowBackgroundColorForTask(taskId);
@@ -218,11 +219,13 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo
public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.removeStartingWindow(
removalInfo));
- mSplashScreenExecutor.executeDelayed(() -> {
- synchronized (mTaskBackgroundColors) {
- mTaskBackgroundColors.delete(removalInfo.taskId);
- }
- }, TASK_BG_COLOR_RETAIN_TIME_MS);
+ if (!removalInfo.windowlessSurface) {
+ mSplashScreenExecutor.executeDelayed(() -> {
+ synchronized (mTaskBackgroundColors) {
+ mTaskBackgroundColors.delete(removalInfo.taskId);
+ }
+ }, TASK_BG_COLOR_RETAIN_TIME_MS);
+ }
}
/**
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 7b498e4f54ec..c2f15f6cba75 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
@@ -16,52 +16,18 @@
package com.android.wm.shell.startingsurface;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.graphics.Color.WHITE;
-import static android.graphics.Color.alpha;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
-import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
-import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
-import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
-import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
-import static android.view.WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
-import static android.view.WindowManager.LayoutParams.FLAG_SCALED;
-import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
-import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
-import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
-import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
-import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
-import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_USE_BLAST;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
-import static com.android.internal.policy.DecorView.NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES;
-import static com.android.internal.policy.DecorView.STATUS_BAR_COLOR_VIEW_ATTRIBUTES;
-import static com.android.internal.policy.DecorView.getNavigationBarRect;
-
import android.annotation.BinderThread;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskDescription;
-import android.app.ActivityThread;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.GraphicBuffer;
-import android.graphics.Matrix;
import android.graphics.Paint;
-import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
-import android.graphics.RectF;
-import android.hardware.HardwareBuffer;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
@@ -73,19 +39,14 @@ import android.view.InputChannel;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.window.ClientWindowFrames;
+import android.window.SnapshotDrawerUtils;
import android.window.StartingWindowInfo;
import android.window.TaskSnapshot;
-import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.DecorView;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.view.BaseIWindow;
import com.android.wm.shell.common.ShellExecutor;
@@ -99,59 +60,17 @@ import java.lang.ref.WeakReference;
* @hide
*/
public class TaskSnapshotWindow {
- /**
- * When creating the starting window, we use the exact same layout flags such that we end up
- * with a window with the exact same dimensions etc. However, these flags are not used in layout
- * and might cause other side effects so we exclude them.
- */
- static final int FLAG_INHERIT_EXCLUDES = FLAG_NOT_FOCUSABLE
- | FLAG_NOT_TOUCHABLE
- | FLAG_NOT_TOUCH_MODAL
- | FLAG_ALT_FOCUSABLE_IM
- | FLAG_NOT_FOCUSABLE
- | FLAG_HARDWARE_ACCELERATED
- | FLAG_IGNORE_CHEEK_PRESSES
- | FLAG_LOCAL_FOCUS_MODE
- | FLAG_SLIPPERY
- | FLAG_WATCH_OUTSIDE_TOUCH
- | FLAG_SPLIT_TOUCH
- | FLAG_SCALED
- | FLAG_SECURE;
-
private static final String TAG = StartingWindowController.TAG;
- private static final String TITLE_FORMAT = "SnapshotStartingWindow for taskId=%s";
-
- private static final long DELAY_REMOVAL_TIME_GENERAL = 100;
- /**
- * The max delay time in milliseconds for removing the task snapshot window with IME visible.
- * Ideally the delay time will be shorter when receiving
- * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}.
- */
- private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600;
+ private static final String TITLE_FORMAT = "SnapshotStartingWindow for taskId=";
private final Window mWindow;
private final Runnable mClearWindowHandler;
private final ShellExecutor mSplashScreenExecutor;
- private final SurfaceControl mSurfaceControl;
private final IWindowSession mSession;
- private final Rect mTaskBounds;
- private final Rect mFrame = new Rect();
- private final Rect mSystemBarInsets = new Rect();
- private TaskSnapshot mSnapshot;
- private final RectF mTmpSnapshotSize = new RectF();
- private final RectF mTmpDstFrame = new RectF();
- private final CharSequence mTitle;
private boolean mHasDrawn;
- private boolean mSizeMismatch;
private final Paint mBackgroundPaint = new Paint();
- private final int mActivityType;
- private final int mStatusBarColor;
- private final SystemBarBackgroundPainter mSystemBarBackgroundPainter;
private final int mOrientationOnCreation;
- private final SurfaceControl.Transaction mTransaction;
- private final Matrix mSnapshotMatrix = new Matrix();
- private final float[] mTmpFloat9 = new float[9];
- private final Runnable mScheduledRunnable = this::removeImmediately;
+
private final boolean mHasImeSurface;
static TaskSnapshotWindow create(StartingWindowInfo info, IBinder appToken,
@@ -159,71 +78,44 @@ public class TaskSnapshotWindow {
@NonNull Runnable clearWindowHandler) {
final ActivityManager.RunningTaskInfo runningTaskInfo = info.taskInfo;
final int taskId = runningTaskInfo.taskId;
+
+ // if we're in PIP we don't want to create the snapshot
+ if (runningTaskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+ "did not create taskSnapshot due to being in PIP");
+ return null;
+ }
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"create taskSnapshot surface for task: %d", taskId);
- final WindowManager.LayoutParams attrs = info.topOpaqueWindowLayoutParams;
- final WindowManager.LayoutParams mainWindowParams = info.mainWindowLayoutParams;
final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState;
- if (attrs == null || mainWindowParams == null || topWindowInsetsState == null) {
- Slog.w(TAG, "unable to create taskSnapshot surface for task: " + taskId);
+
+ final WindowManager.LayoutParams layoutParams = SnapshotDrawerUtils.createLayoutParameters(
+ info, TITLE_FORMAT + taskId, TYPE_APPLICATION_STARTING,
+ snapshot.getHardwareBuffer().getFormat(), appToken);
+ if (layoutParams == null) {
+ Slog.e(TAG, "TaskSnapshotWindow no layoutParams");
return null;
}
- final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
-
- final int appearance = attrs.insetsFlags.appearance;
- final int windowFlags = attrs.flags;
- final int windowPrivateFlags = attrs.privateFlags;
-
- layoutParams.packageName = mainWindowParams.packageName;
- layoutParams.windowAnimations = mainWindowParams.windowAnimations;
- layoutParams.dimAmount = mainWindowParams.dimAmount;
- layoutParams.type = TYPE_APPLICATION_STARTING;
- layoutParams.format = snapshot.getHardwareBuffer().getFormat();
- layoutParams.flags = (windowFlags & ~FLAG_INHERIT_EXCLUDES)
- | FLAG_NOT_FOCUSABLE
- | FLAG_NOT_TOUCHABLE;
- // Setting as trusted overlay to let touches pass through. This is safe because this
- // window is controlled by the system.
- layoutParams.privateFlags = (windowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS)
- | PRIVATE_FLAG_TRUSTED_OVERLAY | PRIVATE_FLAG_USE_BLAST;
- layoutParams.token = appToken;
- layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
- layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
- layoutParams.insetsFlags.appearance = appearance;
- layoutParams.insetsFlags.behavior = attrs.insetsFlags.behavior;
- layoutParams.layoutInDisplayCutoutMode = attrs.layoutInDisplayCutoutMode;
- layoutParams.setFitInsetsTypes(attrs.getFitInsetsTypes());
- layoutParams.setFitInsetsSides(attrs.getFitInsetsSides());
- layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility());
-
- layoutParams.setTitle(String.format(TITLE_FORMAT, taskId));
final Point taskSize = snapshot.getTaskSize();
final Rect taskBounds = new Rect(0, 0, taskSize.x, taskSize.y);
final int orientation = snapshot.getOrientation();
- final int activityType = runningTaskInfo.topActivityType;
final int displayId = runningTaskInfo.displayId;
final IWindowSession session = WindowManagerGlobal.getWindowSession();
final SurfaceControl surfaceControl = new SurfaceControl();
final ClientWindowFrames tmpFrames = new ClientWindowFrames();
- final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0];
+ final InsetsSourceControl.Array tmpControls = new InsetsSourceControl.Array();
final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration();
- final TaskDescription taskDescription;
- if (runningTaskInfo.taskDescription != null) {
- taskDescription = runningTaskInfo.taskDescription;
- } else {
- taskDescription = new TaskDescription();
- taskDescription.setBackgroundColor(WHITE);
- }
+ final TaskDescription taskDescription =
+ SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo);
final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow(
- surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance,
- windowFlags, windowPrivateFlags, taskBounds, orientation, activityType,
- topWindowInsetsState, clearWindowHandler, splashScreenExecutor);
+ snapshot, taskDescription, orientation,
+ clearWindowHandler, splashScreenExecutor);
final Window window = snapshotSurface.mWindow;
final InsetsState tmpInsetsState = new InsetsState();
@@ -233,7 +125,7 @@ public class TaskSnapshotWindow {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay");
final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId,
- info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls,
+ info.requestedVisibleTypes, tmpInputChannel, tmpInsetsState, tmpControls,
new Rect(), sizeCompatScale);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (res < 0) {
@@ -252,35 +144,28 @@ public class TaskSnapshotWindow {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
} catch (RemoteException e) {
snapshotSurface.clearWindowSynced();
+ Slog.w(TAG, "Failed to relayout snapshot starting window");
+ return null;
}
- final Rect systemBarInsets = getSystemBarInsets(tmpFrames.frame, topWindowInsetsState);
- snapshotSurface.setFrames(tmpFrames.frame, systemBarInsets);
- snapshotSurface.drawSnapshot();
+ SnapshotDrawerUtils.drawSnapshotOnSurface(info, layoutParams, surfaceControl, snapshot,
+ taskBounds, tmpFrames.frame, topWindowInsetsState, true /* releaseAfterDraw */);
+ snapshotSurface.mHasDrawn = true;
+ snapshotSurface.reportDrawn();
+
return snapshotSurface;
}
- public TaskSnapshotWindow(SurfaceControl surfaceControl,
- TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription,
- int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds,
- int currentOrientation, int activityType, InsetsState topWindowInsetsState,
- Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) {
+ public TaskSnapshotWindow(TaskSnapshot snapshot, TaskDescription taskDescription,
+ int currentOrientation, Runnable clearWindowHandler,
+ ShellExecutor splashScreenExecutor) {
mSplashScreenExecutor = splashScreenExecutor;
mSession = WindowManagerGlobal.getWindowSession();
mWindow = new Window();
mWindow.setSession(mSession);
- mSurfaceControl = surfaceControl;
- mSnapshot = snapshot;
- mTitle = title;
int backgroundColor = taskDescription.getBackgroundColor();
mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE);
- mTaskBounds = taskBounds;
- mSystemBarBackgroundPainter = new SystemBarBackgroundPainter(windowFlags,
- windowPrivateFlags, appearance, taskDescription, 1f, topWindowInsetsState);
- mStatusBarColor = taskDescription.getStatusBarColor();
mOrientationOnCreation = currentOrientation;
- mActivityType = activityType;
- mTransaction = new SurfaceControl.Transaction();
mClearWindowHandler = clearWindowHandler;
mHasImeSurface = snapshot.hasImeSurface();
}
@@ -293,40 +178,7 @@ public class TaskSnapshotWindow {
return mHasImeSurface;
}
- /**
- * Ask system bar background painter to draw status bar background.
- * @hide
- */
- public void drawStatusBarBackground(Canvas c, @Nullable Rect alreadyDrawnFrame) {
- mSystemBarBackgroundPainter.drawStatusBarBackground(c, alreadyDrawnFrame,
- mSystemBarBackgroundPainter.getStatusBarColorViewHeight());
- }
-
- /**
- * Ask system bar background painter to draw navigation bar background.
- * @hide
- */
- public void drawNavigationBarBackground(Canvas c) {
- mSystemBarBackgroundPainter.drawNavigationBarBackground(c);
- }
-
- void scheduleRemove(boolean deferRemoveForIme) {
- // Show the latest content as soon as possible for unlocking to home.
- if (mActivityType == ACTIVITY_TYPE_HOME) {
- removeImmediately();
- return;
- }
- mSplashScreenExecutor.removeCallbacks(mScheduledRunnable);
- final long delayRemovalTime = mHasImeSurface && deferRemoveForIme
- ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE
- : DELAY_REMOVAL_TIME_GENERAL;
- mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime);
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "Defer removing snapshot surface in %d", delayRemovalTime);
- }
-
void removeImmediately() {
- mSplashScreenExecutor.removeCallbacks(mScheduledRunnable);
try {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"Removing taskSnapshot surface, mHasDrawn=%b", mHasDrawn);
@@ -337,178 +189,6 @@ public class TaskSnapshotWindow {
}
/**
- * Set frame size.
- * @hide
- */
- public void setFrames(Rect frame, Rect systemBarInsets) {
- mFrame.set(frame);
- mSystemBarInsets.set(systemBarInsets);
- final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer();
- mSizeMismatch = (mFrame.width() != snapshot.getWidth()
- || mFrame.height() != snapshot.getHeight());
- mSystemBarBackgroundPainter.setInsets(systemBarInsets);
- }
-
- static Rect getSystemBarInsets(Rect frame, InsetsState state) {
- return state.calculateInsets(frame, WindowInsets.Type.systemBars(),
- false /* ignoreVisibility */).toRect();
- }
-
- private void drawSnapshot() {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
- "Drawing snapshot surface sizeMismatch=%b", mSizeMismatch);
- if (mSizeMismatch) {
- // The dimensions of the buffer and the window don't match, so attaching the buffer
- // will fail. Better create a child window with the exact dimensions and fill the parent
- // window with the background color!
- drawSizeMismatchSnapshot();
- } else {
- drawSizeMatchSnapshot();
- }
- mHasDrawn = true;
- reportDrawn();
-
- // In case window manager leaks us, make sure we don't retain the snapshot.
- if (mSnapshot.getHardwareBuffer() != null) {
- mSnapshot.getHardwareBuffer().close();
- }
- mSnapshot = null;
- mSurfaceControl.release();
- }
-
- private void drawSizeMatchSnapshot() {
- mTransaction.setBuffer(mSurfaceControl, mSnapshot.getHardwareBuffer())
- .setColorSpace(mSurfaceControl, mSnapshot.getColorSpace())
- .apply();
- }
-
- private void drawSizeMismatchSnapshot() {
- final HardwareBuffer buffer = mSnapshot.getHardwareBuffer();
- final SurfaceSession session = new SurfaceSession();
-
- // We consider nearly matched dimensions as there can be rounding errors and the user won't
- // notice very minute differences from scaling one dimension more than the other
- final boolean aspectRatioMismatch = Math.abs(
- ((float) buffer.getWidth() / buffer.getHeight())
- - ((float) mFrame.width() / mFrame.height())) > 0.01f;
-
- // Keep a reference to it such that it doesn't get destroyed when finalized.
- SurfaceControl childSurfaceControl = new SurfaceControl.Builder(session)
- .setName(mTitle + " - task-snapshot-surface")
- .setBLASTLayer()
- .setFormat(buffer.getFormat())
- .setParent(mSurfaceControl)
- .setCallsite("TaskSnapshotWindow.drawSizeMismatchSnapshot")
- .build();
-
- final Rect frame;
- // We can just show the surface here as it will still be hidden as the parent is
- // still hidden.
- mTransaction.show(childSurfaceControl);
- if (aspectRatioMismatch) {
- // Clip off ugly navigation bar.
- final Rect crop = calculateSnapshotCrop();
- frame = calculateSnapshotFrame(crop);
- mTransaction.setWindowCrop(childSurfaceControl, crop);
- mTransaction.setPosition(childSurfaceControl, frame.left, frame.top);
- mTmpSnapshotSize.set(crop);
- mTmpDstFrame.set(frame);
- } else {
- frame = null;
- mTmpSnapshotSize.set(0, 0, buffer.getWidth(), buffer.getHeight());
- mTmpDstFrame.set(mFrame);
- mTmpDstFrame.offsetTo(0, 0);
- }
-
- // Scale the mismatch dimensions to fill the task bounds
- mSnapshotMatrix.setRectToRect(mTmpSnapshotSize, mTmpDstFrame, Matrix.ScaleToFit.FILL);
- mTransaction.setMatrix(childSurfaceControl, mSnapshotMatrix, mTmpFloat9);
- mTransaction.setColorSpace(childSurfaceControl, mSnapshot.getColorSpace());
- mTransaction.setBuffer(childSurfaceControl, mSnapshot.getHardwareBuffer());
-
- if (aspectRatioMismatch) {
- GraphicBuffer background = GraphicBuffer.create(mFrame.width(), mFrame.height(),
- PixelFormat.RGBA_8888,
- GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_HW_COMPOSER
- | GraphicBuffer.USAGE_SW_WRITE_RARELY);
- // TODO: Support this on HardwareBuffer
- final Canvas c = background.lockCanvas();
- drawBackgroundAndBars(c, frame);
- background.unlockCanvasAndPost(c);
- mTransaction.setBuffer(mSurfaceControl,
- HardwareBuffer.createFromGraphicBuffer(background));
- }
- mTransaction.apply();
- childSurfaceControl.release();
- }
-
- /**
- * Calculates the snapshot crop in snapshot coordinate space.
- *
- * @return crop rect in snapshot coordinate space.
- */
- public Rect calculateSnapshotCrop() {
- final Rect rect = new Rect();
- final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer();
- rect.set(0, 0, snapshot.getWidth(), snapshot.getHeight());
- final Rect insets = mSnapshot.getContentInsets();
-
- final float scaleX = (float) snapshot.getWidth() / mSnapshot.getTaskSize().x;
- final float scaleY = (float) snapshot.getHeight() / mSnapshot.getTaskSize().y;
-
- // Let's remove all system decorations except the status bar, but only if the task is at the
- // very top of the screen.
- final boolean isTop = mTaskBounds.top == 0 && mFrame.top == 0;
- rect.inset((int) (insets.left * scaleX),
- isTop ? 0 : (int) (insets.top * scaleY),
- (int) (insets.right * scaleX),
- (int) (insets.bottom * scaleY));
- return rect;
- }
-
- /**
- * Calculates the snapshot frame in window coordinate space from crop.
- *
- * @param crop rect that is in snapshot coordinate space.
- */
- public Rect calculateSnapshotFrame(Rect crop) {
- final HardwareBuffer snapshot = mSnapshot.getHardwareBuffer();
- final float scaleX = (float) snapshot.getWidth() / mSnapshot.getTaskSize().x;
- final float scaleY = (float) snapshot.getHeight() / mSnapshot.getTaskSize().y;
-
- // Rescale the frame from snapshot to window coordinate space
- final Rect frame = new Rect(0, 0,
- (int) (crop.width() / scaleX + 0.5f),
- (int) (crop.height() / scaleY + 0.5f)
- );
-
- // However, we also need to make space for the navigation bar on the left side.
- frame.offset(mSystemBarInsets.left, 0);
- return frame;
- }
-
- /**
- * Draw status bar and navigation bar background.
- * @hide
- */
- public void drawBackgroundAndBars(Canvas c, Rect frame) {
- final int statusBarHeight = mSystemBarBackgroundPainter.getStatusBarColorViewHeight();
- final boolean fillHorizontally = c.getWidth() > frame.right;
- final boolean fillVertically = c.getHeight() > frame.bottom;
- if (fillHorizontally) {
- c.drawRect(frame.right, alpha(mStatusBarColor) == 0xFF ? statusBarHeight : 0,
- c.getWidth(), fillVertically
- ? frame.bottom
- : c.getHeight(),
- mBackgroundPaint);
- }
- if (fillVertically) {
- c.drawRect(0, frame.bottom, c.getWidth(), c.getHeight(), mBackgroundPaint);
- }
- mSystemBarBackgroundPainter.drawDecors(c, frame);
- }
-
- /**
* Clear window from drawer, must be post on main executor.
*/
private void clearWindowSynced() {
@@ -535,7 +215,7 @@ public class TaskSnapshotWindow {
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId,
- int resizeMode) {
+ boolean dragResizing) {
final TaskSnapshotWindow snapshot = mOuter.get();
if (snapshot == null) {
return;
@@ -556,91 +236,4 @@ public class TaskSnapshotWindow {
});
}
}
-
- /**
- * Helper class to draw the background of the system bars in regions the task snapshot isn't
- * filling the window.
- */
- static class SystemBarBackgroundPainter {
- private final Paint mStatusBarPaint = new Paint();
- private final Paint mNavigationBarPaint = new Paint();
- private final int mStatusBarColor;
- private final int mNavigationBarColor;
- private final int mWindowFlags;
- private final int mWindowPrivateFlags;
- private final float mScale;
- private final InsetsState mInsetsState;
- private final Rect mSystemBarInsets = new Rect();
-
- SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int appearance,
- TaskDescription taskDescription, float scale, InsetsState insetsState) {
- mWindowFlags = windowFlags;
- mWindowPrivateFlags = windowPrivateFlags;
- mScale = scale;
- final Context context = ActivityThread.currentActivityThread().getSystemUiContext();
- final int semiTransparent = context.getColor(
- R.color.system_bar_background_semi_transparent);
- mStatusBarColor = DecorView.calculateBarColor(windowFlags, FLAG_TRANSLUCENT_STATUS,
- semiTransparent, taskDescription.getStatusBarColor(), appearance,
- APPEARANCE_LIGHT_STATUS_BARS,
- taskDescription.getEnsureStatusBarContrastWhenTransparent());
- mNavigationBarColor = DecorView.calculateBarColor(windowFlags,
- FLAG_TRANSLUCENT_NAVIGATION, semiTransparent,
- taskDescription.getNavigationBarColor(), appearance,
- APPEARANCE_LIGHT_NAVIGATION_BARS,
- taskDescription.getEnsureNavigationBarContrastWhenTransparent()
- && context.getResources().getBoolean(R.bool.config_navBarNeedsScrim));
- mStatusBarPaint.setColor(mStatusBarColor);
- mNavigationBarPaint.setColor(mNavigationBarColor);
- mInsetsState = insetsState;
- }
-
- void setInsets(Rect systemBarInsets) {
- mSystemBarInsets.set(systemBarInsets);
- }
-
- int getStatusBarColorViewHeight() {
- final boolean forceBarBackground =
- (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0;
- if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible(
- mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) {
- return (int) (mSystemBarInsets.top * mScale);
- } else {
- return 0;
- }
- }
-
- private boolean isNavigationBarColorViewVisible() {
- final boolean forceBarBackground =
- (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0;
- return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible(
- mInsetsState, mNavigationBarColor, mWindowFlags, forceBarBackground);
- }
-
- void drawDecors(Canvas c, @Nullable Rect alreadyDrawnFrame) {
- drawStatusBarBackground(c, alreadyDrawnFrame, getStatusBarColorViewHeight());
- drawNavigationBarBackground(c);
- }
-
- void drawStatusBarBackground(Canvas c, @Nullable Rect alreadyDrawnFrame,
- int statusBarHeight) {
- if (statusBarHeight > 0 && Color.alpha(mStatusBarColor) != 0
- && (alreadyDrawnFrame == null || c.getWidth() > alreadyDrawnFrame.right)) {
- final int rightInset = (int) (mSystemBarInsets.right * mScale);
- final int left = alreadyDrawnFrame != null ? alreadyDrawnFrame.right : 0;
- c.drawRect(left, 0, c.getWidth() - rightInset, statusBarHeight, mStatusBarPaint);
- }
- }
-
- @VisibleForTesting
- void drawNavigationBarBackground(Canvas c) {
- final Rect navigationBarRect = new Rect();
- getNavigationBarRect(c.getWidth(), c.getHeight(), mSystemBarInsets, navigationBarRect,
- mScale);
- final boolean visible = isNavigationBarColorViewVisible();
- if (visible && Color.alpha(mNavigationBarColor) != 0 && !navigationBarRect.isEmpty()) {
- c.drawRect(navigationBarRect, mNavigationBarPaint);
- }
- }
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
new file mode 100644
index 000000000000..144547885501
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
@@ -0,0 +1,165 @@
+/*
+ * 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.startingsurface;
+
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.window.SnapshotDrawerUtils;
+import android.window.StartingWindowInfo;
+import android.window.TaskSnapshot;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+
+class WindowlessSnapshotWindowCreator {
+ private static final int DEFAULT_FADEOUT_DURATION = 233;
+ private final StartingSurfaceDrawer.StartingWindowRecordManager
+ mStartingWindowRecordManager;
+ private final DisplayManager mDisplayManager;
+ private final Context mContext;
+ private final SplashscreenContentDrawer mSplashscreenContentDrawer;
+ private final TransactionPool mTransactionPool;
+
+ WindowlessSnapshotWindowCreator(
+ StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager,
+ Context context,
+ DisplayManager displayManager, SplashscreenContentDrawer splashscreenContentDrawer,
+ TransactionPool transactionPool) {
+ mStartingWindowRecordManager = startingWindowRecordManager;
+ mContext = context;
+ mDisplayManager = displayManager;
+ mSplashscreenContentDrawer = splashscreenContentDrawer;
+ mTransactionPool = transactionPool;
+ }
+
+ void makeTaskSnapshotWindow(StartingWindowInfo info, SurfaceControl rootSurface,
+ TaskSnapshot snapshot, ShellExecutor removeExecutor) {
+ final ActivityManager.RunningTaskInfo runningTaskInfo = info.taskInfo;
+ final int taskId = runningTaskInfo.taskId;
+ final String title = "Windowless Snapshot " + taskId;
+ final WindowManager.LayoutParams lp = SnapshotDrawerUtils.createLayoutParameters(
+ info, title, TYPE_APPLICATION_OVERLAY, snapshot.getHardwareBuffer().getFormat(),
+ null /* token */);
+ if (lp == null) {
+ return;
+ }
+ final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId);
+ final StartingSurfaceDrawer.WindowlessStartingWindow wlw =
+ new StartingSurfaceDrawer.WindowlessStartingWindow(
+ runningTaskInfo.configuration, rootSurface);
+ final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost(
+ mContext, display, wlw, "WindowlessSnapshotWindowCreator");
+ final Point taskSize = snapshot.getTaskSize();
+ final Rect snapshotBounds = new Rect(0, 0, taskSize.x, taskSize.y);
+ final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds();
+ final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState;
+ final FrameLayout rootLayout = new FrameLayout(
+ mSplashscreenContentDrawer.createViewContextWrapper(mContext));
+ mViewHost.setView(rootLayout, lp);
+ SnapshotDrawerUtils.drawSnapshotOnSurface(info, lp, wlw.mChildSurface, snapshot,
+ snapshotBounds, windowBounds, topWindowInsetsState, false /* releaseAfterDraw */);
+
+ final ActivityManager.TaskDescription taskDescription =
+ SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo);
+
+ final SnapshotWindowRecord record = new SnapshotWindowRecord(mViewHost, wlw.mChildSurface,
+ taskDescription.getBackgroundColor(), snapshot.hasImeSurface(),
+ runningTaskInfo.topActivityType, removeExecutor);
+ mStartingWindowRecordManager.addRecord(taskId, record);
+ info.notifyAddComplete(wlw.mChildSurface);
+ }
+
+ private class SnapshotWindowRecord extends StartingSurfaceDrawer.SnapshotRecord {
+ private SurfaceControlViewHost mViewHost;
+ private SurfaceControl mChildSurface;
+ private final boolean mHasImeSurface;
+
+ SnapshotWindowRecord(SurfaceControlViewHost viewHost, SurfaceControl childSurface,
+ int bgColor, boolean hasImeSurface, int activityType,
+ ShellExecutor removeExecutor) {
+ super(activityType, removeExecutor);
+ mViewHost = viewHost;
+ mChildSurface = childSurface;
+ mBGColor = bgColor;
+ mHasImeSurface = hasImeSurface;
+ }
+
+ @Override
+ protected void removeImmediately() {
+ super.removeImmediately();
+ fadeoutThenRelease();
+ }
+
+ void fadeoutThenRelease() {
+ final ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1f, 0f);
+ fadeOutAnimator.setDuration(DEFAULT_FADEOUT_DURATION);
+ final SurfaceControl.Transaction t = mTransactionPool.acquire();
+ fadeOutAnimator.addUpdateListener(animation -> {
+ if (mChildSurface == null || !mChildSurface.isValid()) {
+ fadeOutAnimator.cancel();
+ return;
+ }
+ t.setAlpha(mChildSurface, (float) animation.getAnimatedValue());
+ t.apply();
+ });
+
+ fadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mChildSurface == null || !mChildSurface.isValid()) {
+ fadeOutAnimator.cancel();
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mTransactionPool.release(t);
+ if (mChildSurface != null) {
+ final SurfaceControl.Transaction t = mTransactionPool.acquire();
+ t.remove(mChildSurface).apply();
+ mTransactionPool.release(t);
+ mChildSurface = null;
+ }
+ if (mViewHost != null) {
+ mViewHost.release();
+ mViewHost = null;
+ }
+ }
+ });
+ fadeOutAnimator.start();
+ }
+
+ @Override
+ protected boolean hasImeSurface() {
+ return mHasImeSurface;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java
new file mode 100644
index 000000000000..98a803128587
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java
@@ -0,0 +1,151 @@
+/*
+ * 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.startingsurface;
+
+import static android.graphics.Color.WHITE;
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.os.SystemClock;
+import android.view.Display;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.window.SplashScreenView;
+import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+
+class WindowlessSplashWindowCreator extends AbsSplashWindowCreator {
+
+ private final TransactionPool mTransactionPool;
+
+ WindowlessSplashWindowCreator(SplashscreenContentDrawer contentDrawer,
+ Context context,
+ ShellExecutor splashScreenExecutor,
+ DisplayManager displayManager,
+ StartingSurfaceDrawer.StartingWindowRecordManager startingWindowRecordManager,
+ TransactionPool pool) {
+ super(contentDrawer, context, splashScreenExecutor, displayManager,
+ startingWindowRecordManager);
+ mTransactionPool = pool;
+ }
+
+ void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, SurfaceControl rootSurface) {
+ final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
+ final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
+ ? windowInfo.targetActivityInfo
+ : taskInfo.topActivityInfo;
+ if (activityInfo == null || activityInfo.packageName == null) {
+ return;
+ }
+
+ final int displayId = taskInfo.displayId;
+ final Display display = mDisplayManager.getDisplay(displayId);
+ if (display == null) {
+ // Can't show splash screen on requested display, so skip showing at all.
+ return;
+ }
+ final Context myContext = SplashscreenContentDrawer.createContext(mContext, windowInfo,
+ 0 /* theme */, STARTING_WINDOW_TYPE_SPLASH_SCREEN, mDisplayManager);
+ if (myContext == null) {
+ return;
+ }
+ final StartingSurfaceDrawer.WindowlessStartingWindow wlw =
+ new StartingSurfaceDrawer.WindowlessStartingWindow(
+ taskInfo.configuration, rootSurface);
+ final SurfaceControlViewHost viewHost = new SurfaceControlViewHost(
+ myContext, display, wlw, "WindowlessSplashWindowCreator");
+ final String title = "Windowless Splash " + taskInfo.taskId;
+ final WindowManager.LayoutParams lp = SplashscreenContentDrawer.createLayoutParameters(
+ myContext, windowInfo, STARTING_WINDOW_TYPE_SPLASH_SCREEN, title,
+ PixelFormat.TRANSLUCENT, new Binder());
+ final Rect windowBounds = taskInfo.configuration.windowConfiguration.getBounds();
+ lp.width = windowBounds.width();
+ lp.height = windowBounds.height();
+ final ActivityManager.TaskDescription taskDescription;
+ if (taskInfo.taskDescription != null) {
+ taskDescription = taskInfo.taskDescription;
+ } else {
+ taskDescription = new ActivityManager.TaskDescription();
+ taskDescription.setBackgroundColor(WHITE);
+ }
+
+ final FrameLayout rootLayout = new FrameLayout(
+ mSplashscreenContentDrawer.createViewContextWrapper(mContext));
+ viewHost.setView(rootLayout, lp);
+
+ final int bgColor = taskDescription.getBackgroundColor();
+ final SplashScreenView splashScreenView = mSplashscreenContentDrawer
+ .makeSimpleSplashScreenContentView(myContext, windowInfo, bgColor);
+ rootLayout.addView(splashScreenView);
+ final SplashWindowRecord record = new SplashWindowRecord(viewHost, splashScreenView,
+ wlw.mChildSurface, bgColor);
+ mStartingWindowRecordManager.addRecord(taskInfo.taskId, record);
+ windowInfo.notifyAddComplete(wlw.mChildSurface);
+ }
+
+ private class SplashWindowRecord extends StartingSurfaceDrawer.StartingWindowRecord {
+ private SurfaceControlViewHost mViewHost;
+ private final long mCreateTime;
+ private SurfaceControl mChildSurface;
+ private final SplashScreenView mSplashView;
+
+ SplashWindowRecord(SurfaceControlViewHost viewHost, SplashScreenView splashView,
+ SurfaceControl childSurface, int bgColor) {
+ mViewHost = viewHost;
+ mSplashView = splashView;
+ mChildSurface = childSurface;
+ mBGColor = bgColor;
+ mCreateTime = SystemClock.uptimeMillis();
+ }
+
+ @Override
+ public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
+ if (!immediately) {
+ mSplashscreenContentDrawer.applyExitAnimation(mSplashView,
+ info.windowAnimationLeash, info.mainFrame,
+ this::release, mCreateTime, 0 /* roundedCornerRadius */);
+ } else {
+ release();
+ }
+ return true;
+ }
+
+ void release() {
+ if (mChildSurface != null) {
+ final SurfaceControl.Transaction t = mTransactionPool.acquire();
+ t.remove(mChildSurface).apply();
+ mTransactionPool.release(t);
+ mChildSurface = null;
+ }
+ if (mViewHost != null) {
+ mViewHost.release();
+ mViewHost = null;
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
index bb43d7c1a090..72fc8686f648 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
@@ -22,6 +22,7 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS;
import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED;
import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_DRAWN;
import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT;
@@ -30,6 +31,7 @@ import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK;
import static android.window.StartingWindowInfo.TYPE_PARAMETER_PROCESS_RUNNING;
import static android.window.StartingWindowInfo.TYPE_PARAMETER_TASK_SWITCH;
import static android.window.StartingWindowInfo.TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN;
+import static android.window.StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS;
import android.window.StartingWindowInfo;
@@ -55,6 +57,7 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor
final boolean legacySplashScreen =
((parameter & TYPE_PARAMETER_LEGACY_SPLASH_SCREEN) != 0);
final boolean activityDrawn = (parameter & TYPE_PARAMETER_ACTIVITY_DRAWN) != 0;
+ final boolean windowlessSurface = (parameter & TYPE_PARAMETER_WINDOWLESS) != 0;
final boolean topIsHome = windowInfo.taskInfo.topActivityType == ACTIVITY_TYPE_HOME;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
@@ -67,10 +70,15 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor
+ "isSolidColorSplashScreen=%b, "
+ "legacySplashScreen=%b, "
+ "activityDrawn=%b, "
+ + "windowless=%b, "
+ "topIsHome=%b",
newTask, taskSwitch, processRunning, allowTaskSnapshot, activityCreated,
- isSolidColorSplashScreen, legacySplashScreen, activityDrawn, topIsHome);
+ isSolidColorSplashScreen, legacySplashScreen, activityDrawn, windowlessSurface,
+ topIsHome);
+ if (windowlessSurface) {
+ return STARTING_WINDOW_TYPE_WINDOWLESS;
+ }
if (!topIsHome) {
if (!processRunning || newTask || (taskSwitch && !activityCreated)) {
return getSplashscreenType(isSolidColorSplashScreen, legacySplashScreen);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java
index 74fe8fbbd5e0..04ae2f960102 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java
@@ -16,7 +16,7 @@
package com.android.wm.shell.startingsurface.tv;
-import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN;
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE;
import android.window.StartingWindowInfo;
@@ -24,12 +24,12 @@ import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
/**
* Algorithm for determining the type of a new starting window on Android TV.
- * For now we always show empty splash screens on Android TV.
+ * For now we do not want to show any splash screens on Android TV.
*/
public class TvStartingWindowTypeAlgorithm implements StartingWindowTypeAlgorithm {
@Override
public int getSuggestedWindowType(StartingWindowInfo windowInfo) {
- // For now we want to always show empty splash screens on TV.
- return STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN;
+ // For now we do not want to show any splash screens on TV.
+ return STARTING_WINDOW_TYPE_NONE;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index 3f944cb6d628..0eb7c2d98e0a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -32,6 +32,7 @@ import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.ArrayMap;
+import android.view.SurfaceControlRegistry;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -52,6 +53,7 @@ import java.util.function.Supplier;
public class ShellController {
private static final String TAG = ShellController.class.getSimpleName();
+ private final Context mContext;
private final ShellInit mShellInit;
private final ShellCommandHandler mShellCommandHandler;
private final ShellExecutor mMainExecutor;
@@ -72,8 +74,11 @@ public class ShellController {
private Configuration mLastConfiguration;
- public ShellController(ShellInit shellInit, ShellCommandHandler shellCommandHandler,
+ public ShellController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellExecutor mainExecutor) {
+ mContext = context;
mShellInit = shellInit;
mShellCommandHandler = shellCommandHandler;
mMainExecutor = mainExecutor;
@@ -254,6 +259,16 @@ public class ShellController {
}
}
+ private void handleInit() {
+ SurfaceControlRegistry.createProcessInstance(mContext);
+ mShellInit.init();
+ }
+
+ private void handleDump(PrintWriter pw) {
+ mShellCommandHandler.dump(pw);
+ SurfaceControlRegistry.dump(100 /* limit */, false /* runGc */, pw);
+ }
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
@@ -279,7 +294,7 @@ public class ShellController {
@Override
public void onInit() {
try {
- mMainExecutor.executeBlocking(() -> mShellInit.init());
+ mMainExecutor.executeBlocking(() -> ShellController.this.handleInit());
} catch (InterruptedException e) {
throw new RuntimeException("Failed to initialize the Shell in 2s", e);
}
@@ -344,7 +359,7 @@ public class ShellController {
@Override
public void dump(PrintWriter pw) {
try {
- mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw));
+ mMainExecutor.executeBlocking(() -> ShellController.this.handleDump(pw));
} catch (InterruptedException e) {
throw new RuntimeException("Failed to dump the Shell in 2s", e);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
index ac52235375c4..2e2f569a52b8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
@@ -21,6 +21,7 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT;
import android.os.Build;
import android.os.SystemClock;
import android.util.Pair;
+import android.view.SurfaceControl;
import androidx.annotation.VisibleForTesting;
@@ -75,6 +76,7 @@ public class ShellInit {
@VisibleForTesting
public void init() {
ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size());
+ SurfaceControl.setDebugUsageAfterRelease(true);
// Init in order of registration
for (int i = 0; i < mInitCallbacks.size(); i++) {
final Pair<String, Runnable> info = mInitCallbacks.get(i);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
index bdda6a8e926b..5f54f58557d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
@@ -22,6 +22,8 @@ package com.android.wm.shell.sysui;
public class ShellSharedConstants {
// See IPip.aidl
public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip";
+ // See IBubbles.aidl
+ public static final String KEY_EXTRA_SHELL_BUBBLES = "extra_shell_bubbles";
// See ISplitScreen.aidl
public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen";
// See IOneHanded.aidl
@@ -40,4 +42,6 @@ public class ShellSharedConstants {
public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks";
// See IDesktopMode.aidl
public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode";
+ // See IDragAndDrop.aidl
+ public static final String KEY_EXTRA_SHELL_DRAG_AND_DROP = "extra_shell_drag_and_drop";
}
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
new file mode 100644
index 000000000000..4faa92979733
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.taskview;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A {@link SurfaceView} that can display a task. This is a concrete implementation for
+ * {@link TaskViewBase} which interacts {@link TaskViewTaskController}.
+ */
+public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
+ ViewTreeObserver.OnComputeInternalInsetsListener, TaskViewBase {
+ /** Callback for listening task state. */
+ public interface Listener {
+ /**
+ * Only called once when the surface has been created & the container is ready for
+ * launching activities.
+ */
+ default void onInitialized() {}
+
+ /** Called when the container can no longer launch activities. */
+ default void onReleased() {}
+
+ /** Called when a task is created inside the container. */
+ default void onTaskCreated(int taskId, ComponentName name) {}
+
+ /** Called when a task visibility changes. */
+ default void onTaskVisibilityChanged(int taskId, boolean visible) {}
+
+ /** Called when a task is about to be removed from the stack inside the container. */
+ default void onTaskRemovalStarted(int taskId) {}
+
+ /** Called when a task is created inside the container. */
+ default void onBackPressedOnTaskRoot(int taskId) {}
+ }
+
+ private final Rect mTmpRect = new Rect();
+ private final Rect mTmpRootRect = new Rect();
+ private final int[] mTmpLocation = new int[2];
+ private final TaskViewTaskController mTaskViewTaskController;
+ private Region mObscuredTouchRegion;
+
+ public TaskView(Context context, TaskViewTaskController taskViewTaskController) {
+ super(context, null, 0, 0, true /* disableBackgroundLayer */);
+ mTaskViewTaskController = taskViewTaskController;
+ // TODO(b/266736992): Think about a better way to set the TaskViewBase on the
+ // TaskViewTaskController and vice-versa
+ mTaskViewTaskController.setTaskViewBase(this);
+ getHolder().addCallback(this);
+ }
+
+ /**
+ * Launch a new activity.
+ *
+ * @param pendingIntent Intent used to launch an activity.
+ * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()}
+ * @param options options for the activity.
+ * @param launchBounds the bounds (window size and position) that the activity should be
+ * launched in, in pixels and in screen coordinates.
+ */
+ public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent,
+ @NonNull ActivityOptions options, @Nullable Rect launchBounds) {
+ mTaskViewTaskController.startActivity(pendingIntent, fillInIntent, options, launchBounds);
+ }
+
+ /**
+ * Launch an activity represented by {@link ShortcutInfo}.
+ * <p>The owner of this container must be allowed to access the shortcut information,
+ * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method.
+ *
+ * @param shortcut the shortcut used to launch the activity.
+ * @param options options for the activity.
+ * @param launchBounds the bounds (window size and position) that the activity should be
+ * launched in, in pixels and in screen coordinates.
+ */
+ public void startShortcutActivity(@NonNull ShortcutInfo shortcut,
+ @NonNull ActivityOptions options, @Nullable Rect launchBounds) {
+ mTaskViewTaskController.startShortcutActivity(shortcut, options, launchBounds);
+ }
+
+ @Override
+ public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+ onLocationChanged();
+ if (taskInfo.taskDescription != null) {
+ setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
+ }
+ }
+
+ @Override
+ public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ if (taskInfo.taskDescription != null) {
+ setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
+ }
+ }
+
+ /**
+ * @return {@code True} when the TaskView's surface has been created, {@code False} otherwise.
+ */
+ public boolean isInitialized() {
+ return mTaskViewTaskController.isInitialized();
+ }
+
+ @Override
+ public Rect getCurrentBoundsOnScreen() {
+ getBoundsOnScreen(mTmpRect);
+ return mTmpRect;
+ }
+
+ @Override
+ public void setResizeBgColor(SurfaceControl.Transaction t, int bgColor) {
+ setResizeBackgroundColor(t, bgColor);
+ }
+
+ /**
+ * Only one listener may be set on the view, throws an exception otherwise.
+ */
+ public void setListener(@NonNull Executor executor, TaskView.Listener listener) {
+ mTaskViewTaskController.setListener(executor, listener);
+ }
+
+ /**
+ * Indicates a region of the view that is not touchable.
+ *
+ * @param obscuredRect the obscured region of the view.
+ */
+ public void setObscuredTouchRect(Rect obscuredRect) {
+ mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null;
+ }
+
+ /**
+ * Indicates a region of the view that is not touchable.
+ *
+ * @param obscuredRegion the obscured region of the view.
+ */
+ public void setObscuredTouchRegion(Region obscuredRegion) {
+ mObscuredTouchRegion = obscuredRegion;
+ }
+
+ /**
+ * Call when view position or size has changed. Do not call when animating.
+ */
+ public void onLocationChanged() {
+ getBoundsOnScreen(mTmpRect);
+ mTaskViewTaskController.setWindowBounds(mTmpRect);
+ }
+
+ /**
+ * Call to remove the task from window manager. This task will not appear in recents.
+ */
+ public void removeTask() {
+ mTaskViewTaskController.removeTask();
+ }
+
+ /**
+ * Release this container if it is initialized.
+ */
+ public void release() {
+ getHolder().removeCallback(this);
+ mTaskViewTaskController.release();
+ }
+
+ @Override
+ public String toString() {
+ return mTaskViewTaskController.toString();
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mTaskViewTaskController.surfaceCreated(getSurfaceControl());
+ }
+
+ @Override
+ public void surfaceChanged(@androidx.annotation.NonNull SurfaceHolder holder, int format,
+ int width, int height) {
+ getBoundsOnScreen(mTmpRect);
+ mTaskViewTaskController.setWindowBounds(mTmpRect);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mTaskViewTaskController.surfaceDestroyed();
+ }
+
+ @Override
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this
+ // is dependent on the order of listener.
+ // If there are multiple TaskViews, we'll set the touchable area as the root-view, then
+ // subtract each TaskView from it.
+ if (inoutInfo.touchableRegion.isEmpty()) {
+ inoutInfo.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ View root = getRootView();
+ root.getLocationInWindow(mTmpLocation);
+ mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight());
+ inoutInfo.touchableRegion.set(mTmpRootRect);
+ }
+ getLocationInWindow(mTmpLocation);
+ mTmpRect.set(mTmpLocation[0], mTmpLocation[1],
+ mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight());
+ inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE);
+
+ if (mObscuredTouchRegion != null) {
+ inoutInfo.touchableRegion.op(mObscuredTouchRegion, Region.Op.UNION);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+ }
+
+ /** Returns the task info for the task in the TaskView. */
+ @Nullable
+ public ActivityManager.RunningTaskInfo getTaskInfo() {
+ return mTaskViewTaskController.getTaskInfo();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java
new file mode 100644
index 000000000000..5fdb60d2d342
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java
@@ -0,0 +1,64 @@
+/*
+ * 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.taskview;
+
+import android.app.ActivityManager;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+/**
+ * A stub for SurfaceView used by {@link TaskViewTaskController}
+ */
+public interface TaskViewBase {
+ /**
+ * Returns the current bounds on screen for the task view.
+ * @return
+ */
+ // TODO(b/266242294): Remove getBoundsOnScreen() and instead send the bounds from the TaskView
+ // to TaskViewTaskController.
+ Rect getCurrentBoundsOnScreen();
+
+ /**
+ * This method should set the resize background color on the SurfaceView that is exposed to
+ * clients.
+ * See {@link android.view.SurfaceView#setResizeBackgroundColor(SurfaceControl.Transaction,
+ * int)}
+ */
+ void setResizeBgColor(SurfaceControl.Transaction transaction, int color);
+
+ /**
+ * Called when a task appears on the TaskView. See
+ * {@link TaskViewTaskController#onTaskAppeared(ActivityManager.RunningTaskInfo,
+ * SurfaceControl)} for details.
+ */
+ default void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+ }
+
+ /**
+ * Called when a task is vanished from the TaskView. See
+ * {@link TaskViewTaskController#onTaskVanished(ActivityManager.RunningTaskInfo)} for details.
+ */
+ default void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+ }
+
+ /**
+ * Called when the task in the TaskView is changed. See
+ * {@link TaskViewTaskController#onTaskInfoChanged(ActivityManager.RunningTaskInfo)} for details.
+ */
+ default void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
index a29e7a085a21..a7e4b0119480 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell;
+package com.android.wm.shell.taskview;
import android.annotation.UiContext;
import android.content.Context;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
index 42844b57b92a..7eed5883043d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package com.android.wm.shell;
+package com.android.wm.shell.taskview;
import android.annotation.UiContext;
import android.content.Context;
+import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.annotations.ExternalThread;
@@ -51,13 +52,17 @@ public class TaskViewFactoryController {
mTaskViewTransitions = null;
}
+ /**
+ * @return the underlying {@link TaskViewFactory}.
+ */
public TaskViewFactory asTaskViewFactory() {
return mImpl;
}
/** Creates an {@link TaskView} */
public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) {
- TaskView taskView = new TaskView(context, mTaskOrganizer, mTaskViewTransitions, mSyncQueue);
+ TaskView taskView = new TaskView(context, new TaskViewTaskController(context,
+ mTaskOrganizer, mTaskViewTransitions, mSyncQueue));
executor.execute(() -> {
onCreate.accept(taskView);
});
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
index 95a89b1d23ff..163cf501734c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell;
+package com.android.wm.shell.taskview;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
@@ -29,51 +29,28 @@ import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
-import android.graphics.Region;
import android.os.Binder;
import android.util.CloseGuard;
+import android.util.Slog;
import android.view.SurfaceControl;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewTreeObserver;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
+import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SyncTransactionQueue;
import java.io.PrintWriter;
import java.util.concurrent.Executor;
/**
- * View that can display a task.
+ * This class implements the core logic to show a task on the {@link TaskView}. All the {@link
+ * TaskView} to {@link TaskViewTaskController} interactions are done via direct method calls.
+ *
+ * The reverse communication is done via the {@link TaskViewBase} interface.
*/
-public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
- ShellTaskOrganizer.TaskListener, ViewTreeObserver.OnComputeInternalInsetsListener {
-
- /** Callback for listening task state. */
- public interface Listener {
- /**
- * Only called once when the surface has been created & the container is ready for
- * launching activities.
- */
- default void onInitialized() {}
-
- /** Called when the container can no longer launch activities. */
- default void onReleased() {}
-
- /** Called when a task is created inside the container. */
- default void onTaskCreated(int taskId, ComponentName name) {}
+public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
- /** Called when a task visibility changes. */
- default void onTaskVisibilityChanged(int taskId, boolean visible) {}
-
- /** Called when a task is about to be removed from the stack inside the container. */
- default void onTaskRemovalStarted(int taskId) {}
-
- /** Called when a task is created inside the container. */
- default void onBackPressedOnTaskRoot(int taskId) {}
- }
+ private static final String TAG = TaskViewTaskController.class.getSimpleName();
private final CloseGuard mGuard = new CloseGuard();
@@ -81,26 +58,33 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
private final Executor mShellExecutor;
private final SyncTransactionQueue mSyncQueue;
private final TaskViewTransitions mTaskViewTransitions;
+ private TaskViewBase mTaskViewBase;
+ private final Context mContext;
+ /**
+ * There could be a situation where we have task info and receive
+ * {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)}, however, the
+ * activity might fail to open, and in this case we need to clean up the task view / notify
+ * listeners of a task removal. This requires task info, so we save the info from onTaskAppeared
+ * in this situation to allow us to notify listeners correctly if the task failed to open.
+ */
+ private ActivityManager.RunningTaskInfo mPendingInfo;
+ /* Indicates that the task we attempted to launch in the task view failed to launch. */
+ private boolean mTaskNotFound;
protected ActivityManager.RunningTaskInfo mTaskInfo;
private WindowContainerToken mTaskToken;
private SurfaceControl mTaskLeash;
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private boolean mSurfaceCreated;
+ private SurfaceControl mSurfaceControl;
private boolean mIsInitialized;
private boolean mNotifiedForInitialized;
- private Listener mListener;
+ private TaskView.Listener mListener;
private Executor mListenerExecutor;
- private Region mObscuredTouchRegion;
- private final Rect mTmpRect = new Rect();
- private final Rect mTmpRootRect = new Rect();
- private final int[] mTmpLocation = new int[2];
-
- public TaskView(Context context, ShellTaskOrganizer organizer,
+ public TaskViewTaskController(Context context, ShellTaskOrganizer organizer,
TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) {
- super(context, null, 0, 0, true /* disableBackgroundLayer */);
-
+ mContext = context;
mTaskOrganizer = organizer;
mShellExecutor = organizer.getExecutor();
mSyncQueue = syncQueue;
@@ -108,11 +92,21 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
if (mTaskViewTransitions != null) {
mTaskViewTransitions.addTaskView(this);
}
- setUseAlpha();
- getHolder().addCallback(this);
mGuard.open("release");
}
+ SurfaceControl getSurfaceControl() {
+ return mSurfaceControl;
+ }
+
+ /**
+ * Sets the provided {@link TaskViewBase}, which is used to notify the client part about the
+ * task related changes and getting the current bounds.
+ */
+ public void setTaskViewBase(TaskViewBase taskViewBase) {
+ mTaskViewBase = taskViewBase;
+ }
+
/**
* @return {@code True} when the TaskView's surface has been created, {@code False} otherwise.
*/
@@ -121,14 +115,14 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
}
/** Until all users are converted, we may have mixed-use (eg. Car). */
- private boolean isUsingShellTransitions() {
+ public boolean isUsingShellTransitions() {
return mTaskViewTransitions != null && mTaskViewTransitions.isEnabled();
}
/**
* Only one listener may be set on the view, throws an exception otherwise.
*/
- public void setListener(@NonNull Executor executor, Listener listener) {
+ void setListener(@NonNull Executor executor, TaskView.Listener listener) {
if (mListener != null) {
throw new IllegalStateException(
"Trying to set a listener when one has already been set");
@@ -155,7 +149,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
mShellExecutor.execute(() -> {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.startShortcut(mContext.getPackageName(), shortcut, options.toBundle());
- mTaskViewTransitions.startTaskView(wct, this);
+ mTaskViewTransitions.startTaskView(wct, this, options.getLaunchCookie());
});
return;
}
@@ -182,7 +176,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
mShellExecutor.execute(() -> {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.sendPendingIntent(pendingIntent, fillInIntent, options.toBundle());
- mTaskViewTransitions.startTaskView(wct, this);
+ mTaskViewTransitions.startTaskView(wct, this, options.getLaunchCookie());
});
return;
}
@@ -207,45 +201,6 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
}
/**
- * Indicates a region of the view that is not touchable.
- *
- * @param obscuredRect the obscured region of the view.
- */
- public void setObscuredTouchRect(Rect obscuredRect) {
- mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null;
- }
-
- /**
- * Indicates a region of the view that is not touchable.
- *
- * @param obscuredRegion the obscured region of the view.
- */
- public void setObscuredTouchRegion(Region obscuredRegion) {
- mObscuredTouchRegion = obscuredRegion;
- }
-
- /**
- * Call when view position or size has changed. Do not call when animating.
- */
- public void onLocationChanged() {
- if (mTaskToken == null) {
- return;
- }
- // Sync Transactions can't operate simultaneously with shell transition collection.
- // The transition animation (upon showing) will sync the location itself.
- if (isUsingShellTransitions() && mTaskViewTransitions.hasPending()) return;
-
- WindowContainerTransaction wct = new WindowContainerTransaction();
- updateWindowBounds(wct);
- mSyncQueue.queue(wct);
- }
-
- private void updateWindowBounds(WindowContainerTransaction wct) {
- getBoundsOnScreen(mTmpRect);
- wct.setBounds(mTaskToken, mTmpRect);
- }
-
- /**
* Release this container if it is initialized.
*/
public void release() {
@@ -265,7 +220,6 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
}
private void performRelease() {
- getHolder().removeCallback(this);
if (mTaskViewTransitions != null) {
mTaskViewTransitions.removeTaskView(this);
}
@@ -278,7 +232,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
notifyReleased();
}
- /** Called when the {@link TaskView} has been released. */
+ /** Called when the {@link TaskViewTaskController} has been released. */
protected void notifyReleased() {
if (mListener != null && mNotifiedForInitialized) {
mListenerExecutor.execute(() -> {
@@ -292,6 +246,8 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
mTaskInfo = null;
mTaskToken = null;
mTaskLeash = null;
+ mPendingInfo = null;
+ mTaskNotFound = false;
}
private void updateTaskVisibility() {
@@ -313,6 +269,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl leash) {
if (isUsingShellTransitions()) {
+ mPendingInfo = taskInfo;
+ if (mTaskNotFound) {
+ // If we were already notified by shell transit that we don't have the
+ // the task, clean it up now.
+ cleanUpPendingTask();
+ }
// Everything else handled by enter transition.
return;
}
@@ -322,7 +284,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
if (mSurfaceCreated) {
// Surface is ready, so just reparent the task to this surface control
- mTransaction.reparent(mTaskLeash, getSurfaceControl())
+ mTransaction.reparent(mTaskLeash, mSurfaceControl)
.show(mTaskLeash)
.apply();
} else {
@@ -331,13 +293,9 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
updateTaskVisibility();
}
mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true);
- onLocationChanged();
- if (taskInfo.taskDescription != null) {
- int backgroundColor = taskInfo.taskDescription.getBackgroundColor();
- mSyncQueue.runInSync((t) -> {
- setResizeBackgroundColor(t, backgroundColor);
- });
- }
+ mSyncQueue.runInSync((t) -> {
+ mTaskViewBase.onTaskAppeared(taskInfo, leash);
+ });
if (mListener != null) {
final int taskId = taskInfo.taskId;
@@ -365,13 +323,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
// Unparent the task when this surface is destroyed
mTransaction.reparent(mTaskLeash, null).apply();
resetTaskInfo();
+ mTaskViewBase.onTaskVanished(taskInfo);
}
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
- if (taskInfo.taskDescription != null) {
- setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
- }
+ mTaskViewBase.onTaskInfoChanged(taskInfo);
}
@Override
@@ -412,13 +369,18 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
@Override
public String toString() {
- return "TaskView" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null");
+ return "TaskViewTaskController" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null");
}
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
+ /**
+ * Should be called when the client surface is created.
+ *
+ * @param surfaceControl the {@link SurfaceControl} for the underlying surface.
+ */
+ public void surfaceCreated(SurfaceControl surfaceControl) {
mSurfaceCreated = true;
mIsInitialized = true;
+ mSurfaceControl = surfaceControl;
notifyInitialized();
mShellExecutor.execute(() -> {
if (mTaskToken == null) {
@@ -430,34 +392,54 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
return;
}
// Reparent the task when this surface is created
- mTransaction.reparent(mTaskLeash, getSurfaceControl())
+ mTransaction.reparent(mTaskLeash, mSurfaceControl)
.show(mTaskLeash)
.apply();
updateTaskVisibility();
});
}
- /** Called when the {@link TaskView} is initialized. */
- protected void notifyInitialized() {
- if (mListener != null && !mNotifiedForInitialized) {
- mNotifiedForInitialized = true;
- mListenerExecutor.execute(() -> {
- mListener.onInitialized();
- });
+ /**
+ * Sets the window bounds to {@code boundsOnScreen}.
+ * Call when view position or size has changed. Can also be called before the animation when
+ * the final bounds are known.
+ * Do not call during the animation.
+ *
+ * @param boundsOnScreen the on screen bounds of the surface view.
+ */
+ public void setWindowBounds(Rect boundsOnScreen) {
+ if (mTaskToken == null) {
+ return;
+ }
+ // Sync Transactions can't operate simultaneously with shell transition collection.
+ if (isUsingShellTransitions()) {
+ mTaskViewTransitions.setTaskBounds(this, boundsOnScreen);
+ return;
}
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(mTaskToken, boundsOnScreen);
+ mSyncQueue.queue(wct);
}
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ /**
+ * Call to remove the task from window manager. This task will not appear in recents.
+ */
+ void removeTask() {
if (mTaskToken == null) {
+ // Call to remove task before we have one, do nothing
+ Slog.w(TAG, "Trying to remove a task that was never added? (no taskToken)");
return;
}
- onLocationChanged();
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.removeTask(mTaskToken);
+ mTaskViewTransitions.closeTaskView(wct, this);
}
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
+ /** Should be called when the client surface is destroyed. */
+ public void surfaceDestroyed() {
mSurfaceCreated = false;
+ mSurfaceControl = null;
mShellExecutor.execute(() -> {
if (mTaskToken == null) {
// Nothing to update, task is not yet available
@@ -475,55 +457,65 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
});
}
- @Override
- public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
- // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this
- // is dependent on the order of listener.
- // If there are multiple TaskViews, we'll set the touchable area as the root-view, then
- // subtract each TaskView from it.
- if (inoutInfo.touchableRegion.isEmpty()) {
- inoutInfo.setTouchableInsets(
- ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
- View root = getRootView();
- root.getLocationInWindow(mTmpLocation);
- mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight());
- inoutInfo.touchableRegion.set(mTmpRootRect);
- }
- getLocationInWindow(mTmpLocation);
- mTmpRect.set(mTmpLocation[0], mTmpLocation[1],
- mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight());
- inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE);
-
- if (mObscuredTouchRegion != null) {
- inoutInfo.touchableRegion.op(mObscuredTouchRegion, Region.Op.UNION);
+ /** Called when the {@link TaskViewTaskController} is initialized. */
+ protected void notifyInitialized() {
+ if (mListener != null && !mNotifiedForInitialized) {
+ mNotifiedForInitialized = true;
+ mListenerExecutor.execute(() -> {
+ mListener.onInitialized();
+ });
}
}
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- getViewTreeObserver().addOnComputeInternalInsetsListener(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
- }
-
/** Returns the task info for the task in the TaskView. */
@Nullable
public ActivityManager.RunningTaskInfo getTaskInfo() {
return mTaskInfo;
}
+ /**
+ * Indicates that the task was not found in the start animation for the transition.
+ * In this case we should clean up the task if we have the pending info. If we don't
+ * have the pending info, we'll do it when we receive it in
+ * {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)}.
+ */
+ void setTaskNotFound() {
+ mTaskNotFound = true;
+ if (mPendingInfo != null) {
+ cleanUpPendingTask();
+ }
+ }
+
+ /**
+ * Called when a task failed to open and we need to clean up task view /
+ * notify users of task view.
+ */
+ void cleanUpPendingTask() {
+ if (mPendingInfo != null) {
+ if (mListener != null) {
+ final int taskId = mPendingInfo.taskId;
+ mListenerExecutor.execute(() -> {
+ mListener.onTaskRemovalStarted(taskId);
+ });
+ }
+ mTaskViewBase.onTaskVanished(mPendingInfo);
+ mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mPendingInfo.token, false);
+
+ // Make sure the task is removed
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.removeTask(mPendingInfo.token);
+ mTaskViewTransitions.closeTaskView(wct, this);
+ }
+ resetTaskInfo();
+ }
+
void prepareHideAnimation(@NonNull SurfaceControl.Transaction finishTransaction) {
if (mTaskToken == null) {
// Nothing to update, task is not yet available
return;
}
- finishTransaction.reparent(mTaskLeash, null).apply();
+ finishTransaction.reparent(mTaskLeash, null);
if (mListener != null) {
final int taskId = mTaskInfo.taskId;
@@ -543,6 +535,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
mListener.onTaskRemovalStarted(taskId);
});
}
+ mTaskViewBase.onTaskVanished(mTaskInfo);
mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false);
}
resetTaskInfo();
@@ -553,26 +546,29 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
@NonNull SurfaceControl.Transaction finishTransaction,
ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash,
WindowContainerTransaction wct) {
+ mPendingInfo = null;
mTaskInfo = taskInfo;
mTaskToken = mTaskInfo.token;
mTaskLeash = leash;
if (mSurfaceCreated) {
// Surface is ready, so just reparent the task to this surface control
- startTransaction.reparent(mTaskLeash, getSurfaceControl())
- .show(mTaskLeash)
- .apply();
+ startTransaction.reparent(mTaskLeash, mSurfaceControl)
+ .show(mTaskLeash);
// Also reparent on finishTransaction since the finishTransaction will reparent back
// to its "original" parent by default.
- finishTransaction.reparent(mTaskLeash, getSurfaceControl())
+ Rect boundsOnScreen = mTaskViewBase.getCurrentBoundsOnScreen();
+ finishTransaction.reparent(mTaskLeash, mSurfaceControl)
.setPosition(mTaskLeash, 0, 0)
- .apply();
-
- // TODO: determine if this is really necessary or not
- updateWindowBounds(wct);
+ // TODO: maybe once b/280900002 is fixed this will be unnecessary
+ .setWindowCrop(mTaskLeash, boundsOnScreen.width(), boundsOnScreen.height());
+ mTaskViewTransitions.updateBoundsState(this, boundsOnScreen);
+ mTaskViewTransitions.updateVisibilityState(this, true /* visible */);
+ wct.setBounds(mTaskToken, boundsOnScreen);
} else {
// The surface has already been destroyed before the task has appeared,
// so go ahead and hide the task entirely
wct.setHidden(mTaskToken, true /* hidden */);
+ mTaskViewTransitions.updateVisibilityState(this, false /* visible */);
// listener callback is below
}
if (newTask) {
@@ -581,7 +577,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
if (mTaskInfo.taskDescription != null) {
int backgroundColor = mTaskInfo.taskDescription.getBackgroundColor();
- setResizeBackgroundColor(startTransaction, backgroundColor);
+ mTaskViewBase.setResizeBgColor(startTransaction, backgroundColor);
}
if (mListener != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
new file mode 100644
index 000000000000..5baf2e320227
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.taskview;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Handles Shell Transitions that involve TaskView tasks.
+ */
+public class TaskViewTransitions implements Transitions.TransitionHandler {
+ static final String TAG = "TaskViewTransitions";
+
+ private final ArrayMap<TaskViewTaskController, TaskViewRequestedState> mTaskViews =
+ new ArrayMap<>();
+ private final ArrayList<PendingTransition> mPending = new ArrayList<>();
+ private final Transitions mTransitions;
+ private final boolean[] mRegistered = new boolean[]{ false };
+
+ /**
+ * TaskView makes heavy use of startTransition. Only one shell-initiated transition can be
+ * in-flight (collecting) at a time (because otherwise, the operations could get merged into
+ * a single transition). So, keep a queue here until we add a queue in server-side.
+ */
+ @VisibleForTesting
+ static class PendingTransition {
+ final @WindowManager.TransitionType int mType;
+ final WindowContainerTransaction mWct;
+ final @NonNull TaskViewTaskController mTaskView;
+ IBinder mClaimed;
+
+ /**
+ * This is needed because arbitrary activity launches can still "intrude" into any
+ * transition since `startActivity` is a synchronous call. Once that is solved, we can
+ * remove this.
+ */
+ final IBinder mLaunchCookie;
+
+ PendingTransition(@WindowManager.TransitionType int type,
+ @Nullable WindowContainerTransaction wct,
+ @NonNull TaskViewTaskController taskView,
+ @Nullable IBinder launchCookie) {
+ mType = type;
+ mWct = wct;
+ mTaskView = taskView;
+ mLaunchCookie = launchCookie;
+ }
+ }
+
+ /**
+ * Visibility and bounds state that has been requested for a {@link TaskViewTaskController}.
+ */
+ private static class TaskViewRequestedState {
+ boolean mVisible;
+ Rect mBounds = new Rect();
+ }
+
+ public TaskViewTransitions(Transitions transitions) {
+ mTransitions = transitions;
+ // Defer registration until the first TaskView because we want this to be the "first" in
+ // priority when handling requests.
+ // TODO(210041388): register here once we have an explicit ordering mechanism.
+ }
+
+ void addTaskView(TaskViewTaskController tv) {
+ synchronized (mRegistered) {
+ if (!mRegistered[0]) {
+ mRegistered[0] = true;
+ mTransitions.addHandler(this);
+ }
+ }
+ mTaskViews.put(tv, new TaskViewRequestedState());
+ }
+
+ void removeTaskView(TaskViewTaskController tv) {
+ mTaskViews.remove(tv);
+ // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell
+ }
+
+ boolean isEnabled() {
+ return mTransitions.isRegistered();
+ }
+
+ /**
+ * Looks through the pending transitions for a closing transaction that matches the provided
+ * `taskView`.
+ * @param taskView the pending transition should be for this.
+ */
+ private PendingTransition findPendingCloseTransition(TaskViewTaskController taskView) {
+ for (int i = mPending.size() - 1; i >= 0; --i) {
+ if (mPending.get(i).mTaskView != taskView) continue;
+ if (TransitionUtil.isClosingType(mPending.get(i).mType)) {
+ return mPending.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Looks through the pending transitions for a opening transaction that matches the provided
+ * `taskView`.
+ * @param taskView the pending transition should be for this.
+ */
+ @VisibleForTesting
+ PendingTransition findPendingOpeningTransition(TaskViewTaskController taskView) {
+ for (int i = mPending.size() - 1; i >= 0; --i) {
+ if (mPending.get(i).mTaskView != taskView) continue;
+ if (TransitionUtil.isOpeningType(mPending.get(i).mType)) {
+ return mPending.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Looks through the pending transitions for one matching `taskView`.
+ * @param taskView the pending transition should be for this.
+ * @param type the type of transition it's looking for
+ */
+ PendingTransition findPending(TaskViewTaskController taskView, int type) {
+ for (int i = mPending.size() - 1; i >= 0; --i) {
+ if (mPending.get(i).mTaskView != taskView) continue;
+ if (mPending.get(i).mType == type) {
+ return mPending.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns all the pending transitions for a given `taskView`.
+ * @param taskView the pending transition should be for this.
+ */
+ ArrayList<PendingTransition> findAllPending(TaskViewTaskController taskView) {
+ ArrayList<PendingTransition> list = new ArrayList<>();
+ for (int i = mPending.size() - 1; i >= 0; --i) {
+ if (mPending.get(i).mTaskView != taskView) continue;
+ list.add(mPending.get(i));
+ }
+ return list;
+ }
+
+ private PendingTransition findPending(IBinder claimed) {
+ for (int i = 0; i < mPending.size(); ++i) {
+ if (mPending.get(i).mClaimed != claimed) continue;
+ return mPending.get(i);
+ }
+ return null;
+ }
+
+ /** @return whether there are pending transitions on TaskViews. */
+ public boolean hasPending() {
+ return !mPending.isEmpty();
+ }
+
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @Nullable TransitionRequestInfo request) {
+ final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
+ if (triggerTask == null) {
+ return null;
+ }
+ final TaskViewTaskController taskView = findTaskView(triggerTask);
+ if (taskView == null) return null;
+ // Opening types should all be initiated by shell
+ if (!TransitionUtil.isClosingType(request.getType())) return null;
+ PendingTransition pending = findPendingCloseTransition(taskView);
+ if (pending == null) {
+ pending = new PendingTransition(request.getType(), null, taskView, null /* cookie */);
+ }
+ if (pending.mClaimed != null) {
+ throw new IllegalStateException("Task is closing in 2 collecting transitions?"
+ + " This state doesn't make sense");
+ }
+ pending.mClaimed = transition;
+ return new WindowContainerTransaction();
+ }
+
+ private TaskViewTaskController findTaskView(ActivityManager.RunningTaskInfo taskInfo) {
+ for (int i = 0; i < mTaskViews.size(); ++i) {
+ if (mTaskViews.keyAt(i).getTaskInfo() == null) continue;
+ if (taskInfo.token.equals(mTaskViews.keyAt(i).getTaskInfo().token)) {
+ return mTaskViews.keyAt(i);
+ }
+ }
+ return null;
+ }
+
+ void startTaskView(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie) {
+ updateVisibilityState(taskView, true /* visible */);
+ mPending.add(new PendingTransition(TRANSIT_OPEN, wct, taskView, launchCookie));
+ startNextTransition();
+ }
+
+ void closeTaskView(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskViewTaskController taskView) {
+ updateVisibilityState(taskView, false /* visible */);
+ mPending.add(new PendingTransition(TRANSIT_CLOSE, wct, taskView, null /* cookie */));
+ startNextTransition();
+ }
+
+ void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) {
+ if (mTaskViews.get(taskView) == null) return;
+ if (mTaskViews.get(taskView).mVisible == visible) return;
+ if (taskView.getTaskInfo() == null) {
+ // Nothing to update, task is not yet available
+ return;
+ }
+ mTaskViews.get(taskView).mVisible = visible;
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */);
+ wct.setBounds(taskView.getTaskInfo().token, mTaskViews.get(taskView).mBounds);
+ PendingTransition pending = new PendingTransition(
+ visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */);
+ mPending.add(pending);
+ startNextTransition();
+ // visibility is reported in transition.
+ }
+
+ void updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen) {
+ TaskViewRequestedState state = mTaskViews.get(taskView);
+ if (state == null) return;
+ state.mBounds.set(boundsOnScreen);
+ }
+
+ void updateVisibilityState(TaskViewTaskController taskView, boolean visible) {
+ TaskViewRequestedState state = mTaskViews.get(taskView);
+ if (state == null) return;
+ state.mVisible = visible;
+ }
+
+ void setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen) {
+ TaskViewRequestedState state = mTaskViews.get(taskView);
+ if (state == null || Objects.equals(boundsOnScreen, state.mBounds)) {
+ return;
+ }
+ state.mBounds.set(boundsOnScreen);
+ if (!state.mVisible) {
+ // Task view isn't visible, the bounds will next visibility update.
+ return;
+ }
+ PendingTransition pendingOpen = findPendingOpeningTransition(taskView);
+ if (pendingOpen != null) {
+ // There is already an opening transition in-flight, the window bounds will be
+ // set in prepareOpenAnimation (via the window crop) if needed.
+ return;
+ }
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(taskView.getTaskInfo().token, boundsOnScreen);
+ mPending.add(new PendingTransition(TRANSIT_CHANGE, wct, taskView, null /* cookie */));
+ startNextTransition();
+ }
+
+ private void startNextTransition() {
+ if (mPending.isEmpty()) return;
+ final PendingTransition pending = mPending.get(0);
+ if (pending.mClaimed != null) {
+ // Wait for this to start animating.
+ return;
+ }
+ pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this);
+ }
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ if (!aborted) return;
+ final PendingTransition pending = findPending(transition);
+ if (pending == null) return;
+ mPending.remove(pending);
+ startNextTransition();
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ PendingTransition pending = findPending(transition);
+ if (pending != null) {
+ mPending.remove(pending);
+ }
+ if (mTaskViews.isEmpty()) {
+ if (pending != null) {
+ Slog.e(TAG, "Pending taskview transition but no task-views");
+ }
+ return false;
+ }
+ boolean stillNeedsMatchingLaunch = pending != null && pending.mLaunchCookie != null;
+ int changesHandled = 0;
+ WindowContainerTransaction wct = null;
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change chg = info.getChanges().get(i);
+ if (chg.getTaskInfo() == null) continue;
+ if (TransitionUtil.isClosingType(chg.getMode())) {
+ final boolean isHide = chg.getMode() == TRANSIT_TO_BACK;
+ TaskViewTaskController tv = findTaskView(chg.getTaskInfo());
+ if (tv == null && !isHide) {
+ // TaskView can be null when closing
+ changesHandled++;
+ continue;
+ }
+ if (tv == null) {
+ if (pending != null) {
+ Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
+ + "shouldn't happen, so there may be a visual artifact: "
+ + chg.getTaskInfo().taskId);
+ }
+ continue;
+ }
+ if (isHide) {
+ tv.prepareHideAnimation(finishTransaction);
+ } else {
+ tv.prepareCloseAnimation();
+ }
+ changesHandled++;
+ } else if (TransitionUtil.isOpeningType(chg.getMode())) {
+ final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN;
+ final TaskViewTaskController tv;
+ if (taskIsNew) {
+ if (pending == null
+ || !chg.getTaskInfo().containsLaunchCookie(pending.mLaunchCookie)) {
+ Slog.e(TAG, "Found a launching TaskView in the wrong transition. All "
+ + "TaskView launches should be initiated by shell and in their "
+ + "own transition: " + chg.getTaskInfo().taskId);
+ continue;
+ }
+ stillNeedsMatchingLaunch = false;
+ tv = pending.mTaskView;
+ } else {
+ tv = findTaskView(chg.getTaskInfo());
+ if (tv == null) {
+ if (pending != null) {
+ Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
+ + "shouldn't happen, so there may be a visual artifact: "
+ + chg.getTaskInfo().taskId);
+ }
+ continue;
+ }
+ }
+ if (wct == null) wct = new WindowContainerTransaction();
+ tv.prepareOpenAnimation(taskIsNew, startTransaction, finishTransaction,
+ chg.getTaskInfo(), chg.getLeash(), wct);
+ changesHandled++;
+ } else if (chg.getMode() == TRANSIT_CHANGE) {
+ TaskViewTaskController tv = findTaskView(chg.getTaskInfo());
+ if (tv == null) {
+ if (pending != null) {
+ Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
+ + "shouldn't happen, so there may be a visual artifact: "
+ + chg.getTaskInfo().taskId);
+ }
+ continue;
+ }
+ startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl());
+ finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl())
+ .setPosition(chg.getLeash(), 0, 0);
+ changesHandled++;
+ }
+ }
+ if (stillNeedsMatchingLaunch) {
+ Slog.w(TAG, "Expected a TaskView launch in this transition but didn't get one, "
+ + "cleaning up the task view");
+ // Didn't find a task so the task must have never launched
+ pending.mTaskView.setTaskNotFound();
+ } else if (wct == null && pending == null && changesHandled != info.getChanges().size()) {
+ // Just some house-keeping, let another handler animate.
+ return false;
+ }
+ // No animation, just show it immediately.
+ startTransaction.apply();
+ finishCallback.onTransitionFinished(wct, null /* wctCB */);
+ startNextTransition();
+ return true;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
index 19133e29de4b..628ce27fe514 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
@@ -28,6 +28,7 @@ import android.window.WindowContainerToken;
import androidx.annotation.NonNull;
import com.android.wm.shell.util.CounterRotator;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.List;
@@ -57,7 +58,7 @@ public class CounterRotatorHelper {
for (int i = numChanges - 1; i >= 0; --i) {
final TransitionInfo.Change change = changes.get(i);
final WindowContainerToken parent = change.getParent();
- if (!Transitions.isClosingType(change.getMode())
+ if (!TransitionUtil.isClosingType(change.getMode())
|| !TransitionInfo.isIndependent(change, info) || parent == null) {
continue;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index a2d7bc43653a..d0a361a8ecd2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -17,30 +17,43 @@
package com.android.wm.shell.transition;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
+import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.PendingIntent;
import android.os.IBinder;
+import android.util.Pair;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransactionCallback;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.split.SplitScreenUtils;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.splitscreen.StageCoordinator;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.unfold.UnfoldTransitionHandler;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.Optional;
@@ -49,11 +62,15 @@ import java.util.Optional;
* A handler for dealing with transitions involving multiple other handlers. For example: an
* activity in split-screen going into PiP.
*/
-public class DefaultMixedHandler implements Transitions.TransitionHandler {
+public class DefaultMixedHandler implements Transitions.TransitionHandler,
+ RecentsTransitionHandler.RecentsMixedHandler {
private final Transitions mPlayer;
private PipTransitionController mPipHandler;
+ private RecentsTransitionHandler mRecentsHandler;
private StageCoordinator mSplitHandler;
+ private final KeyguardTransitionHandler mKeyguardHandler;
+ private UnfoldTransitionHandler mUnfoldHandler;
private static class MixedTransition {
static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
@@ -61,17 +78,31 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
/** Both the display and split-state (enter/exit) is changing */
static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2;
+ /** Pip was entered while handling an intent with its own remoteTransition. */
+ static final int TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE = 3;
+
+ /** Recents transition while split-screen foreground. */
+ static final int TYPE_RECENTS_DURING_SPLIT = 4;
+
+ /** Keyguard exit/occlude/unocclude transition. */
+ static final int TYPE_KEYGUARD = 5;
+
+ /** Fuld/Unfold transition. */
+ static final int TYPE_UNFOLD = 6;
+
/** The default animation for this mixed transition. */
static final int ANIM_TYPE_DEFAULT = 0;
/** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */
static final int ANIM_TYPE_GOING_HOME = 1;
+ /** For RECENTS_DURING_SPLIT, is set when this turns into a pair->pair task switch. */
+ static final int ANIM_TYPE_PAIR_TO_PAIR = 1;
+
final int mType;
- int mAnimType = 0;
+ int mAnimType = ANIM_TYPE_DEFAULT;
final IBinder mTransition;
- Transitions.TransitionFinishCallback mFinishCallback = null;
Transitions.TransitionHandler mLeftoversHandler = null;
WindowContainerTransaction mFinishWCT = null;
@@ -85,14 +116,35 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
mType = type;
mTransition = transition;
}
+
+ void joinFinishArgs(WindowContainerTransaction wct,
+ WindowContainerTransactionCallback wctCB) {
+ if (wctCB != null) {
+ // Technically can probably support 1, but don't want to encourage CB usage since
+ // it creates instabliity, so just throw.
+ throw new IllegalArgumentException("Can't mix transitions that require finish"
+ + " sync callback");
+ }
+ if (wct != null) {
+ if (mFinishWCT == null) {
+ mFinishWCT = wct;
+ } else {
+ mFinishWCT.merge(wct, true /* transfer */);
+ }
+ }
+ }
}
private final ArrayList<MixedTransition> mActiveTransitions = new ArrayList<>();
public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player,
Optional<SplitScreenController> splitScreenControllerOptional,
- Optional<PipTouchHandler> pipTouchHandlerOptional) {
+ Optional<PipTouchHandler> pipTouchHandlerOptional,
+ Optional<RecentsTransitionHandler> recentsHandlerOptional,
+ KeyguardTransitionHandler keyguardHandler,
+ Optional<UnfoldTransitionHandler> unfoldHandler) {
mPlayer = player;
+ mKeyguardHandler = keyguardHandler;
if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent()
&& splitScreenControllerOptional.isPresent()) {
// Add after dependencies because it is higher priority
@@ -103,6 +155,11 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
if (mSplitHandler != null) {
mSplitHandler.setMixedHandler(this);
}
+ mRecentsHandler = recentsHandlerOptional.orElse(null);
+ if (mRecentsHandler != null) {
+ mRecentsHandler.addMixer(this);
+ }
+ mUnfoldHandler = unfoldHandler.orElse(null);
}, this);
}
}
@@ -111,7 +168,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
- if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitScreenVisible()) {
+ if (mSplitHandler.requestImpliesSplitToPip(request)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while "
+ "Split-Screen is active, so treat it as Mixed.");
if (request.getRemoteTransition() != null) {
@@ -125,26 +182,104 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
mPipHandler.augmentRequest(transition, request, out);
mSplitHandler.addEnterOrExitIfNeeded(request, out);
return out;
+ } else if (request.getRemoteTransition() != null
+ && TransitionUtil.isOpeningType(request.getType())
+ && (request.getTriggerTask() == null
+ || (request.getTriggerTask().topActivityType != ACTIVITY_TYPE_HOME
+ && request.getTriggerTask().topActivityType != ACTIVITY_TYPE_RECENTS))) {
+ // Only select transitions with an intent-provided remote-animation because that will
+ // usually grab priority and often won't handle PiP. If there isn't an intent-provided
+ // remote, then the transition will be dispatched normally and the PipHandler will
+ // pick it up.
+ Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler =
+ mPlayer.dispatchRequest(transition, request, this);
+ if (handler == null) {
+ return null;
+ }
+ final MixedTransition mixed = new MixedTransition(
+ MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
+ mixed.mLeftoversHandler = handler.first;
+ mActiveTransitions.add(mixed);
+ return handler.second;
+ } else if (mSplitHandler.isSplitScreenVisible()
+ && isOpeningType(request.getType())
+ && request.getTriggerTask() != null
+ && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+ && request.getTriggerTask().getActivityType() == ACTIVITY_TYPE_HOME) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a going-home request while "
+ + "Split-Screen is foreground, so treat it as Mixed.");
+ Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler =
+ mPlayer.dispatchRequest(transition, request, this);
+ if (handler == null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ " Lean on the remote transition handler to fetch a proper remote via"
+ + " TransitionFilter");
+ handler = new Pair<>(
+ mPlayer.getRemoteTransitionHandler(),
+ new WindowContainerTransaction());
+ }
+ final MixedTransition mixed = new MixedTransition(
+ MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
+ mixed.mLeftoversHandler = handler.first;
+ mActiveTransitions.add(mixed);
+ return handler.second;
+ } else if (mUnfoldHandler != null && mUnfoldHandler.hasUnfold(request)) {
+ final WindowContainerTransaction wct =
+ mUnfoldHandler.handleRequest(transition, request);
+ if (wct != null) {
+ final MixedTransition mixed = new MixedTransition(
+ MixedTransition.TYPE_UNFOLD, transition);
+ mixed.mLeftoversHandler = mUnfoldHandler;
+ mActiveTransitions.add(mixed);
+ }
+ return wct;
}
return null;
}
+ @Override
+ public Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT) {
+ if (mRecentsHandler != null && mSplitHandler.isSplitScreenVisible()) {
+ return this;
+ }
+ return null;
+ }
+
+ @Override
+ public void setRecentsTransition(IBinder transition) {
+ if (mSplitHandler.isSplitScreenVisible()) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+ + "Split-Screen is foreground, so treat it as Mixed.");
+ final MixedTransition mixed = new MixedTransition(
+ MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
+ mixed.mLeftoversHandler = mRecentsHandler;
+ mActiveTransitions.add(mixed);
+ } else {
+ throw new IllegalStateException("Accepted a recents transition but don't know how to"
+ + " handle it");
+ }
+ }
+
private TransitionInfo subCopy(@NonNull TransitionInfo info,
@WindowManager.TransitionType int newType, boolean withChanges) {
final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
+ out.setTrack(info.getTrack());
+ out.setDebugId(info.getDebugId());
if (withChanges) {
for (int i = 0; i < info.getChanges().size(); ++i) {
out.getChanges().add(info.getChanges().get(i));
}
}
- out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ out.addRoot(info.getRoot(i));
+ }
out.setAnimationOptions(info.getAnimationOptions());
return out;
}
private boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
return change.getTaskInfo() != null
- && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME;
+ && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
}
private boolean isWallpaper(@NonNull TransitionInfo.Change change) {
@@ -156,12 +291,26 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+
MixedTransition mixed = null;
for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
if (mActiveTransitions.get(i).mTransition != transition) continue;
mixed = mActiveTransitions.get(i);
break;
}
+
+ // Offer Keyguard the opportunity to take over lock transitions - ideally we could know by
+ // the time of handleRequest, but we need more information than is available at that time.
+ if (KeyguardTransitionHandler.handles(info)) {
+ if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Converting mixed transition into a keyguard transition");
+ onTransitionConsumed(transition, false, null);
+ }
+ mixed = new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
+ mActiveTransitions.add(mixed);
+ }
+
if (mixed == null) return false;
if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
@@ -169,6 +318,30 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
finishCallback);
} else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
return false;
+ } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
+ return animateOpenIntentWithRemoteAndPip(mixed, info, startTransaction,
+ finishTransaction, finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ // Pip auto-entering info might be appended to recent transition like pressing
+ // home-key in 3-button navigation. This offers split handler the opportunity to
+ // handle split to pip animation.
+ if (mPipHandler.isEnteringPip(change, info.getType())
+ && mSplitHandler.getSplitItemPosition(change.getLastParent())
+ != SPLIT_POSITION_UNDEFINED) {
+ return animateEnterPipFromSplit(mixed, info, startTransaction,
+ finishTransaction, finishCallback);
+ }
+ }
+
+ return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction,
+ finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
+ return animateKeyguard(mixed, info, startTransaction, finishTransaction,
+ finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
+ return animateUnfold(mixed, info, startTransaction, finishTransaction, finishCallback);
} else {
mActiveTransitions.remove(mixed);
throw new IllegalStateException("Starting mixed animation without a known mixed type? "
@@ -176,13 +349,69 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
}
}
+ private boolean animateOpenIntentWithRemoteAndPip(@NonNull MixedTransition mixed,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ TransitionInfo.Change pipChange = null;
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (mPipHandler.isEnteringPip(change, info.getType())) {
+ if (pipChange != null) {
+ throw new IllegalStateException("More than 1 pip-entering changes in one"
+ + " transition? " + info);
+ }
+ pipChange = change;
+ info.getChanges().remove(i);
+ }
+ }
+ Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
+ --mixed.mInFlightSubAnimations;
+ mixed.joinFinishArgs(wct, wctCB);
+ if (mixed.mInFlightSubAnimations > 0) return;
+ mActiveTransitions.remove(mixed);
+ finishCallback.onTransitionFinished(mixed.mFinishWCT, wctCB);
+ };
+ if (pipChange == null) {
+ if (mixed.mLeftoversHandler != null) {
+ mixed.mInFlightSubAnimations = 1;
+ if (mixed.mLeftoversHandler.startAnimation(mixed.mTransition,
+ info, startTransaction, finishTransaction, finishCB)) {
+ return true;
+ }
+ }
+ mActiveTransitions.remove(mixed);
+ return false;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
+ + " animation because remote-animation likely doesn't support it");
+ // Split the transition into 2 parts: the pip part and the rest.
+ mixed.mInFlightSubAnimations = 2;
+ // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+ // we need a separate one to send over to launcher.
+ SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+
+ mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
+
+ // Dispatch the rest of the transition normally.
+ if (mixed.mLeftoversHandler != null
+ && mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info,
+ startTransaction, finishTransaction, finishCB)) {
+ return true;
+ }
+ mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, info,
+ startTransaction, finishTransaction, finishCB, this);
+ return true;
+ }
+
private boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
- + "entering PIP while Split-Screen is active.");
+ + "entering PIP while Split-Screen is foreground.");
TransitionInfo.Change pipChange = null;
TransitionInfo.Change wallpaper = null;
final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
@@ -205,20 +434,22 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
}
if (pipChange == null) {
// um, something probably went wrong.
+ mActiveTransitions.remove(mixed);
return false;
}
final boolean isGoingHome = homeIsOpening;
- mixed.mFinishCallback = finishCallback;
Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
--mixed.mInFlightSubAnimations;
+ mixed.joinFinishArgs(wct, wctCB);
if (mixed.mInFlightSubAnimations > 0) return;
mActiveTransitions.remove(mixed);
if (isGoingHome) {
mSplitHandler.onTransitionAnimationComplete();
}
- mixed.mFinishCallback.onTransitionFinished(wct, wctCB);
+ finishCallback.onTransitionFinished(mixed.mFinishWCT, wctCB);
};
- if (isGoingHome) {
+ if (isGoingHome || mSplitHandler.getSplitItemPosition(pipChange.getLastParent())
+ != SPLIT_POSITION_UNDEFINED) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
+ "since entering-PiP caused us to leave split and return home.");
// We need to split the transition into 2 parts: the pip part (animated by pip)
@@ -247,6 +478,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
}
}
+ mPipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
mPipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
finishCB);
// Dispatch the rest of the transition normally. This will most-likely be taken by
@@ -287,10 +519,27 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
}
/**
+ * This is intended to be called by SplitCoordinator as a helper to mix a split handling
+ * transition with an entering-pip change. The use-case for this is when an auto-pip change
+ * gets collected into the transition which has already claimed by
+ * StageCoordinator.handleRequest. This happens when launching a fullscreen app while having an
+ * auto-pip activity in the foreground split pair.
+ */
+ // TODO(b/287704263): Remove when split/mixed are reversed.
+ public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
+ Transitions.TransitionFinishCallback finishCallback) {
+ final MixedTransition mixed = new MixedTransition(
+ MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition);
+ mActiveTransitions.add(mixed);
+ return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback);
+ }
+
+ /**
* This is intended to be called by SplitCoordinator as a helper to mix an already-pending
* split transition with a display-change. The use-case for this is when a display
* change/rotation gets collected into a split-screen enter/exit transition which has already
- * been claimed by StageCoordinator.handleRequest . This happens during launcher tests.
+ * been claimed by StageCoordinator.handleRequest. This happens during launcher tests.
*/
public boolean animatePendingSplitWithDisplayChange(@NonNull IBinder transition,
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT,
@@ -308,7 +557,6 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
unlinkMissingParents(everythingElse);
final MixedTransition mixed = new MixedTransition(
MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
- mixed.mFinishCallback = finishCallback;
mActiveTransitions.add(mixed);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
+ "and split change.");
@@ -317,20 +565,10 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
--mixed.mInFlightSubAnimations;
- if (wctCB != null) {
- throw new IllegalArgumentException("Can't mix transitions that require finish"
- + " sync callback");
- }
- if (wct != null) {
- if (mixed.mFinishWCT == null) {
- mixed.mFinishWCT = wct;
- } else {
- mixed.mFinishWCT.merge(wct, true /* transfer */);
- }
- }
+ mixed.joinFinishArgs(wct, wctCB);
if (mixed.mInFlightSubAnimations > 0) return;
mActiveTransitions.remove(mixed);
- mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */);
+ finishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */);
};
// Dispatch the display change. This will most-likely be taken by the default handler.
@@ -342,23 +580,124 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
// Note: at this point, startT has probably already been applied, so we are basically
// giving splitHandler an empty startT. This is currently OK because display-change will
// grab a screenshot and paste it on top anyways.
- mSplitHandler.startPendingAnimation(
- transition, everythingElse, startT, finishT, finishCB);
+ mSplitHandler.startPendingAnimation(transition, everythingElse, startT, finishT, finishCB);
return true;
}
+ private boolean animateRecentsDuringSplit(@NonNull final MixedTransition mixed,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ // Split-screen is only interested in the recents transition finishing (and merging), so
+ // just wrap finish and start recents animation directly.
+ Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
+ mixed.mInFlightSubAnimations = 0;
+ mActiveTransitions.remove(mixed);
+ // If pair-to-pair switching, the post-recents clean-up isn't needed.
+ wct = wct != null ? wct : new WindowContainerTransaction();
+ if (mixed.mAnimType != MixedTransition.ANIM_TYPE_PAIR_TO_PAIR) {
+ mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+ } else {
+ // notify pair-to-pair recents animation finish
+ mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
+ }
+ mSplitHandler.onTransitionAnimationComplete();
+ finishCallback.onTransitionFinished(wct, wctCB);
+ };
+ mixed.mInFlightSubAnimations = 1;
+ mSplitHandler.onRecentsInSplitAnimationStart(info);
+ final boolean handled = mixed.mLeftoversHandler.startAnimation(mixed.mTransition, info,
+ startTransaction, finishTransaction, finishCB);
+ if (!handled) {
+ mSplitHandler.onRecentsInSplitAnimationCanceled();
+ mActiveTransitions.remove(mixed);
+ }
+ return handled;
+ }
+
+ private boolean animateKeyguard(@NonNull final MixedTransition mixed,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
+ mixed.mInFlightSubAnimations--;
+ if (mixed.mInFlightSubAnimations == 0) {
+ mActiveTransitions.remove(mixed);
+ finishCallback.onTransitionFinished(wct, wctCB);
+ }
+ };
+ mixed.mInFlightSubAnimations++;
+ // Sync pip state.
+ if (mPipHandler != null) {
+ mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+ }
+ if (!mKeyguardHandler.startAnimation(
+ mixed.mTransition, info, startTransaction, finishTransaction, finishCB)) {
+ mixed.mInFlightSubAnimations--;
+ return false;
+ }
+ return true;
+ }
+
+ private boolean animateUnfold(@NonNull final MixedTransition mixed,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
+ mixed.mInFlightSubAnimations--;
+ if (mixed.mInFlightSubAnimations > 0) return;
+ mActiveTransitions.remove(mixed);
+ finishCallback.onTransitionFinished(wct, wctCB);
+ };
+ mixed.mInFlightSubAnimations = 1;
+ // Sync pip state.
+ if (mPipHandler != null) {
+ mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+ }
+ return mUnfoldHandler.startAnimation(
+ mixed.mTransition, info, startTransaction, finishTransaction, finishCB);
+ }
+
+ /** Use to when split use intent to enter, check if this enter transition should be mixed or
+ * not.*/
+ public boolean shouldSplitEnterMixed(PendingIntent intent) {
+ // Check if this intent package is same as pip one or not, if true we want let the pip
+ // task enter split.
+ if (mPipHandler != null) {
+ return mPipHandler.isInPipPackage(SplitScreenUtils.getPackageName(intent.getIntent()));
+ }
+ return false;
+ }
+
+ /** @return whether the transition-request represents a pip-entry. */
+ public boolean requestHasPipEnter(TransitionRequestInfo request) {
+ return mPipHandler.requestHasPipEnter(request);
+ }
+
+ /** Whether a particular change is a window that is entering pip. */
+ // TODO(b/287704263): Remove when split/mixed are reversed.
+ public boolean isEnteringPip(TransitionInfo.Change change,
+ @WindowManager.TransitionType int transitType) {
+ return mPipHandler.isEnteringPip(change, transitType);
+ }
+
@Override
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
for (int i = 0; i < mActiveTransitions.size(); ++i) {
- if (mActiveTransitions.get(i) != mergeTarget) continue;
+ if (mActiveTransitions.get(i).mTransition != mergeTarget) continue;
MixedTransition mixed = mActiveTransitions.get(i);
if (mixed.mInFlightSubAnimations <= 0) {
// Already done, so no need to end it.
return;
}
- if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
+ if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
+ // queue since no actual animation.
+ } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) {
boolean ended = mSplitHandler.end();
// If split couldn't end (because it is remote), then don't end everything else
@@ -372,8 +711,24 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
} else {
mPipHandler.end();
}
- } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
- // queue
+ } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
+ mPipHandler.end();
+ if (mixed.mLeftoversHandler != null) {
+ mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+ finishCallback);
+ }
+ } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
+ if (mSplitHandler.isPendingEnter(transition)) {
+ // Recents -> enter-split means that we are switching from one pair to
+ // another pair.
+ mixed.mAnimType = MixedTransition.ANIM_TYPE_PAIR_TO_PAIR;
+ }
+ mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+ finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
+ mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
+ mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
} else {
throw new IllegalStateException("Playing a mixed transition with unknown type? "
+ mixed.mType);
@@ -393,6 +748,14 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler {
if (mixed == null) return;
if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+ } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
+ mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+ } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
+ mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+ } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
+ mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
+ } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
+ mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
}
}
}
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 928e71f8d3a6..e52fd00e7df7 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
@@ -21,8 +21,10 @@ import static android.app.ActivityOptions.ANIM_CUSTOM;
import static android.app.ActivityOptions.ANIM_NONE;
import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
import static android.app.ActivityOptions.ANIM_SCALE_UP;
+import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION;
import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE;
@@ -35,12 +37,9 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
-import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_RELAUNCH;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
@@ -59,11 +58,9 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
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.loadAttributeAnimation;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -78,6 +75,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
@@ -91,7 +89,6 @@ import android.view.Choreographer;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.WindowManager;
-import android.view.WindowManager.TransitionType;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Transformation;
@@ -106,12 +103,14 @@ import com.android.internal.policy.AttributeCache;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.List;
@@ -139,6 +138,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private final Rect mInsets = new Rect(0, 0, 0, 0);
private float mTransitionAnimationScaleSetting = 1.0f;
+ private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
private final int mCurrentUserId;
private Drawable mEnterpriseThumbnailDrawable;
@@ -159,7 +159,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@NonNull DisplayController displayController,
@NonNull TransactionPool transactionPool,
@NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
- @NonNull ShellExecutor animExecutor) {
+ @NonNull ShellExecutor animExecutor,
+ @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) {
mDisplayController = displayController;
mTransactionPool = transactionPool;
mContext = context;
@@ -170,6 +171,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
mCurrentUserId = UserHandle.myUserId();
mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
shellInit.addInitCallback(this::onInit, this);
+ mRootTDAOrganizer = rootTDAOrganizer;
}
private void onInit() {
@@ -258,6 +260,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
// This is the only way to get display-id currently, so check display capabilities here.
final DisplayLayout displayLayout = displayController.getDisplayLayout(
topTaskInfo.displayId);
+ // This condition should be true when using gesture navigation or the screen size is large
+ // (>600dp) because the bar is small relative to screen.
+ if (displayLayout.allowSeamlessRotationDespiteNavBarMoving()) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " nav bar allows seamless.");
+ return ROTATION_ANIMATION_SEAMLESS;
+ }
// For the upside down rotation we don't rotate seamlessly as the navigation bar moves
// position. Note most apps (using orientation:sensor or user as opposed to fullSensor)
// will not enter the reverse portrait orientation, so actually the orientation won't
@@ -270,13 +278,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
return animationHint;
}
- // If the navigation bar can't change sides, then it will jump when we change orientations
- // and we don't rotate seamlessly - unless that is allowed, e.g. with gesture navigation
- // where the navbar is low-profile enough that this isn't very noticeable.
- if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving()
- && (!(displayLayout.navigationBarCanMove()
- && (displayChange.getStartAbsBounds().width()
- != displayChange.getStartAbsBounds().height())))) {
+ // If the navigation bar cannot change sides, then it will jump when changing orientation
+ // so do not use seamless rotation.
+ if (!displayLayout.navigationBarCanMove()) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
" nav bar changes sides, so not seamless.");
return animationHint;
@@ -300,6 +304,15 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
return true;
}
+ // Early check if the transition doesn't warrant an animation.
+ if (Transitions.isAllNoAnimation(info) || Transitions.isAllOrderOnly(info)
+ || (info.getFlags() & WindowManager.TRANSIT_FLAG_INVISIBLE) != 0) {
+ startTransaction.apply();
+ finishTransaction.apply();
+ finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ return true;
+ }
+
if (mAnimations.containsKey(transition)) {
throw new IllegalStateException("Got a duplicate startAnimation call for "
+ transition);
@@ -318,6 +331,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@ColorInt int backgroundColorForTransition = 0;
final int wallpaperTransit = getWallpaperTransitType(info);
+ boolean isDisplayRotationAnimationStarted = false;
+ final boolean isDreamTransition = isDreamTransition(info);
+ final boolean isOnlyTranslucent = isOnlyTranslucent(info);
+
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (change.hasAllFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY
@@ -332,15 +349,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
continue;
}
final boolean isTask = change.getTaskInfo() != null;
+ final int mode = change.getMode();
boolean isSeamlessDisplayChange = false;
- if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) {
+ if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
if (info.getType() == TRANSIT_CHANGE) {
final int anim = getRotationAnimationHint(change, info, mDisplayController);
isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS;
if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) {
startRotationAnimation(startTransaction, change, info, anim, animations,
onAnimFinish);
+ isDisplayRotationAnimationStarted = true;
continue;
}
} else {
@@ -349,7 +368,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
}
- if (change.getMode() == TRANSIT_CHANGE) {
+ if (mode == TRANSIT_CHANGE) {
// If task is child task, only set position in parent and update crop when needed.
if (isTask && change.getParent() != null
&& info.getChange(change.getParent()).getTaskInfo() != null) {
@@ -374,9 +393,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
continue;
}
// No default animation for this, so just update bounds/position.
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
startTransaction.setPosition(change.getLeash(),
- change.getEndAbsBounds().left - info.getRootOffset().x,
- change.getEndAbsBounds().top - info.getRootOffset().y);
+ change.getEndAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
+ change.getEndAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
// Seamless display transition doesn't need to animate.
if (isSeamlessDisplayChange) continue;
if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)
@@ -395,19 +415,27 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
}
+ // Hide the invisible surface directly without animating it if there is a display
+ // rotation animation playing.
+ if (isDisplayRotationAnimationStarted && TransitionUtil.isClosingType(mode)) {
+ startTransaction.hide(change.getLeash());
+ continue;
+ }
+
+ // The back gesture has animated this change before transition happen, so here we don't
+ // play the animation again.
+ if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) {
+ continue;
+ }
// Don't animate anything that isn't independent.
if (!TransitionInfo.isIndependent(change, info)) continue;
- Animation a = loadAnimation(info, change, wallpaperTransit);
+ Animation a = loadAnimation(info, change, wallpaperTransit, isDreamTransition);
if (a != null) {
if (isTask) {
- final @TransitionType int type = info.getType();
- final boolean isOpenOrCloseTransition = type == TRANSIT_OPEN
- || type == TRANSIT_CLOSE
- || type == TRANSIT_TO_FRONT
- || type == TRANSIT_TO_BACK;
final boolean isTranslucent = (change.getFlags() & FLAG_TRANSLUCENT) != 0;
- if (isOpenOrCloseTransition && !isTranslucent
+ if (!isTranslucent && TransitionUtil.isOpenOrCloseMode(mode)
+ && TransitionUtil.isOpenOrCloseMode(info.getType())
&& wallpaperTransit == WALLPAPER_TRANSITION_NONE) {
// Use the overview background as the background for the animation
final Context uiContext = ActivityThread.currentActivityThread()
@@ -415,6 +443,34 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
backgroundColorForTransition =
uiContext.getColor(R.color.overview_background);
}
+ if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN
+ && TransitionUtil.isOpeningType(info.getType())) {
+ // Need to flip the z-order of opening/closing because the WALLPAPER_OPEN
+ // always animates the closing task over the opening one while
+ // traditionally, an OPEN transition animates the opening over the closing.
+
+ // See Transitions#setupAnimHierarchy for details about these variables.
+ final int numChanges = info.getChanges().size();
+ final int zSplitLine = numChanges + 1;
+ if (TransitionUtil.isOpeningType(mode)) {
+ final int layer = zSplitLine - i;
+ startTransaction.setLayer(change.getLeash(), layer);
+ } else if (TransitionUtil.isClosingType(mode)) {
+ final int layer = zSplitLine + numChanges - i;
+ startTransaction.setLayer(change.getLeash(), layer);
+ }
+ } else if (isOnlyTranslucent && TransitionUtil.isOpeningType(info.getType())
+ && TransitionUtil.isClosingType(mode)) {
+ // If there is a closing translucent task in an OPENING transition, we will
+ // actually select a CLOSING animation, so move the closing task into
+ // the animating part of the z-order.
+
+ // See Transitions#setupAnimHierarchy for details about these variables.
+ final int numChanges = info.getChanges().size();
+ final int zSplitLine = numChanges + 1;
+ final int layer = zSplitLine + numChanges - i;
+ startTransaction.setLayer(change.getLeash(), layer);
+ }
}
final float cornerRadius;
@@ -431,9 +487,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a,
backgroundColorForTransition);
- boolean delayedEdgeExtension = false;
if (!isTask && a.hasExtension()) {
- if (!Transitions.isOpeningType(change.getMode())) {
+ if (!TransitionUtil.isOpeningType(mode)) {
// Can screenshot now (before startTransaction is applied)
edgeExtendWindow(change, a, startTransaction, finishTransaction);
} else {
@@ -441,28 +496,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
// may not be visible or ready yet.
postStartTransactionCallbacks
.add(t -> edgeExtendWindow(change, a, t, finishTransaction));
- delayedEdgeExtension = true;
}
}
- final Rect clipRect = Transitions.isClosingType(change.getMode())
+ final Rect clipRect = TransitionUtil.isClosingType(mode)
? new Rect(mRotator.getEndBoundsInStartRotation(change))
: new Rect(change.getEndAbsBounds());
clipRect.offsetTo(0, 0);
- if (delayedEdgeExtension) {
- // If the edge extension needs to happen after the startTransition has been
- // applied, then we want to only start the animation after the edge extension
- // postStartTransaction callback has been run
- postStartTransactionCallbacks.add(t ->
- startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
- mTransactionPool, mMainExecutor, mAnimExecutor,
- change.getEndRelOffset(), cornerRadius, clipRect));
- } else {
- startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
- mTransactionPool, mMainExecutor, mAnimExecutor,
- change.getEndRelOffset(), cornerRadius, clipRect);
- }
+ buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
+ mTransactionPool, mMainExecutor, change.getEndRelOffset(), cornerRadius,
+ clipRect);
if (info.getAnimationOptions() != null) {
attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(),
@@ -472,23 +516,29 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
if (backgroundColorForTransition != 0) {
- addBackgroundToTransition(info.getRootLeash(), backgroundColorForTransition,
- startTransaction, finishTransaction);
+ addBackgroundColorOnTDA(info, backgroundColorForTransition, startTransaction,
+ finishTransaction);
}
- // postStartTransactionCallbacks require that the start transaction is already
- // applied to run otherwise they may result in flickers and UI inconsistencies.
- boolean waitForStartTransactionApply = postStartTransactionCallbacks.size() > 0;
- startTransaction.apply(waitForStartTransactionApply);
-
- // Run tasks that require startTransaction to already be applied
- for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback :
- postStartTransactionCallbacks) {
- final SurfaceControl.Transaction t = mTransactionPool.acquire();
- postStartTransactionCallback.accept(t);
- t.apply();
- mTransactionPool.release(t);
+ if (postStartTransactionCallbacks.size() > 0) {
+ // postStartTransactionCallbacks require that the start transaction is already
+ // applied to run otherwise they may result in flickers and UI inconsistencies.
+ startTransaction.apply(true /* sync */);
+ // startTransaction is empty now, so fill it with the edge-extension setup
+ for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback :
+ postStartTransactionCallbacks) {
+ postStartTransactionCallback.accept(startTransaction);
+ }
}
+ startTransaction.apply();
+
+ // now start animations. they are started on another thread, so we have to post them
+ // *after* applying the startTransaction
+ mAnimExecutor.execute(() -> {
+ for (int i = 0; i < animations.size(); ++i) {
+ animations.get(i).start();
+ }
+ });
mRotator.cleanUp(finishTransaction);
TransitionMetrics.getInstance().reportAnimationStart(transition);
@@ -497,6 +547,63 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
return true;
}
+ private void addBackgroundColorOnTDA(@NonNull TransitionInfo info,
+ @ColorInt int color, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final Color bgColor = Color.valueOf(color);
+ final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() };
+
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ final int displayId = info.getRoot(i).getDisplayId();
+ final SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
+ .setName("animation-background")
+ .setCallsite("DefaultTransitionHandler")
+ .setColorLayer();
+
+ mRootTDAOrganizer.attachToDisplayArea(displayId, colorLayerBuilder);
+ final SurfaceControl backgroundSurface = colorLayerBuilder.build();
+ startTransaction.setColor(backgroundSurface, colorArray)
+ .setLayer(backgroundSurface, -1)
+ .show(backgroundSurface);
+ finishTransaction.remove(backgroundSurface);
+ }
+ }
+
+ private static boolean isDreamTransition(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Does `info` only contain translucent visibility changes (CHANGEs are ignored). We select
+ * different animations and z-orders for these
+ */
+ private static boolean isOnlyTranslucent(@NonNull TransitionInfo info) {
+ int translucentOpen = 0;
+ int translucentClose = 0;
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getMode() == TRANSIT_CHANGE) continue;
+ if (change.hasFlags(FLAG_TRANSLUCENT)) {
+ if (TransitionUtil.isOpeningType(change.getMode())) {
+ translucentOpen += 1;
+ } else {
+ translucentClose += 1;
+ }
+ } else {
+ return false;
+ }
+ }
+ return (translucentOpen + translucentClose) > 0;
+ }
+
@Override
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@@ -512,8 +619,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private void startRotationAnimation(SurfaceControl.Transaction startTransaction,
TransitionInfo.Change change, TransitionInfo info, int animHint,
ArrayList<Animator> animations, Runnable onAnimFinish) {
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession,
- mTransactionPool, startTransaction, change, info.getRootLeash(), animHint);
+ mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(),
+ animHint);
// The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
// content, and background color. The item of "animGroup" will be removed if the sub
// animation is finished. Then if the list becomes empty, the rotation animation is done.
@@ -525,8 +634,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
animations.removeAll(animGroupStore);
onAnimFinish.run();
};
- anim.startAnimation(animGroup, finishCallback, mTransitionAnimationScaleSetting,
- mMainExecutor, mAnimExecutor);
+ anim.buildAnimation(animGroup, finishCallback, mTransitionAnimationScaleSetting,
+ mMainExecutor);
for (int i = animGroup.size() - 1; i >= 0; i--) {
final Animator animator = animGroup.get(i);
animGroupStore.add(animator);
@@ -548,20 +657,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@Nullable
private Animation loadAnimation(@NonNull TransitionInfo info,
- @NonNull TransitionInfo.Change change, int wallpaperTransit) {
+ @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();
- final boolean isOpeningType = Transitions.isOpeningType(type);
- final boolean enter = Transitions.isOpeningType(changeMode);
+ final boolean isOpeningType = TransitionUtil.isOpeningType(type);
+ final boolean enter = TransitionUtil.isOpeningType(changeMode);
final boolean isTask = change.getTaskInfo() != null;
final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
final int overrideType = options != null ? options.getType() : ANIM_NONE;
- final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty;
- final Rect endBounds = Transitions.isClosingType(changeMode)
+ final Rect endBounds = TransitionUtil.isClosingType(changeMode)
? mRotator.getEndBoundsInStartRotation(change)
: change.getEndAbsBounds();
@@ -583,7 +692,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
} else if (type == TRANSIT_RELAUNCH) {
a = mTransitionAnimation.createRelaunchAnimation(endBounds, mInsets, endBounds);
} else if (overrideType == ANIM_CUSTOM
- && (canCustomContainer || options.getOverrideTaskTransition())) {
+ && (!isTask || options.getOverrideTaskTransition())) {
a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter
? options.getEnterResId() : options.getExitResId());
} else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) {
@@ -603,15 +712,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
} else if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0 && isOpeningType) {
// This received a transferred starting window, so don't animate
return null;
+ } else if (overrideType == ANIM_SCENE_TRANSITION) {
+ // If there's a scene-transition, then jump-cut.
+ return null;
} else {
- a = loadAttributeAnimation(info, change, wallpaperTransit, mTransitionAnimation);
+ a = loadAttributeAnimation(
+ info, change, wallpaperTransit, mTransitionAnimation, isDreamTransition);
}
if (a != null) {
if (!a.isInitialized()) {
- final int width = endBounds.width();
- final int height = endBounds.height();
- a.initialize(width, height, width, height);
+ final Rect animationRange = TransitionUtil.isClosingType(changeMode)
+ ? change.getStartAbsBounds() : change.getEndAbsBounds();
+ a.initialize(animationRange.width(), animationRange.height(),
+ endBounds.width(), endBounds.height());
}
a.restrictDuration(MAX_ANIMATION_DURATION);
a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
@@ -619,11 +733,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
return a;
}
- static void startSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+ /** Builds an animator for the surface and adds it to the `animations` list. */
+ static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations,
@NonNull Animation anim, @NonNull SurfaceControl leash,
@NonNull Runnable finishCallback, @NonNull TransactionPool pool,
- @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor,
- @Nullable Point position, float cornerRadius, @Nullable Rect clipRect) {
+ @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius,
+ @Nullable Rect clipRect) {
final SurfaceControl.Transaction transaction = pool.acquire();
final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
final Transformation transformation = new Transformation();
@@ -677,14 +792,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
});
animations.add(va);
- animExecutor.execute(va::start);
}
private void attachThumbnail(@NonNull ArrayList<Animator> animations,
@NonNull Runnable finishCallback, TransitionInfo.Change change,
TransitionInfo.AnimationOptions options, float cornerRadius) {
- final boolean isOpen = Transitions.isOpeningType(change.getMode());
- final boolean isClose = Transitions.isClosingType(change.getMode());
+ final boolean isOpen = TransitionUtil.isOpeningType(change.getMode());
+ final boolean isClose = TransitionUtil.isClosingType(change.getMode());
if (isOpen) {
if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) {
attachCrossProfileThumbnailAnimation(animations, finishCallback, change,
@@ -731,9 +845,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
};
a.restrictDuration(MAX_ANIMATION_DURATION);
a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
- startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
- mMainExecutor, mAnimExecutor, change.getEndRelOffset(),
- cornerRadius, change.getEndAbsBounds());
+ buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
+ mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds());
}
private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations,
@@ -756,9 +869,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
};
a.restrictDuration(MAX_ANIMATION_DURATION);
a.scaleCurrentDuration(mTransitionAnimationScaleSetting);
- startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
- mMainExecutor, mAnimExecutor, change.getEndRelOffset(),
- cornerRadius, change.getEndAbsBounds());
+ buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool,
+ mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds());
}
private static int getWallpaperTransitType(TransitionInfo info) {
@@ -768,16 +880,16 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) {
- if (Transitions.isOpeningType(change.getMode())) {
+ if (TransitionUtil.isOpeningType(change.getMode())) {
hasOpenWallpaper = true;
- } else if (Transitions.isClosingType(change.getMode())) {
+ } else if (TransitionUtil.isClosingType(change.getMode())) {
hasCloseWallpaper = true;
}
}
}
if (hasOpenWallpaper && hasCloseWallpaper) {
- return Transitions.isOpeningType(info.getType())
+ return TransitionUtil.isOpeningType(info.getType())
? WALLPAPER_TRANSITION_INTRA_OPEN : WALLPAPER_TRANSITION_INTRA_CLOSE;
} else if (hasOpenWallpaper) {
return WALLPAPER_TRANSITION_OPEN;
@@ -788,18 +900,31 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
}
+ /**
+ * Returns {@code true} if the default transition handler can run the override animation.
+ * @see #loadAnimation(TransitionInfo, TransitionInfo.Change, int, boolean)
+ */
+ public static boolean isSupportedOverrideAnimation(
+ @NonNull TransitionInfo.AnimationOptions options) {
+ final int animType = options.getType();
+ return animType == ANIM_CUSTOM || animType == ANIM_SCALE_UP
+ || animType == ANIM_THUMBNAIL_SCALE_UP || animType == ANIM_THUMBNAIL_SCALE_DOWN
+ || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS;
+ }
+
private static void applyTransformation(long time, SurfaceControl.Transaction t,
- SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix,
+ SurfaceControl leash, Animation anim, Transformation tmpTransformation, float[] matrix,
Point position, float cornerRadius, @Nullable Rect immutableClipRect) {
- anim.getTransformation(time, transformation);
+ tmpTransformation.clear();
+ anim.getTransformation(time, tmpTransformation);
if (position != null) {
- transformation.getMatrix().postTranslate(position.x, position.y);
+ tmpTransformation.getMatrix().postTranslate(position.x, position.y);
}
- t.setMatrix(leash, transformation.getMatrix(), matrix);
- t.setAlpha(leash, transformation.getAlpha());
+ t.setMatrix(leash, tmpTransformation.getMatrix(), matrix);
+ t.setAlpha(leash, tmpTransformation.getAlpha());
final Rect clipRect = immutableClipRect == null ? null : new Rect(immutableClipRect);
- Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE);
+ Insets extensionInsets = Insets.min(tmpTransformation.getInsets(), Insets.NONE);
if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) {
// Clip out any overflowing edge extension
clipRect.inset(extensionInsets);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
index bdcdb63d2cd6..cc4d268a0000 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
@@ -34,4 +34,9 @@ interface IShellTransitions {
* Unregisters a remote transition handler.
*/
oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2;
+
+ /**
+ * Retrieves the apply-token used by transactions in Shell
+ */
+ IBinder getShellApplyToken() = 3;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
index 61e92f355dc2..61e11e877b90 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
@@ -107,7 +107,7 @@ public class LegacyTransitions {
}
@Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
+ public void onAnimationCancelled() throws RemoteException {
mCancelled = true;
mApps = mWallpapers = mNonApps = null;
checkApply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
index 485b400f458d..4e3d220f1ea2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
@@ -63,7 +63,7 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
@NonNull Transitions.TransitionFinishCallback finishCallback) {
if (mTransition != transition) return false;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote"
- + " transition %s for %s.", mRemote, transition);
+ + " transition %s for #%d.", mRemote, info.getDebugId());
final IBinder.DeathRecipient remoteDied = () -> {
Log.e(Transitions.TAG, "Remote transition died, finishing");
@@ -113,9 +113,6 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
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_TRANSITIONS, "Using registered One-shot remote"
- + " transition %s for %s.", mRemote, transition);
-
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
@Override
public void onTransitionFinished(WindowContainerTransaction wct,
@@ -154,4 +151,10 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
+ " for %s: %s", transition, remote);
return new WindowContainerTransaction();
}
+
+ @Override
+ public String toString() {
+ return "OneShotRemoteHandler:" + mRemote.getDebugName() + ":"
+ + mRemote.getRemoteTransition();
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index b4e05848882c..a242c72db8b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -39,6 +39,7 @@ import androidx.annotation.BinderThread;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
@@ -93,10 +94,16 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (!Transitions.SHELL_TRANSITIONS_ROTATION && TransitionUtil.hasDisplayChange(info)) {
+ // Note that if the remote doesn't have permission ACCESS_SURFACE_FLINGER, some
+ // operations of the start transaction may be ignored.
+ mRequestedRemotes.remove(transition);
+ return false;
+ }
RemoteTransition pendingRemote = mRequestedRemotes.get(transition);
if (pendingRemote == null) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have "
- + "explicit remote, search filters for match for %s", transition, info);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition doesn't have "
+ + "explicit remote, search filters for match for %s", info);
// If no explicit remote, search filters until one matches
for (int i = mFilters.size() - 1; i >= 0; --i) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s",
@@ -110,8 +117,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
}
}
}
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for %s to %s",
- transition, pendingRemote);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for #%d to %s",
+ info.getDebugId(), pendingRemote);
if (pendingRemote == null) return false;
@@ -178,9 +185,10 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget).getRemoteTransition();
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt merge %s into %s",
- transition, remote);
+ final RemoteTransition remoteTransition = mRequestedRemotes.get(mergeTarget);
+ final IRemoteTransition remote = remoteTransition.getRemoteTransition();
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Merge into remote: %s",
+ remoteTransition);
if (remote == null) return;
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index b647f43da522..9ce22094d56b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -16,14 +16,12 @@
package com.android.wm.shell.transition;
-import static android.hardware.HardwareBuffer.RGBA_8888;
-import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
import static android.util.RotationUtils.deltaRotation;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
import static android.view.WindowManagerPolicyConstants.SCREEN_FREEZE_LAYER_BASE;
-import static com.android.wm.shell.transition.DefaultTransitionHandler.startSurfaceAnimation;
+import static com.android.wm.shell.transition.DefaultTransitionHandler.buildSurfaceAnimation;
import static com.android.wm.shell.transition.Transitions.TAG;
import android.animation.Animator;
@@ -33,12 +31,9 @@ import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Color;
-import android.graphics.ColorSpace;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
-import android.media.Image;
-import android.media.ImageReader;
import android.util.Slog;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -46,15 +41,15 @@ import android.view.SurfaceControl.Transaction;
import android.view.SurfaceSession;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
+import android.window.ScreenCapture;
import android.window.TransitionInfo;
import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
-import java.nio.ByteBuffer;
import java.util.ArrayList;
-import java.util.Arrays;
/**
* This class handles the rotation animation when the device is rotated.
@@ -144,14 +139,15 @@ class ScreenRotationAnimation {
t.reparent(mScreenshotLayer, mAnimLeash);
mStartLuma = change.getSnapshotLuma();
} else {
- SurfaceControl.LayerCaptureArgs args =
- new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl)
+ ScreenCapture.LayerCaptureArgs args =
+ new ScreenCapture.LayerCaptureArgs.Builder(mSurfaceControl)
.setCaptureSecureLayers(true)
.setAllowProtected(true)
.setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight))
+ .setHintForSeamlessTransition(true)
.build();
- SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
- SurfaceControl.captureLayers(args);
+ ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
+ ScreenCapture.captureLayers(args);
if (screenshotBuffer == null) {
Slog.w(TAG, "Unable to take screenshot of display");
return;
@@ -166,14 +162,14 @@ class ScreenRotationAnimation {
.setName("RotationLayer")
.build();
- final ColorSpace colorSpace = screenshotBuffer.getColorSpace();
+ TransitionAnimation.configureScreenshotLayer(t, mScreenshotLayer, screenshotBuffer);
final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
- t.setDataSpace(mScreenshotLayer, colorSpace.getDataSpace());
- t.setBuffer(mScreenshotLayer, hardwareBuffer);
t.show(mScreenshotLayer);
if (!isCustomRotate()) {
- mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace);
+ mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer,
+ screenshotBuffer.getColorSpace());
}
+ hardwareBuffer.close();
}
t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
@@ -233,7 +229,7 @@ class ScreenRotationAnimation {
} else if ((mEndWidth > mStartWidth) == (mEndHeight > mStartHeight)
&& (mEndWidth != mStartWidth || mEndHeight != mStartHeight)) {
// Display resizes without rotation change.
- final float scale = Math.max((float) mEndWidth / mStartHeight,
+ final float scale = Math.max((float) mEndWidth / mStartWidth,
(float) mEndHeight / mStartHeight);
matrix.setScale(scale, scale);
}
@@ -247,11 +243,11 @@ class ScreenRotationAnimation {
}
/**
- * Returns true if animating.
+ * Returns true if any animations were added to `animations`.
*/
- public boolean startAnimation(@NonNull ArrayList<Animator> animations,
+ boolean buildAnimation(@NonNull ArrayList<Animator> animations,
@NonNull Runnable finishCallback, float animationScale,
- @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) {
+ @NonNull ShellExecutor mainExecutor) {
if (mScreenshotLayer == null) {
// Can't do animation.
return false;
@@ -314,13 +310,11 @@ class ScreenRotationAnimation {
mRotateAlphaAnimation.restrictDuration(MAX_ANIMATION_DURATION);
mRotateAlphaAnimation.scaleCurrentDuration(animationScale);
- startScreenshotAlphaAnimation(animations, finishCallback, mainExecutor,
- animExecutor);
- startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor);
+ buildScreenshotAlphaAnimation(animations, finishCallback, mainExecutor);
+ startDisplayRotation(animations, finishCallback, mainExecutor);
} else {
- startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor);
- startScreenshotRotationAnimation(animations, finishCallback, mainExecutor,
- animExecutor);
+ startDisplayRotation(animations, finishCallback, mainExecutor);
+ startScreenshotRotationAnimation(animations, finishCallback, mainExecutor);
//startColorAnimation(mTransaction, animationScale);
}
@@ -328,27 +322,24 @@ class ScreenRotationAnimation {
}
private void startDisplayRotation(@NonNull ArrayList<Animator> animations,
- @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor,
- @NonNull ShellExecutor animExecutor) {
- startSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback,
- mTransactionPool, mainExecutor, animExecutor, null /* position */,
- 0 /* cornerRadius */, null /* clipRect */);
+ @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) {
+ buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback,
+ mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */,
+ null /* clipRect */);
}
private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations,
- @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor,
- @NonNull ShellExecutor animExecutor) {
- startSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback,
- mTransactionPool, mainExecutor, animExecutor, null /* position */,
- 0 /* cornerRadius */, null /* clipRect */);
+ @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) {
+ buildSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback,
+ mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */,
+ null /* clipRect */);
}
- private void startScreenshotAlphaAnimation(@NonNull ArrayList<Animator> animations,
- @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor,
- @NonNull ShellExecutor animExecutor) {
- startSurfaceAnimation(animations, mRotateAlphaAnimation, mAnimLeash, finishCallback,
- mTransactionPool, mainExecutor, animExecutor, null /* position */,
- 0 /* cornerRadius */, null /* clipRect */);
+ private void buildScreenshotAlphaAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) {
+ buildSurfaceAnimation(animations, mRotateAlphaAnimation, mAnimLeash, finishCallback,
+ mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */,
+ null /* clipRect */);
}
private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) {
@@ -403,93 +394,6 @@ class ScreenRotationAnimation {
mTransactionPool.release(t);
}
- /**
- * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
- * luminance at the borders of the bitmap
- * @return the average luminance of all the pixels at the borders of the bitmap
- */
- private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
- // Cannot read content from buffer with protected usage.
- if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
- || hasProtectedContent(hardwareBuffer)) {
- return 0;
- }
-
- ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
- hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
- ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
- Image image = ir.acquireLatestImage();
- if (image == null || image.getPlanes().length == 0) {
- return 0;
- }
-
- Image.Plane plane = image.getPlanes()[0];
- ByteBuffer buffer = plane.getBuffer();
- int width = image.getWidth();
- int height = image.getHeight();
- int pixelStride = plane.getPixelStride();
- int rowStride = plane.getRowStride();
- float[] borderLumas = new float[2 * width + 2 * height];
-
- // Grab the top and bottom borders
- int l = 0;
- for (int x = 0; x < width; x++) {
- borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
- borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
- }
-
- // Grab the left and right borders
- for (int y = 0; y < height; y++) {
- borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
- borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
- }
-
- // Cleanup
- ir.close();
-
- // Oh, is this too simple and inefficient for you?
- // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
- Arrays.sort(borderLumas);
- return borderLumas[borderLumas.length / 2];
- }
-
- /**
- * @return whether the hardwareBuffer passed in is marked as protected.
- */
- private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
- return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
- }
-
- private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
- int pixelStride, int rowStride) {
- int offset = y * rowStride + x * pixelStride;
- int pixel = 0;
- pixel |= (buffer.get(offset) & 0xff) << 16; // R
- pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G
- pixel |= (buffer.get(offset + 2) & 0xff); // B
- pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
- return Color.valueOf(pixel).luminance();
- }
-
- /**
- * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
- * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
- */
- private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) {
- if (surfaceControl == null) {
- return 0;
- }
-
- Rect crop = new Rect(0, 0, bounds.width(), bounds.height());
- SurfaceControl.ScreenshotHardwareBuffer buffer =
- SurfaceControl.captureLayers(surfaceControl, crop, 1);
- if (buffer == null) {
- return 0;
- }
-
- return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
- }
-
private static void applyColor(int startColor, int endColor, float[] rgbFloat,
float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java
new file mode 100644
index 000000000000..d2795959494a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SleepHandler.java
@@ -0,0 +1,63 @@
+/*
+ * 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.transition;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import java.util.ArrayList;
+
+/**
+ * A Simple handler that tracks SLEEP transitions. We track them specially since we (ab)use these
+ * as sentinels for fast-forwarding through animations when the screen is off.
+ *
+ * There should only be one SleepHandler and it is used explicitly by {@link Transitions} so we
+ * don't register it like a normal handler.
+ */
+class SleepHandler implements Transitions.TransitionHandler {
+ final ArrayList<IBinder> mSleepTransitions = new ArrayList<>();
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ mSleepTransitions.remove(transition);
+ startTransaction.apply();
+ finishCallback.onTransitionFinished(null, null);
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ mSleepTransitions.add(transition);
+ return new WindowContainerTransaction();
+ }
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishTransaction) {
+ mSleepTransitions.remove(transition);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
new file mode 100644
index 000000000000..e27e4f990407
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
@@ -0,0 +1,342 @@
+/*
+ * 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.transition;
+
+import static android.os.Build.IS_USER;
+
+import static com.android.wm.shell.nano.WmShellTransitionTraceProto.MAGIC_NUMBER_H;
+import static com.android.wm.shell.nano.WmShellTransitionTraceProto.MAGIC_NUMBER_L;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+
+import com.android.internal.util.TraceBuffer;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to collect and dump transition traces.
+ */
+public class Tracer implements ShellCommandHandler.ShellCommandActionHandler {
+ private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB
+ private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB
+
+ private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+ static final String WINSCOPE_EXT = ".winscope";
+ private static final String TRACE_FILE =
+ "/data/misc/wmtrace/shell_transition_trace" + WINSCOPE_EXT;
+
+ private final Object mEnabledLock = new Object();
+ private boolean mActiveTracingEnabled = false;
+
+ private final TraceBuffer.ProtoProvider mProtoProvider =
+ new TraceBuffer.ProtoProvider<MessageNano,
+ com.android.wm.shell.nano.WmShellTransitionTraceProto,
+ com.android.wm.shell.nano.Transition>() {
+ @Override
+ public int getItemSize(MessageNano proto) {
+ return proto.getCachedSize();
+ }
+
+ @Override
+ public byte[] getBytes(MessageNano proto) {
+ return MessageNano.toByteArray(proto);
+ }
+
+ @Override
+ public void write(
+ com.android.wm.shell.nano.WmShellTransitionTraceProto encapsulatingProto,
+ Queue<com.android.wm.shell.nano.Transition> buffer, OutputStream os)
+ throws IOException {
+ encapsulatingProto.transitions = buffer.toArray(
+ new com.android.wm.shell.nano.Transition[0]);
+ os.write(getBytes(encapsulatingProto));
+ }
+ };
+ private final TraceBuffer<MessageNano,
+ com.android.wm.shell.nano.WmShellTransitionTraceProto,
+ com.android.wm.shell.nano.Transition> mTraceBuffer
+ = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY, mProtoProvider,
+ (proto) -> handleOnEntryRemovedFromTrace(proto));
+ private final Map<Object, Runnable> mRemovedFromTraceCallbacks = new HashMap<>();
+
+ private final Map<Transitions.TransitionHandler, Integer> mHandlerIds = new HashMap<>();
+ private final Map<Transitions.TransitionHandler, Integer> mHandlerUseCountInTrace =
+ new HashMap<>();
+
+ /**
+ * Adds an entry in the trace to log that a transition has been dispatched to a handler.
+ *
+ * @param transitionId The id of the transition being dispatched.
+ * @param handler The handler the transition is being dispatched to.
+ */
+ public void logDispatched(int transitionId, Transitions.TransitionHandler handler) {
+ final int handlerId;
+ if (mHandlerIds.containsKey(handler)) {
+ handlerId = mHandlerIds.get(handler);
+ } else {
+ // + 1 to avoid 0 ids which can be confused with missing value when dumped to proto
+ handlerId = mHandlerIds.size() + 1;
+ mHandlerIds.put(handler, handlerId);
+ }
+
+ com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
+ proto.id = transitionId;
+ proto.dispatchTimeNs = SystemClock.elapsedRealtimeNanos();
+ proto.handler = handlerId;
+
+ final int useCountAfterAdd = mHandlerUseCountInTrace.getOrDefault(handler, 0) + 1;
+ mHandlerUseCountInTrace.put(handler, useCountAfterAdd);
+
+ mRemovedFromTraceCallbacks.put(proto, () -> {
+ final int useCountAfterRemove = mHandlerUseCountInTrace.get(handler) - 1;
+ mHandlerUseCountInTrace.put(handler, useCountAfterRemove);
+ });
+
+ mTraceBuffer.add(proto);
+ }
+
+ /**
+ * Adds an entry in the trace to log that a request to merge a transition was made.
+ *
+ * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged.
+ * @param playingTransitionId The id of the transition we was to merge the transition into.
+ */
+ public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) {
+ com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
+ proto.id = mergeRequestedTransitionId;
+ proto.mergeRequestTimeNs = SystemClock.elapsedRealtimeNanos();
+ proto.mergedInto = playingTransitionId;
+
+ mTraceBuffer.add(proto);
+ }
+
+ /**
+ * Adds an entry in the trace to log that a transition was merged by the handler.
+ *
+ * @param mergedTransitionId The id of the transition that was merged.
+ * @param playingTransitionId The id of the transition the transition was merged into.
+ */
+ public void logMerged(int mergedTransitionId, int playingTransitionId) {
+ com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
+ proto.id = mergedTransitionId;
+ proto.mergeTimeNs = SystemClock.elapsedRealtimeNanos();
+ proto.mergedInto = playingTransitionId;
+
+ mTraceBuffer.add(proto);
+ }
+
+ /**
+ * Adds an entry in the trace to log that a transition was aborted.
+ *
+ * @param transitionId The id of the transition that was aborted.
+ */
+ public void logAborted(int transitionId) {
+ com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
+ proto.id = transitionId;
+ proto.abortTimeNs = SystemClock.elapsedRealtimeNanos();
+
+ mTraceBuffer.add(proto);
+ }
+
+ /**
+ * Starts collecting transitions for the trace.
+ * If called while a trace is already running, this will reset the trace.
+ */
+ public void startTrace(@Nullable PrintWriter pw) {
+ if (IS_USER) {
+ LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+ return;
+ }
+ Trace.beginSection("Tracer#startTrace");
+ LogAndPrintln.i(pw, "Starting shell transition trace.");
+ synchronized (mEnabledLock) {
+ mActiveTracingEnabled = true;
+ mTraceBuffer.resetBuffer();
+ mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY);
+ }
+ Trace.endSection();
+ }
+
+ /**
+ * Stops collecting the transition trace and dump to trace to file.
+ *
+ * Dumps the trace to @link{TRACE_FILE}.
+ */
+ public void stopTrace(@Nullable PrintWriter pw) {
+ stopTrace(pw, new File(TRACE_FILE));
+ }
+
+ /**
+ * Stops collecting the transition trace and dump to trace to file.
+ * @param outputFile The file to dump the transition trace to.
+ */
+ public void stopTrace(@Nullable PrintWriter pw, File outputFile) {
+ if (IS_USER) {
+ LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+ return;
+ }
+ Trace.beginSection("Tracer#stopTrace");
+ LogAndPrintln.i(pw, "Stopping shell transition trace.");
+ synchronized (mEnabledLock) {
+ mActiveTracingEnabled = false;
+ writeTraceToFileLocked(pw, outputFile);
+ mTraceBuffer.resetBuffer();
+ mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY);
+ }
+ Trace.endSection();
+ }
+
+ /**
+ * Being called while taking a bugreport so that tracing files can be included in the bugreport.
+ *
+ * @param pw Print writer
+ */
+ public void saveForBugreport(@Nullable PrintWriter pw) {
+ if (IS_USER) {
+ LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+ return;
+ }
+ Trace.beginSection("TransitionTracer#saveForBugreport");
+ synchronized (mEnabledLock) {
+ final File outputFile = new File(TRACE_FILE);
+ writeTraceToFileLocked(pw, outputFile);
+ }
+ Trace.endSection();
+ }
+
+ private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) {
+ Trace.beginSection("TransitionTracer#writeTraceToFileLocked");
+ try {
+ com.android.wm.shell.nano.WmShellTransitionTraceProto proto =
+ new com.android.wm.shell.nano.WmShellTransitionTraceProto();
+ proto.magicNumber = MAGIC_NUMBER_VALUE;
+ writeHandlerMappingToProto(proto);
+ long timeOffsetNs =
+ TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
+ - SystemClock.elapsedRealtimeNanos();
+ proto.realToElapsedTimeOffsetNanos = timeOffsetNs;
+ int pid = android.os.Process.myPid();
+ LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath()
+ + " from process " + pid);
+ mTraceBuffer.writeTraceToFile(file, proto);
+ } catch (IOException e) {
+ LogAndPrintln.e(pw, "Unable to write buffer to file", e);
+ }
+ Trace.endSection();
+ }
+
+ private void writeHandlerMappingToProto(
+ com.android.wm.shell.nano.WmShellTransitionTraceProto proto) {
+ ArrayList<com.android.wm.shell.nano.HandlerMapping> handlerMappings = new ArrayList<>();
+ for (Transitions.TransitionHandler handler : mHandlerUseCountInTrace.keySet()) {
+ final int count = mHandlerUseCountInTrace.get(handler);
+ if (count > 0) {
+ com.android.wm.shell.nano.HandlerMapping mapping =
+ new com.android.wm.shell.nano.HandlerMapping();
+ mapping.id = mHandlerIds.get(handler);
+ mapping.name = handler.getClass().getName();
+ handlerMappings.add(mapping);
+ }
+ }
+ proto.handlerMappings = handlerMappings.toArray(
+ new com.android.wm.shell.nano.HandlerMapping[0]);
+ }
+
+ private void handleOnEntryRemovedFromTrace(Object proto) {
+ if (mRemovedFromTraceCallbacks.containsKey(proto)) {
+ mRemovedFromTraceCallbacks.get(proto).run();
+ mRemovedFromTraceCallbacks.remove(proto);
+ }
+ }
+
+ @Override
+ public boolean onShellCommand(String[] args, PrintWriter pw) {
+ switch (args[0]) {
+ case "start": {
+ startTrace(pw);
+ return true;
+ }
+ case "stop": {
+ stopTrace(pw);
+ return true;
+ }
+ case "save-for-bugreport": {
+ saveForBugreport(pw);
+ return true;
+ }
+ default: {
+ pw.println("Invalid command: " + args[0]);
+ printShellCommandHelp(pw, "");
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void printShellCommandHelp(PrintWriter pw, String prefix) {
+ pw.println(prefix + "start");
+ pw.println(prefix + " Start tracing the transitions.");
+ pw.println(prefix + "stop");
+ pw.println(prefix + " Stop tracing the transitions.");
+ pw.println(prefix + "save-for-bugreport");
+ pw.println(prefix + " Flush in memory transition trace to file.");
+ }
+
+ private static class LogAndPrintln {
+ private static final String LOG_TAG = "ShellTransitionTracer";
+
+ private static void i(@Nullable PrintWriter pw, String msg) {
+ Log.i(LOG_TAG, msg);
+ if (pw != null) {
+ pw.println(msg);
+ pw.flush();
+ }
+ }
+
+ private static void e(@Nullable PrintWriter pw, String msg) {
+ Log.e(LOG_TAG, msg);
+ if (pw != null) {
+ pw.println("ERROR: " + msg);
+ pw.flush();
+ }
+ }
+
+ private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) {
+ Log.e(LOG_TAG, msg, e);
+ if (pw != null) {
+ pw.println("ERROR: " + msg + " ::\n " + e);
+ pw.flush();
+ }
+ }
+ }
+}
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 ab792ee122c7..d978eafa97f3 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
@@ -18,12 +18,12 @@ package com.android.wm.shell.transition;
import static android.app.ActivityOptions.ANIM_FROM_STYLE;
import static android.app.ActivityOptions.ANIM_NONE;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.transitTypeToString;
+import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
@@ -43,54 +43,37 @@ import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Shader;
-import android.os.SystemProperties;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.animation.Animation;
import android.view.animation.Transformation;
+import android.window.ScreenCapture;
import android.window.TransitionInfo;
import com.android.internal.R;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.util.TransitionUtil;
/** The helper class that provides methods for adding styles to transition animations. */
public class TransitionAnimationHelper {
- /**
- * Restrict ability of activities overriding transition animation in a way such that
- * an activity can do it only when the transition happens within a same task.
- *
- * @see android.app.Activity#overridePendingTransition(int, int)
- */
- private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY =
- "persist.wm.disable_custom_task_animation";
-
- /**
- * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY
- */
- static final boolean sDisableCustomTaskAnimationProperty =
- SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true);
-
/** Loads the animation that is defined through attribute id for the given transition. */
@Nullable
public static Animation loadAttributeAnimation(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change change, int wallpaperTransit,
- @NonNull TransitionAnimation transitionAnimation) {
+ @NonNull TransitionAnimation transitionAnimation, boolean isDreamTransition) {
final int type = info.getType();
final int changeMode = change.getMode();
final int changeFlags = change.getFlags();
- final boolean enter = Transitions.isOpeningType(changeMode);
+ final boolean enter = TransitionUtil.isOpeningType(changeMode);
final boolean isTask = change.getTaskInfo() != null;
final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
final int overrideType = options != null ? options.getType() : ANIM_NONE;
- final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty;
- final boolean isDream =
- isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM;
int animAttr = 0;
boolean translucent = false;
- if (isDream) {
+ if (isDreamTransition) {
if (type == TRANSIT_OPEN) {
animAttr = enter
? R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation
@@ -120,10 +103,11 @@ public class TransitionAnimationHelper {
// We will translucent open animation for translucent activities and tasks. Choose
// WindowAnimation_activityOpenEnterAnimation and set translucent here, then
// TransitionAnimation loads appropriate animation later.
- if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) {
- translucent = true;
- }
- if (isTask && !translucent) {
+ translucent = (changeFlags & FLAG_TRANSLUCENT) != 0;
+ if (isTask && translucent && !enter) {
+ // For closing translucent tasks, use the activity close animation
+ animAttr = R.styleable.WindowAnimation_activityCloseExitAnimation;
+ } else if (isTask && !translucent) {
animAttr = enter
? R.styleable.WindowAnimation_taskOpenEnterAnimation
: R.styleable.WindowAnimation_taskOpenExitAnimation;
@@ -137,14 +121,14 @@ public class TransitionAnimationHelper {
? R.styleable.WindowAnimation_taskToFrontEnterAnimation
: R.styleable.WindowAnimation_taskToFrontExitAnimation;
} else if (type == TRANSIT_CLOSE) {
- if (isTask) {
+ if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
+ translucent = true;
+ }
+ if (isTask && !translucent) {
animAttr = enter
? R.styleable.WindowAnimation_taskCloseEnterAnimation
: R.styleable.WindowAnimation_taskCloseExitAnimation;
} else {
- if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
- translucent = true;
- }
animAttr = enter
? R.styleable.WindowAnimation_activityCloseEnterAnimation
: R.styleable.WindowAnimation_activityCloseExitAnimation;
@@ -157,10 +141,21 @@ public class TransitionAnimationHelper {
Animation a = null;
if (animAttr != 0) {
- if (overrideType == ANIM_FROM_STYLE && canCustomContainer) {
- a = transitionAnimation
- .loadAnimationAttr(options.getPackageName(), options.getAnimations(),
- animAttr, translucent);
+ if (overrideType == ANIM_FROM_STYLE && !isTask) {
+ final TransitionInfo.AnimationOptions.CustomActivityTransition customTransition =
+ getCustomActivityTransition(animAttr, options);
+ if (customTransition != null) {
+ a = loadCustomActivityTransition(
+ customTransition, options, enter, transitionAnimation);
+ } else {
+ a = transitionAnimation
+ .loadAnimationAttr(options.getPackageName(), options.getAnimations(),
+ animAttr, translucent);
+ }
+ } else if (translucent && !isTask && ((changeFlags & FLAGS_IS_NON_APP_WINDOW) == 0)) {
+ // Un-styled translucent activities technically have undefined animations; however,
+ // as is always the case, some apps now rely on this being no-animation, so skip
+ // loading animations here.
} else {
a = transitionAnimation.loadDefaultAnimationAttr(animAttr, translucent);
}
@@ -173,6 +168,37 @@ public class TransitionAnimationHelper {
return a;
}
+ static TransitionInfo.AnimationOptions.CustomActivityTransition getCustomActivityTransition(
+ int animAttr, TransitionInfo.AnimationOptions options) {
+ boolean isOpen = false;
+ switch (animAttr) {
+ case R.styleable.WindowAnimation_activityOpenEnterAnimation:
+ case R.styleable.WindowAnimation_activityOpenExitAnimation:
+ isOpen = true;
+ break;
+ case R.styleable.WindowAnimation_activityCloseEnterAnimation:
+ case R.styleable.WindowAnimation_activityCloseExitAnimation:
+ break;
+ default:
+ return null;
+ }
+
+ return options.getCustomActivityTransition(isOpen);
+ }
+
+ static Animation loadCustomActivityTransition(
+ @NonNull TransitionInfo.AnimationOptions.CustomActivityTransition transitionAnim,
+ TransitionInfo.AnimationOptions options, boolean enter,
+ TransitionAnimation transitionAnimation) {
+ final Animation a = transitionAnimation.loadAppTransitionAnimation(options.getPackageName(),
+ enter ? transitionAnim.getCustomEnterResId()
+ : transitionAnim.getCustomExitResId());
+ if (a != null && transitionAnim.getCustomBackgroundColor() != 0) {
+ a.setBackdropColor(transitionAnim.getCustomBackgroundColor());
+ }
+ return a;
+ }
+
/**
* Gets the background {@link ColorInt} for the given transition animation if it is set.
*
@@ -314,8 +340,8 @@ public class TransitionAnimationHelper {
.setBufferSize(extensionRect.width(), extensionRect.height())
.build();
- final SurfaceControl.LayerCaptureArgs captureArgs =
- new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend)
+ final ScreenCapture.LayerCaptureArgs captureArgs =
+ new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend)
.setSourceCrop(edgeBounds)
.setFrameScale(1)
.setPixelFormat(PixelFormat.RGBA_8888)
@@ -323,8 +349,8 @@ public class TransitionAnimationHelper {
.setAllowProtected(true)
.setCaptureSecureLayers(true)
.build();
- final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer =
- SurfaceControl.captureLayers(captureArgs);
+ final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer =
+ ScreenCapture.captureLayers(captureArgs);
if (edgeBuffer == null) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
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 39fe4559c88f..2327d86ab618 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
@@ -16,20 +16,27 @@
package com.android.wm.shell.transition;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
-import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.fixScale;
import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
+import static com.android.wm.shell.util.TransitionUtil.isClosingType;
+import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -44,6 +51,7 @@ import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
+import android.util.Pair;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.ITransitionPlayer;
@@ -61,26 +69,56 @@ import androidx.annotation.BinderThread;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.util.TransitionUtil;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-/** Plays transition animations */
-public class Transitions implements RemoteCallable<Transitions> {
+/**
+ * Plays transition animations. Within this player, each transition has a lifecycle.
+ * 1. When a transition is directly started or requested, it is added to "pending" state.
+ * 2. Once WMCore applies the transition and notifies, the transition moves to "ready" state.
+ * 3. When a transition starts animating, it is moved to the "active" state.
+ *
+ * Basically: --start--> PENDING --onTransitionReady--> READY --play--> ACTIVE --finish--> |
+ * --merge--> MERGED --^
+ *
+ * The READY and beyond lifecycle is managed per "track". Within a track, all the animations are
+ * serialized as described; however, multiple tracks can play simultaneously. This implies that,
+ * within a track, only one transition can be animating ("active") at a time.
+ *
+ * While a transition is animating in a track, transitions dispatched to the track will be queued
+ * in the "ready" state for their turn. At the same time, whenever a transition makes it to the
+ * head of the "ready" queue, it will attempt to merge to with the "active" transition. If the
+ * merge succeeds, it will be moved to the "active" transition's "merged" list and then the next
+ * "ready" transition can attempt to merge. Once the "active" transition animation is finished,
+ * the next "ready" transition can play.
+ *
+ * Track assignments are expected to be provided by WMCore and this generally tries to maintain
+ * the same assignments. If, however, WMCore decides that a transition conflicts with >1 active
+ * track, it will be marked as SYNC. This means that all currently active tracks must be flushed
+ * before the SYNC transition can play.
+ */
+public class Transitions implements RemoteCallable<Transitions>,
+ ShellCommandHandler.ShellCommandActionHandler {
static final String TAG = "ShellTransitions";
/** Set to {@code true} to enable shell transitions. */
public static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS
&& SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
@@ -110,6 +148,19 @@ public class Transitions implements RemoteCallable<Transitions> {
/** Transition type for maximize to freeform transition. */
public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9;
+ /** Transition type to freeform in desktop mode. */
+ public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10;
+
+ /** Transition type to freeform in desktop mode. */
+ public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11;
+
+ /** Transition type to fullscreen from desktop mode. */
+ public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12;
+
+ /** Transition type to animate back to fullscreen when drag to freeform is cancelled. */
+ public static final int TRANSIT_CANCEL_ENTERING_DESKTOP_MODE =
+ WindowManager.TRANSIT_FIRST_CUSTOM + 13;
+
private final WindowOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
@@ -120,12 +171,16 @@ public class Transitions implements RemoteCallable<Transitions> {
private final DisplayController mDisplayController;
private final ShellController mShellController;
private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
-
+ private final SleepHandler mSleepHandler = new SleepHandler();
+ private final Tracer mTracer = new Tracer();
private boolean mIsRegistered = false;
/** List of possible handlers. Ordered by specificity (eg. tapped back to front). */
private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>();
+ @Nullable
+ private final ShellCommandHandler mShellCommandHandler;
+
private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
/** List of {@link Runnable} instances to run when the last active transition has finished. */
@@ -133,18 +188,73 @@ public class Transitions implements RemoteCallable<Transitions> {
private float mTransitionAnimationScaleSetting = 1.0f;
+ /**
+ * How much time we allow for an animation to finish itself on sync. If it takes longer, we
+ * will force-finish it (on this end) which may leave it in a bad state but won't hang the
+ * device. This needs to be pretty small because it is an allowance for each queued animation,
+ * however it can't be too small since there is some potential IPC involved.
+ */
+ private static final int SYNC_ALLOWANCE_MS = 120;
+
+ /**
+ * Keyguard gets a more generous timeout to finish its animations, because we are always holding
+ * a sleep token during occlude/unocclude transitions and we want them to finish playing cleanly
+ */
+ private static final int SYNC_ALLOWANCE_KEYGUARD_MS = 2000;
+
+ /** For testing only. Disables the force-finish timeout on sync. */
+ private boolean mDisableForceSync = false;
+
private static final class ActiveTransition {
IBinder mToken;
TransitionHandler mHandler;
- boolean mMerged;
boolean mAborted;
TransitionInfo mInfo;
SurfaceControl.Transaction mStartT;
SurfaceControl.Transaction mFinishT;
+
+ /** Ordered list of transitions which have been merged into this one. */
+ private ArrayList<ActiveTransition> mMerged;
+
+ boolean isSync() {
+ return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0;
+ }
+
+ int getTrack() {
+ return mInfo != null ? mInfo.getTrack() : -1;
+ }
+
+ @Override
+ public String toString() {
+ if (mInfo != null && mInfo.getDebugId() >= 0) {
+ return "(#" + mInfo.getDebugId() + ")" + mToken + "@" + getTrack();
+ }
+ return mToken.toString() + "@" + getTrack();
+ }
}
- /** Keeps track of currently playing transitions in the order of receipt. */
- private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>();
+ private static class Track {
+ /** Keeps track of transitions which are ready to play but still waiting for their turn. */
+ final ArrayList<ActiveTransition> mReadyTransitions = new ArrayList<>();
+
+ /** The currently playing transition in this track. */
+ ActiveTransition mActiveTransition = null;
+
+ boolean isIdle() {
+ return mActiveTransition == null && mReadyTransitions.isEmpty();
+ }
+ }
+
+ /** Keeps track of transitions which have been started, but aren't ready yet. */
+ private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>();
+
+ /**
+ * Transitions which are ready to play, but haven't been sent to a track yet because a sync
+ * is ongoing.
+ */
+ private final ArrayList<ActiveTransition> mReadyDuringSync = new ArrayList<>();
+
+ private final ArrayList<Track> mTracks = new ArrayList<>();
public Transitions(@NonNull Context context,
@NonNull ShellInit shellInit,
@@ -152,8 +262,25 @@ public class Transitions implements RemoteCallable<Transitions> {
@NonNull WindowOrganizer organizer,
@NonNull TransactionPool pool,
@NonNull DisplayController displayController,
- @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
+ @NonNull ShellExecutor mainExecutor,
+ @NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor) {
+ this(context, shellInit, shellController, organizer, pool, displayController, mainExecutor,
+ mainHandler, animExecutor, null,
+ new RootTaskDisplayAreaOrganizer(mainExecutor, context));
+ }
+
+ public Transitions(@NonNull Context context,
+ @NonNull ShellInit shellInit,
+ @NonNull ShellController shellController,
+ @NonNull WindowOrganizer organizer,
+ @NonNull TransactionPool pool,
+ @NonNull DisplayController displayController,
+ @NonNull ShellExecutor mainExecutor,
+ @NonNull Handler mainHandler,
+ @NonNull ShellExecutor animExecutor,
+ @Nullable ShellCommandHandler shellCommandHandler,
+ @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) {
mOrganizer = organizer;
mContext = context;
mMainExecutor = mainExecutor;
@@ -161,7 +288,7 @@ public class Transitions implements RemoteCallable<Transitions> {
mDisplayController = displayController;
mPlayerImpl = new TransitionPlayerImpl();
mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
- displayController, pool, mainExecutor, mainHandler, animExecutor);
+ displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer);
mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
mShellController = shellController;
// The very last handler (0 in the list) should be the default one.
@@ -169,11 +296,15 @@ public class Transitions implements RemoteCallable<Transitions> {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default");
// Next lowest priority is remote transitions.
mHandlers.add(mRemoteTransitionHandler);
+ mShellCommandHandler = shellCommandHandler;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
shellInit.addInitCallback(this::onInit, this);
}
private void onInit() {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mOrganizer.shareTransactionQueue();
+ }
mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
this::createExternalInterface, this);
@@ -197,6 +328,10 @@ public class Transitions implements RemoteCallable<Transitions> {
// Pre-load the instance.
TransitionMetrics.getInstance();
}
+
+ if (mShellCommandHandler != null) {
+ mShellCommandHandler.addCommandCallback("transitions", this, this);
+ }
}
public boolean isRegistered() {
@@ -274,6 +409,10 @@ public class Transitions implements RemoteCallable<Transitions> {
mRemoteTransitionHandler.removeFiltered(remoteTransition);
}
+ RemoteTransitionHandler getRemoteTransitionHandler() {
+ return mRemoteTransitionHandler;
+ }
+
/** Registers an observer on the lifecycle of transitions. */
public void registerObserver(@NonNull TransitionObserver observer) {
mObservers.add(observer);
@@ -305,23 +444,15 @@ public class Transitions implements RemoteCallable<Transitions> {
* will be executed when the last active transition is finished.
*/
public void runOnIdle(Runnable runnable) {
- if (mActiveTransitions.isEmpty()) {
+ if (isIdle()) {
runnable.run();
} else {
mRunWhenIdleQueue.add(runnable);
}
}
- /** @return true if the transition was triggered by opening something vs closing something */
- public static boolean isOpeningType(@WindowManager.TransitionType int type) {
- return type == TRANSIT_OPEN
- || type == TRANSIT_TO_FRONT
- || type == TRANSIT_KEYGUARD_GOING_AWAY;
- }
-
- /** @return true if the transition was triggered by closing something vs opening something */
- public static boolean isClosingType(@WindowManager.TransitionType int type) {
- return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
+ void setDisableForceSyncForTest(boolean disable) {
+ mDisableForceSync = disable;
}
/**
@@ -371,8 +502,13 @@ public class Transitions implements RemoteCallable<Transitions> {
&& (change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
t.setAlpha(leash, 0.f);
}
+ finishT.show(leash);
} else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
finishT.hide(leash);
+ } else if (isOpening && mode == TRANSIT_CHANGE) {
+ // Just in case there is a race with another animation (eg. recents finish()).
+ // Changes are visible->visible so it's a problem if it isn't visible.
+ t.show(leash);
}
}
}
@@ -383,9 +519,11 @@ public class Transitions implements RemoteCallable<Transitions> {
*/
private static void setupAnimHierarchy(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
- boolean isOpening = isOpeningType(info.getType());
- if (info.getRootLeash().isValid()) {
- t.show(info.getRootLeash());
+ final int type = info.getType();
+ final boolean isOpening = isOpeningType(type);
+ final boolean isClosing = isClosingType(type);
+ for (int i = 0; i < info.getRootCount(); ++i) {
+ t.show(info.getRoot(i).getLeash());
}
final int numChanges = info.getChanges().size();
// Put animating stuff above this line and put static stuff below it.
@@ -403,18 +541,27 @@ public class Transitions implements RemoteCallable<Transitions> {
boolean hasParent = change.getParent() != null;
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
if (!hasParent) {
- t.reparent(leash, info.getRootLeash());
- t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x,
- change.getStartAbsBounds().top - info.getRootOffset().y);
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
+ t.setPosition(leash,
+ change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
+ change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
}
final int layer;
// Put all the OPEN/SHOW on top
if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
- // Wallpaper is always at the bottom.
- layer = -zSplitLine;
+ // Wallpaper is always at the bottom, opening wallpaper on top of closing one.
+ if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
+ layer = -zSplitLine + numChanges - i;
+ } else {
+ layer = -zSplitLine - i;
+ }
} else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
- if (isOpening) {
+ if (isOpening
+ // This is for when an activity launches while a different transition is
+ // collecting.
+ || change.hasFlags(FLAG_MOVED_TO_TOP)) {
// put on top
layer = zSplitLine + numChanges - i;
} else {
@@ -430,45 +577,148 @@ public class Transitions implements RemoteCallable<Transitions> {
layer = zSplitLine + numChanges - i;
}
} else { // CHANGE or other
- layer = zSplitLine + numChanges - i;
+ if (isClosing || TransitionUtil.isOrderOnly(change)) {
+ // Put below CLOSE mode (in the "static" section).
+ layer = zSplitLine - i;
+ } else {
+ // Put above CLOSE mode.
+ layer = zSplitLine + numChanges - i;
+ }
}
t.setLayer(leash, layer);
}
}
- private int findActiveTransition(IBinder token) {
- for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
- if (mActiveTransitions.get(i).mToken == token) return i;
+ private static int findByToken(ArrayList<ActiveTransition> list, IBinder token) {
+ for (int i = list.size() - 1; i >= 0; --i) {
+ if (list.get(i).mToken == token) return i;
}
return -1;
}
+ /**
+ * Look through a transition and see if all non-closing changes are no-animation. If so, no
+ * animation should play.
+ */
+ static boolean isAllNoAnimation(TransitionInfo info) {
+ if (isClosingType(info.getType())) {
+ // no-animation is only relevant for launching (open) activities.
+ return false;
+ }
+ boolean hasNoAnimation = false;
+ final int changeSize = info.getChanges().size();
+ for (int i = changeSize - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (isClosingType(change.getMode())) {
+ // ignore closing apps since they are a side-effect of the transition and don't
+ // animate.
+ continue;
+ }
+ if (change.hasFlags(FLAG_NO_ANIMATION)) {
+ hasNoAnimation = true;
+ } else {
+ // at-least one relevant participant *is* animated, so we need to animate.
+ return false;
+ }
+ }
+ return hasNoAnimation;
+ }
+
+ /**
+ * Check if all changes in this transition are only ordering changes. If so, we won't animate.
+ */
+ static boolean isAllOrderOnly(TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ if (!TransitionUtil.isOrderOnly(info.getChanges().get(i))) return false;
+ }
+ return true;
+ }
+
+ private Track getOrCreateTrack(int trackId) {
+ while (trackId >= mTracks.size()) {
+ mTracks.add(new Track());
+ }
+ return mTracks.get(trackId);
+ }
+
@VisibleForTesting
void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
+ info.setUnreleasedWarningCallSiteForAllSurfaces("Transitions.onTransitionReady");
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s",
transitionToken, info);
- final int activeIdx = findActiveTransition(transitionToken);
+ final int activeIdx = findByToken(mPendingTransitions, transitionToken);
if (activeIdx < 0) {
- throw new IllegalStateException("Got transitionReady for non-active transition "
+ throw new IllegalStateException("Got transitionReady for non-pending transition "
+ transitionToken + ". expecting one of "
- + Arrays.toString(mActiveTransitions.stream().map(
+ + Arrays.toString(mPendingTransitions.stream().map(
activeTransition -> activeTransition.mToken).toArray()));
}
+ // Move from pending to ready
+ final ActiveTransition active = mPendingTransitions.remove(activeIdx);
+ active.mInfo = info;
+ active.mStartT = t;
+ active.mFinishT = finishT;
+ if (activeIdx > 0) {
+ Log.i(TAG, "Transition might be ready out-of-order " + activeIdx + " for " + active
+ + ". This is ok if it's on a different track.");
+ }
+ if (!mReadyDuringSync.isEmpty()) {
+ mReadyDuringSync.add(active);
+ } else {
+ dispatchReady(active);
+ }
+ }
+
+ /**
+ * Returns true if dispatching succeeded, otherwise false. Dispatching can fail if it is
+ * blocked by a sync or sleep.
+ */
+ boolean dispatchReady(ActiveTransition active) {
+ final TransitionInfo info = active.mInfo;
+
+ if (info.getType() == TRANSIT_SLEEP || active.isSync()) {
+ // Adding to *front*! If we are here, it means that it was pulled off the front
+ // so we are just putting it back; or, it is the first one so it doesn't matter.
+ mReadyDuringSync.add(0, active);
+ boolean hadPreceding = false;
+ // Now flush all the tracks.
+ for (int i = 0; i < mTracks.size(); ++i) {
+ final Track tr = mTracks.get(i);
+ if (tr.isIdle()) continue;
+ hadPreceding = true;
+ // Sleep starts a process of forcing all prior transitions to finish immediately
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Start finish-for-sync track %d", i);
+ finishForSync(active, i, null /* forceFinish */);
+ }
+ if (hadPreceding) {
+ return false;
+ }
+ // Actually able to process the sleep now, so re-remove it from the queue and continue
+ // the normal flow.
+ mReadyDuringSync.remove(active);
+ }
+
+ final Track track = getOrCreateTrack(info.getTrack());
+ track.mReadyTransitions.add(active);
for (int i = 0; i < mObservers.size(); ++i) {
- mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT);
+ mObservers.get(i).onTransitionReady(
+ active.mToken, info, active.mStartT, active.mFinishT);
}
- if (!info.getRootLeash().isValid()) {
- // Invalid root-leash implies that the transition is empty/no-op, so just do
+ /*
+ * Some transitions we always need to report to keyguard even if they are empty.
+ * TODO (b/274954192): Remove this once keyguard dispatching fully moves to Shell.
+ */
+ if (info.getRootCount() == 0 && !KeyguardTransitionHandler.handles(info)) {
+ // No root-leashes implies that the transition is empty/no-op, so just do
// housekeeping and return.
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Invalid root leash (%s): %s",
- transitionToken, info);
- t.apply();
- finishT.apply();
- onAbort(transitionToken);
- return;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots in %s so"
+ + " abort", active);
+ onAbort(active);
+ return true;
}
final int changeSize = info.getChanges().size();
@@ -493,44 +743,135 @@ public class Transitions implements RemoteCallable<Transitions> {
// changes are underneath another change.
|| ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT)
&& allOccluded)) {
- t.apply();
- finishT.apply();
// Treat this as an abort since we are bypassing any merge logic and effectively
// finishing immediately.
- onAbort(transitionToken);
- releaseSurfaces(info);
- return;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Non-visible anim so abort: %s", active);
+ onAbort(active);
+ return true;
}
- final ActiveTransition active = mActiveTransitions.get(activeIdx);
- active.mInfo = info;
- active.mStartT = t;
- active.mFinishT = finishT;
setupStartState(active.mInfo, active.mStartT, active.mFinishT);
- if (activeIdx > 0) {
- // This is now playing at the same time as an existing animation, so try merging it.
- attemptMergeTransition(mActiveTransitions.get(0), active);
- return;
+ if (track.mReadyTransitions.size() > 1) {
+ // There are already transitions waiting in the queue, so just return.
+ return true;
}
- // The normal case, just play it.
- playTransition(active);
+ processReadyQueue(track);
+ return true;
}
- /**
- * Attempt to merge by delegating the transition start to the handler of the currently
- * playing transition.
- */
- void attemptMergeTransition(@NonNull ActiveTransition playing,
- @NonNull ActiveTransition merging) {
+ private boolean areTracksIdle() {
+ for (int i = 0; i < mTracks.size(); ++i) {
+ if (!mTracks.get(i).isIdle()) return false;
+ }
+ return true;
+ }
+
+ private boolean isAnimating() {
+ return !mReadyDuringSync.isEmpty() || !areTracksIdle();
+ }
+
+ private boolean isIdle() {
+ return mPendingTransitions.isEmpty() && !isAnimating();
+ }
+
+ void processReadyQueue(Track track) {
+ if (track.mReadyTransitions.isEmpty()) {
+ if (track.mActiveTransition == null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Track %d became idle",
+ mTracks.indexOf(track));
+ if (areTracksIdle()) {
+ if (!mReadyDuringSync.isEmpty()) {
+ // Dispatch everything unless we hit another sync
+ while (!mReadyDuringSync.isEmpty()) {
+ ActiveTransition next = mReadyDuringSync.remove(0);
+ boolean success = dispatchReady(next);
+ // Hit a sync or sleep, so stop dispatching.
+ if (!success) break;
+ }
+ } else if (mPendingTransitions.isEmpty()) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition "
+ + "animations finished");
+ // Run all runnables from the run-when-idle queue.
+ for (int i = 0; i < mRunWhenIdleQueue.size(); i++) {
+ mRunWhenIdleQueue.get(i).run();
+ }
+ mRunWhenIdleQueue.clear();
+ }
+ }
+ }
+ return;
+ }
+ final ActiveTransition ready = track.mReadyTransitions.get(0);
+ if (track.mActiveTransition == null) {
+ // The normal case, just play it.
+ track.mReadyTransitions.remove(0);
+ track.mActiveTransition = ready;
+ if (ready.mAborted) {
+ if (ready.mStartT != null) {
+ ready.mStartT.apply();
+ }
+ // finish now since there's nothing to animate. Calls back into processReadyQueue
+ onFinish(ready, null, null);
+ return;
+ }
+ playTransition(ready);
+ // Attempt to merge any more queued-up transitions.
+ processReadyQueue(track);
+ return;
+ }
+ // An existing animation is playing, so see if we can merge.
+ final ActiveTransition playing = track.mActiveTransition;
+ if (ready.mAborted) {
+ // record as merged since it is no-op. Calls back into processReadyQueue
+ onMerged(playing, ready);
+ return;
+ }
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while"
- + " another transition %s is still animating. Notify the animating transition"
- + " in case they can be merged", merging.mToken, playing.mToken);
- playing.mHandler.mergeAnimation(merging.mToken, merging.mInfo, merging.mStartT,
- playing.mToken, (wct, cb) -> onFinish(merging.mToken, wct, cb));
+ + " %s is still animating. Notify the animating transition"
+ + " in case they can be merged", ready, playing);
+ mTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId());
+ playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT,
+ playing.mToken, (wct, cb) -> onMerged(playing, ready));
+ }
+
+ private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) {
+ if (playing.getTrack() != merged.getTrack()) {
+ throw new IllegalStateException("Can't merge across tracks: " + merged + " into "
+ + playing);
+ }
+ final Track track = mTracks.get(playing.getTrack());
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s",
+ merged, playing);
+ int readyIdx = 0;
+ if (track.mReadyTransitions.isEmpty() || track.mReadyTransitions.get(0) != merged) {
+ Log.e(TAG, "Merged transition out-of-order? " + merged);
+ readyIdx = track.mReadyTransitions.indexOf(merged);
+ if (readyIdx < 0) {
+ Log.e(TAG, "Merged a transition that is no-longer queued? " + merged);
+ return;
+ }
+ }
+ track.mReadyTransitions.remove(readyIdx);
+ if (playing.mMerged == null) {
+ playing.mMerged = new ArrayList<>();
+ }
+ playing.mMerged.add(merged);
+ // if it was aborted, then onConsumed has already been reported.
+ if (merged.mHandler != null && !merged.mAborted) {
+ merged.mHandler.onTransitionConsumed(merged.mToken, false /* abort */, merged.mFinishT);
+ }
+ for (int i = 0; i < mObservers.size(); ++i) {
+ mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken);
+ }
+ mTracer.logMerged(merged.mInfo.getDebugId(), playing.mInfo.getDebugId());
+ // See if we should merge another transition.
+ processReadyQueue(track);
}
private void playTransition(@NonNull ActiveTransition active) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active);
for (int i = 0; i < mObservers.size(); ++i) {
mObservers.get(i).onTransitionStarting(active.mToken);
}
@@ -542,15 +883,16 @@ public class Transitions implements RemoteCallable<Transitions> {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s",
active.mHandler);
boolean consumed = active.mHandler.startAnimation(active.mToken, active.mInfo,
- active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb));
+ active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active, wct, cb));
if (consumed) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler");
+ mTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler);
return;
}
}
// Otherwise give every other handler a chance
active.mHandler = dispatchTransition(active.mToken, active.mInfo, active.mStartT,
- active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb), active.mHandler);
+ active.mFinishT, (wct, cb) -> onFinish(active, wct, cb), active.mHandler);
}
/**
@@ -569,6 +911,7 @@ public class Transitions implements RemoteCallable<Transitions> {
if (consumed) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s",
mHandlers.get(i));
+ mTracer.logDispatched(info.getDebugId(), mHandlers.get(i));
return mHandlers.get(i);
}
}
@@ -580,27 +923,40 @@ public class Transitions implements RemoteCallable<Transitions> {
* Gives every handler (in order) a chance to handle request until one consumes the transition.
* @return the WindowContainerTransaction given by the handler which consumed the transition.
*/
- public WindowContainerTransaction dispatchRequest(@NonNull IBinder transition,
- @NonNull TransitionRequestInfo request, @Nullable TransitionHandler skip) {
+ public Pair<TransitionHandler, WindowContainerTransaction> dispatchRequest(
+ @NonNull IBinder transition, @NonNull TransitionRequestInfo request,
+ @Nullable TransitionHandler skip) {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
if (mHandlers.get(i) == skip) continue;
WindowContainerTransaction wct = mHandlers.get(i).handleRequest(transition, request);
if (wct != null) {
- return wct;
+ return new Pair<>(mHandlers.get(i), wct);
}
}
return null;
}
- /** Special version of finish just for dealing with no-op/invalid transitions. */
- private void onAbort(IBinder transition) {
- onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */);
- }
+ /** Aborts a transition. This will still queue it up to maintain order. */
+ private void onAbort(ActiveTransition transition) {
+ final Track track = mTracks.get(transition.getTrack());
+ transition.mAborted = true;
- private void onFinish(IBinder transition,
- @Nullable WindowContainerTransaction wct,
- @Nullable WindowContainerTransactionCallback wctCB) {
- onFinish(transition, wct, wctCB, false /* abort */);
+ mTracer.logAborted(transition.mInfo.getDebugId());
+
+ if (transition.mHandler != null) {
+ // Notifies to clean-up the aborted transition.
+ transition.mHandler.onTransitionConsumed(
+ transition.mToken, true /* aborted */, null /* finishTransaction */);
+ }
+
+ releaseSurfaces(transition.mInfo);
+
+ // This still went into the queue (to maintain the correct finish ordering).
+ if (track.mReadyTransitions.size() > 1) {
+ // There are already transitions waiting in the queue, so just return.
+ return;
+ }
+ processReadyQueue(track);
}
/**
@@ -612,193 +968,239 @@ public class Transitions implements RemoteCallable<Transitions> {
info.releaseAnimSurfaces();
}
- private void onFinish(IBinder transition,
+ private void onFinish(ActiveTransition active,
@Nullable WindowContainerTransaction wct,
- @Nullable WindowContainerTransactionCallback wctCB,
- boolean abort) {
- int activeIdx = findActiveTransition(transition);
- if (activeIdx < 0) {
+ @Nullable WindowContainerTransactionCallback wctCB) {
+ final Track track = mTracks.get(active.getTrack());
+ if (track.mActiveTransition != active) {
Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or "
- + " a handler didn't properly deal with a merge.", new RuntimeException());
- return;
- } else if (activeIdx > 0) {
- // This transition was merged.
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged (abort=%b:"
- + " %s", abort, transition);
- final ActiveTransition active = mActiveTransitions.get(activeIdx);
- active.mMerged = true;
- active.mAborted = abort;
- if (active.mHandler != null) {
- active.mHandler.onTransitionConsumed(
- active.mToken, abort, abort ? null : active.mFinishT);
- }
- for (int i = 0; i < mObservers.size(); ++i) {
- mObservers.get(i).onTransitionMerged(
- active.mToken, mActiveTransitions.get(0).mToken);
- }
+ + " a handler didn't properly deal with a merge. " + active,
+ new RuntimeException());
return;
}
- final ActiveTransition active = mActiveTransitions.get(activeIdx);
- active.mAborted = abort;
- if (active.mAborted && active.mHandler != null) {
- // Notifies to clean-up the aborted transition.
- active.mHandler.onTransitionConsumed(
- transition, true /* aborted */, null /* finishTransaction */);
- }
+ track.mActiveTransition = null;
+
for (int i = 0; i < mObservers.size(); ++i) {
mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted);
}
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Transition animation finished (abort=%b), notifying core %s", abort, transition);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animation finished "
+ + "(aborted=%b), notifying core %s", active.mAborted, active);
if (active.mStartT != null) {
- // Applied by now, so close immediately. Do not set to null yet, though, since nullness
- // is used later to disambiguate malformed transitions.
- active.mStartT.close();
+ // Applied by now, so clear immediately to remove any references. Do not set to null
+ // yet, though, since nullness is used later to disambiguate malformed transitions.
+ active.mStartT.clear();
}
- // Merge all relevant transactions together
+ // Merge all associated transactions together
SurfaceControl.Transaction fullFinish = active.mFinishT;
- for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) {
- final ActiveTransition toMerge = mActiveTransitions.get(iA);
- if (!toMerge.mMerged) break;
- // aborted transitions have no start/finish transactions
- if (mActiveTransitions.get(iA).mStartT == null) break;
- if (fullFinish == null) {
- fullFinish = new SurfaceControl.Transaction();
+ if (active.mMerged != null) {
+ for (int iM = 0; iM < active.mMerged.size(); ++iM) {
+ final ActiveTransition toMerge = active.mMerged.get(iM);
+ // Include start. It will be a no-op if it was already applied. Otherwise, we need
+ // it to maintain consistent state.
+ if (toMerge.mStartT != null) {
+ if (fullFinish == null) {
+ fullFinish = toMerge.mStartT;
+ } else {
+ fullFinish.merge(toMerge.mStartT);
+ }
+ }
+ if (toMerge.mFinishT != null) {
+ if (fullFinish == null) {
+ fullFinish = toMerge.mFinishT;
+ } else {
+ fullFinish.merge(toMerge.mFinishT);
+ }
+ }
}
- // Include start. It will be a no-op if it was already applied. Otherwise, we need it
- // to maintain consistent state.
- fullFinish.merge(mActiveTransitions.get(iA).mStartT);
- fullFinish.merge(mActiveTransitions.get(iA).mFinishT);
}
if (fullFinish != null) {
fullFinish.apply();
}
- // Now perform all the finishes.
+ // Now perform all the finish callbacks (starting with the playing one and then all the
+ // transitions merged into it).
releaseSurfaces(active.mInfo);
- mActiveTransitions.remove(activeIdx);
- mOrganizer.finishTransition(transition, wct, wctCB);
- while (activeIdx < mActiveTransitions.size()) {
- if (!mActiveTransitions.get(activeIdx).mMerged) break;
- ActiveTransition merged = mActiveTransitions.remove(activeIdx);
- mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */);
- releaseSurfaces(merged.mInfo);
- }
- // sift through aborted transitions
- while (mActiveTransitions.size() > activeIdx
- && mActiveTransitions.get(activeIdx).mAborted) {
- ActiveTransition aborted = mActiveTransitions.remove(activeIdx);
- // Notifies to clean-up the aborted transition.
- if (aborted.mHandler != null) {
- aborted.mHandler.onTransitionConsumed(
- transition, true /* aborted */, null /* finishTransaction */);
+ mOrganizer.finishTransition(active.mToken, wct, wctCB);
+ if (active.mMerged != null) {
+ for (int iM = 0; iM < active.mMerged.size(); ++iM) {
+ ActiveTransition merged = active.mMerged.get(iM);
+ mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */);
+ releaseSurfaces(merged.mInfo);
}
- mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */);
- for (int i = 0; i < mObservers.size(); ++i) {
- mObservers.get(i).onTransitionFinished(aborted.mToken, true);
- }
- releaseSurfaces(aborted.mInfo);
- }
- if (mActiveTransitions.size() <= activeIdx) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations "
- + "finished");
- // Run all runnables from the run-when-idle queue.
- for (int i = 0; i < mRunWhenIdleQueue.size(); i++) {
- mRunWhenIdleQueue.get(i).run();
- }
- mRunWhenIdleQueue.clear();
- return;
+ active.mMerged.clear();
}
- // Start animating the next active transition
- final ActiveTransition next = mActiveTransitions.get(activeIdx);
- if (next.mInfo == null) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transition after one"
- + " finished, but it isn't ready yet.");
- return;
+
+ // Now that this is done, check the ready queue for more work.
+ processReadyQueue(track);
+ }
+
+ private boolean isTransitionKnown(IBinder token) {
+ for (int i = 0; i < mPendingTransitions.size(); ++i) {
+ if (mPendingTransitions.get(i).mToken == token) return true;
}
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Pending transitions after one"
- + " finished, so start the next one.");
- playTransition(next);
- // Now try to merge the rest of the transitions (re-acquire activeIdx since next may have
- // finished immediately)
- activeIdx = findActiveTransition(next.mToken);
- if (activeIdx < 0) {
- // This means 'next' finished immediately and thus re-entered this function. Since
- // that is the case, just return here since all relevant logic has already run in the
- // re-entered call.
- return;
+ for (int i = 0; i < mReadyDuringSync.size(); ++i) {
+ if (mReadyDuringSync.get(i).mToken == token) return true;
}
-
- // This logic is also convoluted because 'next' may finish immediately in response to any of
- // the merge requests (eg. if it decided to "cancel" itself).
- int mergeIdx = activeIdx + 1;
- while (mergeIdx < mActiveTransitions.size()) {
- ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx);
- if (mergeCandidate.mAborted) {
- // transition was aborted, so we can skip for now (still leave it in the list
- // so that it gets cleaned-up in the right order).
- ++mergeIdx;
- continue;
- }
- if (mergeCandidate.mMerged) {
- throw new IllegalStateException("Can't merge a transition after not-merging"
- + " a preceding one.");
+ 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;
}
- attemptMergeTransition(next, mergeCandidate);
- mergeIdx = findActiveTransition(mergeCandidate.mToken);
- if (mergeIdx < 0) {
- // This means 'next' finished immediately and thus re-entered this function. Since
- // that is the case, just return here since all relevant logic has already run in
- // the re-entered call.
- return;
+ final ActiveTransition active = tr.mActiveTransition;
+ if (active == null) continue;
+ if (active.mToken == token) return true;
+ if (active.mMerged == null) continue;
+ for (int m = 0; m < active.mMerged.size(); ++m) {
+ if (active.mMerged.get(m).mToken == token) return true;
}
- ++mergeIdx;
}
+ return false;
}
void requestStartTransition(@NonNull IBinder transitionToken,
@Nullable TransitionRequestInfo request) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: %s %s",
transitionToken, request);
- if (findActiveTransition(transitionToken) >= 0) {
+ if (isTransitionKnown(transitionToken)) {
throw new RuntimeException("Transition already started " + transitionToken);
}
final ActiveTransition active = new ActiveTransition();
WindowContainerTransaction wct = null;
- for (int i = mHandlers.size() - 1; i >= 0; --i) {
- wct = mHandlers.get(i).handleRequest(transitionToken, request);
- if (wct != null) {
- active.mHandler = mHandlers.get(i);
- break;
+
+ // If we have sleep, we use a special handler and we try to finish everything ASAP.
+ if (request.getType() == TRANSIT_SLEEP) {
+ mSleepHandler.handleRequest(transitionToken, request);
+ active.mHandler = mSleepHandler;
+ } else {
+ for (int i = mHandlers.size() - 1; i >= 0; --i) {
+ wct = mHandlers.get(i).handleRequest(transitionToken, request);
+ if (wct != null) {
+ active.mHandler = mHandlers.get(i);
+ break;
+ }
}
- }
- if (request.getDisplayChange() != null) {
- TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
- if (change.getEndRotation() != change.getStartRotation()) {
- // Is a rotation, so dispatch to all displayChange listeners
- if (wct == null) {
- wct = new WindowContainerTransaction();
+ if (request.getDisplayChange() != null) {
+ TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
+ if (change.getEndRotation() != change.getStartRotation()) {
+ // Is a rotation, so dispatch to all displayChange listeners
+ if (wct == null) {
+ wct = new WindowContainerTransaction();
+ }
+ mDisplayController.onDisplayRotateRequested(wct, change.getDisplayId(),
+ change.getStartRotation(), change.getEndRotation());
}
- mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
- change.getDisplayId(), change.getStartRotation(), change.getEndRotation(),
- null /* newDisplayAreaInfo */);
}
}
+ if (request.getType() == TRANSIT_KEYGUARD_OCCLUDE && request.getTriggerTask() != null
+ && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ // This freeform task is on top of keyguard, so its windowing mode should be changed to
+ // fullscreen.
+ if (wct == null) {
+ wct = new WindowContainerTransaction();
+ }
+ wct.setWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_FULLSCREEN);
+ wct.setBounds(request.getTriggerTask().token, null);
+ }
mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
active.mToken = transitionToken;
- mActiveTransitions.add(active);
+ // Currently, WMCore only does one transition at a time. If it makes a requestStart, it
+ // is already collecting that transition on core-side, so it will be the next one to
+ // become ready. There may already be pending transitions added as part of direct
+ // `startNewTransition` but if we have a request now, it means WM created the request
+ // transition before it acknowledged any of the pending `startNew` transitions. So, insert
+ // it at the front.
+ mPendingTransitions.add(0, active);
}
/** Start a new transition directly. */
public IBinder startTransition(@WindowManager.TransitionType int type,
@NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition "
+ + "type=%d wct=%s handler=%s", type, wct, handler);
final ActiveTransition active = new ActiveTransition();
active.mHandler = handler;
active.mToken = mOrganizer.startNewTransition(type, wct);
- mActiveTransitions.add(active);
+ mPendingTransitions.add(active);
return active.mToken;
}
/**
+ * Finish running animations (almost) immediately when a SLEEP transition comes in. We use this
+ * as both a way to reduce unnecessary work (animations not visible while screen off) and as a
+ * failsafe to unblock "stuck" animations (in particular remote animations).
+ *
+ * This works by "merging" the sleep transition into the currently-playing transition (even if
+ * its out-of-order) -- turning SLEEP into a signal. If the playing transition doesn't finish
+ * within `SYNC_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and
+ * send an abort/consumed message).
+ *
+ * This is then repeated until there are no more pending sleep transitions.
+ *
+ * @param reason The SLEEP transition that triggered this round of finishes. We will continue
+ * looping round finishing transitions as long as this is still waiting.
+ * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge
+ * signal to -- so it will be force-finished if it's still running.
+ */
+ private void finishForSync(ActiveTransition reason,
+ int trackIdx, @Nullable ActiveTransition forceFinish) {
+ if (!isTransitionKnown(reason.mToken)) {
+ Log.d(TAG, "finishForSleep: already played sync transition " + reason);
+ return;
+ }
+ final Track track = mTracks.get(trackIdx);
+ if (forceFinish != null) {
+ final Track trk = mTracks.get(forceFinish.getTrack());
+ if (trk != track) {
+ Log.e(TAG, "finishForSleep: mismatched Tracks between forceFinish and logic "
+ + forceFinish.getTrack() + " vs " + trackIdx);
+ }
+ if (trk.mActiveTransition == forceFinish) {
+ Log.e(TAG, "Forcing transition to finish due to sync timeout: " + forceFinish);
+ forceFinish.mAborted = true;
+ // Last notify of it being consumed. Note: mHandler should never be null,
+ // but check just to be safe.
+ if (forceFinish.mHandler != null) {
+ forceFinish.mHandler.onTransitionConsumed(
+ forceFinish.mToken, true /* aborted */, null /* finishTransaction */);
+ }
+ onFinish(forceFinish, null, null);
+ }
+ }
+ if (track.isIdle() || mReadyDuringSync.isEmpty()) {
+ // Done finishing things.
+ return;
+ }
+ final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction();
+ final TransitionInfo dummyInfo = new TransitionInfo(TRANSIT_SLEEP, 0 /* flags */);
+ while (track.mActiveTransition != null && !mReadyDuringSync.isEmpty()) {
+ final ActiveTransition playing = track.mActiveTransition;
+ final ActiveTransition nextSync = mReadyDuringSync.get(0);
+ if (!nextSync.isSync()) {
+ Log.e(TAG, "Somehow blocked on a non-sync transition? " + nextSync);
+ }
+ // Attempt to merge a SLEEP info to signal that the playing transition needs to
+ // fast-forward.
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge sync %s"
+ + " into %s via a SLEEP proxy", nextSync, playing);
+ playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT,
+ playing.mToken, (wct, cb) -> {});
+ // it's possible to complete immediately. If that happens, just repeat the signal
+ // loop until we either finish everything or start playing an animation that isn't
+ // finishing immediately.
+ if (track.mActiveTransition == playing) {
+ if (!mDisableForceSync) {
+ // Give it a short amount of time to process it before forcing.
+ final int tolerance = KeyguardTransitionHandler.handles(playing.mInfo)
+ ? SYNC_ALLOWANCE_KEYGUARD_MS
+ : SYNC_ALLOWANCE_MS;
+ mMainExecutor.executeDelayed(
+ () -> finishForSync(reason, trackIdx, playing), tolerance);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
* Interface for a callback that must be called after a TransitionHandler finishes playing an
* animation.
*/
@@ -1016,6 +1418,11 @@ public class Transitions implements RemoteCallable<Transitions> {
transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition);
});
}
+
+ @Override
+ public IBinder getShellApplyToken() {
+ return SurfaceControl.Transaction.getDefaultApplyToken();
+ }
}
private class SettingsObserver extends ContentObserver {
@@ -1032,4 +1439,26 @@ public class Transitions implements RemoteCallable<Transitions> {
mMainExecutor.execute(() -> dispatchAnimScaleSetting(mTransitionAnimationScaleSetting));
}
}
+
+
+ @Override
+ public boolean onShellCommand(String[] args, PrintWriter pw) {
+ switch (args[0]) {
+ case "tracing": {
+ mTracer.onShellCommand(Arrays.copyOfRange(args, 1, args.length), pw);
+ return true;
+ }
+ default: {
+ pw.println("Invalid command: " + args[0]);
+ printShellCommandHelp(pw, "");
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void printShellCommandHelp(PrintWriter pw, String prefix) {
+ pw.println(prefix + "tracing");
+ mTracer.printShellCommandHelp(pw, prefix + " ");
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
index fe0a3fb7b9dc..9d4e6b475c53 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
@@ -19,14 +19,13 @@ package com.android.wm.shell.unfold;
import static android.graphics.Color.blue;
import static android.graphics.Color.green;
import static android.graphics.Color.red;
-import static android.view.Display.DEFAULT_DISPLAY;
+import android.annotation.ColorRes;
import android.annotation.NonNull;
import android.content.Context;
import android.view.SurfaceControl;
import com.android.wm.shell.R;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
/**
* Controls background color layer for the unfold animations
@@ -34,16 +33,15 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
public class UnfoldBackgroundController {
private static final int BACKGROUND_LAYER_Z_INDEX = -1;
-
- private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final float[] mBackgroundColor;
+ private final float[] mSplitScreenBackgroundColor;
+ private float[] mBackgroundColorSet;
private SurfaceControl mBackgroundLayer;
+ private boolean mSplitScreenVisible = false;
- public UnfoldBackgroundController(
- @NonNull Context context,
- @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
- mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
- mBackgroundColor = getBackgroundColor(context);
+ public UnfoldBackgroundController(@NonNull Context context) {
+ mBackgroundColor = getRGBColorFromId(context, R.color.unfold_background);
+ mSplitScreenBackgroundColor = getRGBColorFromId(context, R.color.split_divider_background);
}
/**
@@ -51,19 +49,26 @@ public class UnfoldBackgroundController {
* @param transaction where we should add the background if it is not added
*/
public void ensureBackground(@NonNull SurfaceControl.Transaction transaction) {
- if (mBackgroundLayer != null) return;
+ float[] expectedColor = getCurrentBackgroundColor();
+ if (mBackgroundLayer != null) {
+ if (mBackgroundColorSet != expectedColor) {
+ transaction.setColor(mBackgroundLayer, expectedColor);
+ mBackgroundColorSet = expectedColor;
+ }
+ return;
+ }
SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
.setName("app-unfold-background")
.setCallsite("AppUnfoldTransitionController")
.setColorLayer();
- mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder);
mBackgroundLayer = colorLayerBuilder.build();
transaction
- .setColor(mBackgroundLayer, mBackgroundColor)
+ .setColor(mBackgroundLayer, expectedColor)
.show(mBackgroundLayer)
.setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX);
+ mBackgroundColorSet = expectedColor;
}
/**
@@ -78,8 +83,25 @@ public class UnfoldBackgroundController {
mBackgroundLayer = null;
}
- private float[] getBackgroundColor(Context context) {
- int colorInt = context.getResources().getColor(R.color.unfold_background);
+ /**
+ * Expected to be called whenever split screen visibility changes.
+ *
+ * @param visible True when split screen is visible
+ */
+ public void onSplitVisibilityChanged(boolean visible) {
+ mSplitScreenVisible = visible;
+ }
+
+ private float[] getCurrentBackgroundColor() {
+ if (mSplitScreenVisible) {
+ return mSplitScreenBackgroundColor;
+ } else {
+ return mBackgroundColor;
+ }
+ }
+
+ private float[] getRGBColorFromId(Context context, @ColorRes int id) {
+ int colorInt = context.getResources().getColor(id);
return new float[]{
(float) red(colorInt) / 255.0F,
(float) green(colorInt) / 255.0F,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index 5d7b62905d3b..f148412205bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -18,6 +18,9 @@ package com.android.wm.shell.unfold;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS;
+
+import android.app.ActivityManager;
import android.os.IBinder;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
@@ -27,6 +30,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
@@ -36,6 +40,7 @@ import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
+import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.List;
@@ -105,8 +110,14 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
animator.clearTasks();
info.getChanges().forEach(change -> {
- if (change.getTaskInfo() != null
- && change.getMode() == TRANSIT_CHANGE
+ if (change.getTaskInfo() != null) {
+ ProtoLog.v(WM_SHELL_TRANSITIONS,
+ "startAnimation, check taskInfo: %s, mode: %s, isApplicableTask: %s",
+ change.getTaskInfo(), TransitionInfo.modeToString(change.getMode()),
+ animator.isApplicableTask(change.getTaskInfo()));
+ }
+ if (change.getTaskInfo() != null && (change.getMode() == TRANSIT_CHANGE
+ || TransitionUtil.isOpeningType(change.getMode()))
&& animator.isApplicableTask(change.getTaskInfo())) {
animator.onTaskAppeared(change.getTaskInfo(), change.getLeash());
}
@@ -163,12 +174,41 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
mTransition = null;
}
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull TransitionFinishCallback finishCallback) {
+ if (info.getType() == TRANSIT_CHANGE) {
+ // TODO (b/286928742) unfold transition handler should be part of mixed handler to
+ // handle merges better.
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo != null
+ && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) {
+ // Tasks that are always on top (e.g. bubbles), will handle their own transition
+ // as they are on top of everything else. So skip merging transitions here.
+ return;
+ }
+ }
+ // Apply changes happening during the unfold animation immediately
+ t.apply();
+ finishCallback.onTransitionFinished(null, null);
+ }
+ }
+
+ /** Whether `request` contains an unfold action. */
+ public boolean hasUnfold(@NonNull TransitionRequestInfo request) {
+ return (request.getType() == TRANSIT_CHANGE
+ && request.getDisplayChange() != null
+ && request.getDisplayChange().isPhysicalDisplayChanged());
+ }
+
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
- if (request.getType() == TRANSIT_CHANGE && request.getDisplayChange() != null
- && request.getDisplayChange().isPhysicalDisplayChanged()) {
+ if (hasUnfold(request)) {
mTransition = transition;
return new WindowContainerTransaction();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
index e0f3fcd932c2..f81fc6fbea49 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
@@ -35,6 +35,7 @@ import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
+import android.view.WindowInsets;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -66,13 +67,12 @@ public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2;
private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
- private final int mExpandedTaskBarHeight;
private final DisplayInsetsController mDisplayInsetsController;
private final UnfoldBackgroundController mBackgroundController;
private final Context mContext;
private final ShellController mShellController;
- private InsetsSource mTaskbarInsetsSource;
+ private InsetsSource mExpandedTaskbarInsetsSource;
private float mWindowCornerRadiusPx;
public FullscreenUnfoldTaskAnimator(Context context,
@@ -82,8 +82,6 @@ public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
mDisplayInsetsController = displayInsetsController;
mBackgroundController = backgroundController;
mShellController = shellController;
- mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.taskbar_frame_height);
mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
}
@@ -101,21 +99,32 @@ public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
@Override
public void insetsChanged(InsetsState insetsState) {
- mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ mExpandedTaskbarInsetsSource = getExpandedTaskbarSource(insetsState);
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
AnimationContext context = mAnimationContextByTaskId.valueAt(i);
- context.update(mTaskbarInsetsSource, context.mTaskInfo);
+ context.update(mExpandedTaskbarInsetsSource, context.mTaskInfo);
}
}
+ private static InsetsSource getExpandedTaskbarSource(InsetsState state) {
+ for (int i = state.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = state.sourceAt(i);
+ if (source.getType() == WindowInsets.Type.navigationBars()
+ && source.insetsRoundedCornerFrame()) {
+ return source;
+ }
+ }
+ return null;
+ }
+
public boolean hasActiveTasks() {
return mAnimationContextByTaskId.size() > 0;
}
@Override
public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {
- AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource,
- taskInfo);
+ AnimationContext animationContext = new AnimationContext(
+ leash, mExpandedTaskbarInsetsSource, taskInfo);
mAnimationContextByTaskId.put(taskInfo.taskId, animationContext);
}
@@ -123,7 +132,7 @@ public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
public void onTaskChanged(TaskInfo taskInfo) {
AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId);
if (animationContext != null) {
- animationContext.update(mTaskbarInsetsSource, taskInfo);
+ animationContext.update(mExpandedTaskbarInsetsSource, taskInfo);
}
}
@@ -222,11 +231,7 @@ public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
mStartCropRect.set(mTaskInfo.getConfiguration().windowConfiguration.getBounds());
if (taskBarInsetsSource != null) {
- // Only insets the cropping window with task bar when it's expanded
- if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
- mStartCropRect.inset(taskBarInsetsSource
- .calculateVisibleInsets(mStartCropRect));
- }
+ mStartCropRect.inset(taskBarInsetsSource.calculateVisibleInsets(mStartCropRect));
}
mEndCropRect.set(mStartCropRect);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
index addd0a6012c4..a4cf149cc3b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
@@ -37,6 +37,7 @@ import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
+import android.view.WindowInsets;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -76,7 +77,6 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
private final Executor mExecutor;
private final DisplayInsetsController mDisplayInsetsController;
private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
- private final int mExpandedTaskBarHeight;
private final ShellController mShellController;
private final Lazy<Optional<SplitScreenController>> mSplitScreenController;
private final UnfoldBackgroundController mUnfoldBackgroundController;
@@ -86,7 +86,7 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
private final Rect mRootStageBounds = new Rect();
private float mWindowCornerRadiusPx;
- private InsetsSource mTaskbarInsetsSource;
+ private InsetsSource mExpandedTaskbarInsetsSource;
@SplitPosition
private int mMainStagePosition = SPLIT_POSITION_UNDEFINED;
@@ -103,8 +103,6 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
mShellController = shellController;
mUnfoldBackgroundController = unfoldBackgroundController;
mSplitScreenController = splitScreenController;
- mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.taskbar_frame_height);
mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
}
@@ -143,10 +141,21 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
@Override
public void insetsChanged(InsetsState insetsState) {
- mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ mExpandedTaskbarInsetsSource = getExpandedTaskbarSource(insetsState);
updateContexts();
}
+ private static InsetsSource getExpandedTaskbarSource(InsetsState state) {
+ for (int i = state.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = state.sourceAt(i);
+ if (source.getType() == WindowInsets.Type.navigationBars()
+ && source.insetsRoundedCornerFrame()) {
+ return source;
+ }
+ }
+ return null;
+ }
+
@Override
public void onTaskStageChanged(int taskId, int stage, boolean visible) {
final AnimationContext context = mAnimationContextByTaskId.get(taskId);
@@ -204,7 +213,7 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
@Override
public boolean isApplicableTask(TaskInfo taskInfo) {
return taskInfo.hasParentTask()
- && taskInfo.isVisible
+ && taskInfo.isRunning
&& taskInfo.realActivity != null // to filter out parents created by organizer
&& taskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW;
}
@@ -283,6 +292,11 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
.setCornerRadius(context.mLeash, 0.0F);
}
+ @Override
+ public void onSplitVisibilityChanged(boolean visible) {
+ mUnfoldBackgroundController.onSplitVisibilityChanged(visible);
+ }
+
private class AnimationContext {
final SurfaceControl mLeash;
@@ -307,7 +321,8 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
boolean taskbarExpanded = isTaskbarExpanded();
if (taskbarExpanded) {
// Only insets the cropping window with taskbar when taskbar is expanded
- mStartCropRect.inset(mTaskbarInsetsSource.calculateVisibleInsets(mStartCropRect));
+ mStartCropRect.inset(mExpandedTaskbarInsetsSource.calculateVisibleInsets(
+ mStartCropRect));
}
// Offset to surface coordinates as layout bounds are in screen coordinates
@@ -360,8 +375,7 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
}
private boolean isTaskbarExpanded() {
- return mTaskbarInsetsSource != null
- && mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight;
+ return mExpandedTaskbarInsetsSource != null;
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
new file mode 100644
index 000000000000..9b48a542720c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.util
+
+import android.util.Log
+import com.android.internal.protolog.common.IProtoLogGroup
+import com.android.wm.shell.protolog.ShellProtoLogImpl
+
+/**
+ * Log messages using an API similar to [com.android.internal.protolog.common.ProtoLog]. Useful for
+ * logging from Kotlin classes as ProtoLog does not have support for Kotlin.
+ *
+ * All messages are logged to logcat if logging is enabled for that [IProtoLogGroup].
+ */
+// TODO(b/168581922): remove once ProtoLog adds support for Kotlin
+class KtProtoLog {
+ companion object {
+ /** @see [com.android.internal.protolog.common.ProtoLog.d] */
+ fun d(group: IProtoLogGroup, messageString: String, vararg args: Any) {
+ if (ShellProtoLogImpl.isEnabled(group)) {
+ Log.d(group.tag, String.format(messageString, *args))
+ }
+ }
+
+ /** @see [com.android.internal.protolog.common.ProtoLog.v] */
+ fun v(group: IProtoLogGroup, messageString: String, vararg args: Any) {
+ if (ShellProtoLogImpl.isEnabled(group)) {
+ Log.v(group.tag, String.format(messageString, *args))
+ }
+ }
+
+ /** @see [com.android.internal.protolog.common.ProtoLog.i] */
+ fun i(group: IProtoLogGroup, messageString: String, vararg args: Any) {
+ if (ShellProtoLogImpl.isEnabled(group)) {
+ Log.i(group.tag, String.format(messageString, *args))
+ }
+ }
+
+ /** @see [com.android.internal.protolog.common.ProtoLog.w] */
+ fun w(group: IProtoLogGroup, messageString: String, vararg args: Any) {
+ if (ShellProtoLogImpl.isEnabled(group)) {
+ Log.w(group.tag, String.format(messageString, *args))
+ }
+ }
+
+ /** @see [com.android.internal.protolog.common.ProtoLog.e] */
+ fun e(group: IProtoLogGroup, messageString: String, vararg args: Any) {
+ if (ShellProtoLogImpl.isEnabled(group)) {
+ Log.e(group.tag, String.format(messageString, *args))
+ }
+ }
+
+ /** @see [com.android.internal.protolog.common.ProtoLog.wtf] */
+ fun wtf(group: IProtoLogGroup, messageString: String, vararg args: Any) {
+ if (ShellProtoLogImpl.isEnabled(group)) {
+ Log.wtf(group.tag, String.format(messageString, *args))
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
new file mode 100644
index 000000000000..936faa3ee6bf
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
@@ -0,0 +1,364 @@
+/*
+ * 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.util;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
+import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.util.SparseBooleanArray;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+
+import java.util.function.Predicate;
+
+/** Various utility functions for transitions. */
+public class TransitionUtil {
+
+ /** @return true if the transition was triggered by opening something vs closing something */
+ public static boolean isOpeningType(@WindowManager.TransitionType int type) {
+ return type == TRANSIT_OPEN
+ || type == TRANSIT_TO_FRONT
+ || type == TRANSIT_KEYGUARD_GOING_AWAY;
+ }
+
+ /** @return true if the transition was triggered by closing something vs opening something */
+ public static boolean isClosingType(@WindowManager.TransitionType int type) {
+ return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
+ }
+
+ /** Returns {@code true} if the transition is opening or closing mode. */
+ public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) {
+ return mode == TRANSIT_OPEN || mode == TRANSIT_CLOSE
+ || mode == TRANSIT_TO_FRONT || mode == TRANSIT_TO_BACK;
+ }
+
+ /** Returns {@code true} if the transition has a display change. */
+ public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns `true` if `change` is a wallpaper. */
+ public static boolean isWallpaper(TransitionInfo.Change change) {
+ return (change.getTaskInfo() == null)
+ && change.hasFlags(FLAG_IS_WALLPAPER)
+ && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
+ }
+
+ /** Returns `true` if `change` is not an app window or wallpaper. */
+ public static boolean isNonApp(TransitionInfo.Change change) {
+ return (change.getTaskInfo() == null)
+ && !change.hasFlags(FLAG_IS_WALLPAPER)
+ && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
+ }
+
+ /** Returns `true` if `change` is the divider. */
+ public static boolean isDividerBar(TransitionInfo.Change change) {
+ return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
+ }
+
+ /** Returns `true` if `change` is only re-ordering. */
+ public static boolean isOrderOnly(TransitionInfo.Change change) {
+ return change.getMode() == TRANSIT_CHANGE
+ && (change.getFlags() & FLAG_MOVED_TO_TOP) != 0
+ && change.getStartAbsBounds().equals(change.getEndAbsBounds())
+ && (change.getLastParent() == null
+ || change.getLastParent().equals(change.getParent()));
+ }
+
+ /**
+ * Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you
+ * MUST call `test` in the same order that the changes appear in the TransitionInfo.
+ */
+ public static class LeafTaskFilter implements Predicate<TransitionInfo.Change> {
+ private final SparseBooleanArray mChildTaskTargets = new SparseBooleanArray();
+
+ @Override
+ public boolean test(TransitionInfo.Change change) {
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null) return false;
+ // Children always come before parent since changes are in top-to-bottom z-order.
+ boolean hasChildren = mChildTaskTargets.get(taskInfo.taskId);
+ if (taskInfo.hasParentTask()) {
+ mChildTaskTargets.put(taskInfo.parentTaskId, true);
+ }
+ // If it has children, it's not a leaf.
+ return !hasChildren;
+ }
+ }
+
+
+ private static int newModeToLegacyMode(int newMode) {
+ switch (newMode) {
+ case WindowManager.TRANSIT_OPEN:
+ case WindowManager.TRANSIT_TO_FRONT:
+ return MODE_OPENING;
+ case WindowManager.TRANSIT_CLOSE:
+ case WindowManager.TRANSIT_TO_BACK:
+ return MODE_CLOSING;
+ default:
+ return MODE_CHANGING;
+ }
+ }
+
+ /**
+ * Very similar to Transitions#setupAnimHierarchy but specialized for leashes.
+ */
+ @SuppressLint("NewApi")
+ private static void setupLeash(@NonNull SurfaceControl leash,
+ @NonNull TransitionInfo.Change change, int layer,
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+ final boolean isOpening = TransitionUtil.isOpeningType(info.getType());
+ // Put animating stuff above this line and put static stuff below it.
+ int zSplitLine = info.getChanges().size();
+ // changes should be ordered top-to-bottom in z
+ final int mode = change.getMode();
+
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
+ t.reparent(leash, info.getRoot(rootIdx).getLeash());
+ final Rect absBounds =
+ (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
+ t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x,
+ absBounds.top - info.getRoot(rootIdx).getOffset().y);
+
+ if (isDividerBar(change)) {
+ if (isOpeningType(mode)) {
+ t.setAlpha(leash, 0.f);
+ }
+ // Set the transition leash position to 0 in case the divider leash position being
+ // taking down.
+ t.setPosition(leash, 0, 0);
+ t.setLayer(leash, Integer.MAX_VALUE);
+ return;
+ }
+
+ // Put all the OPEN/SHOW on top
+ if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+ // Wallpaper is always at the bottom, opening wallpaper on top of closing one.
+ if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) {
+ t.setLayer(leash, -zSplitLine + info.getChanges().size() - layer);
+ } else {
+ t.setLayer(leash, -zSplitLine - layer);
+ }
+ } else if (TransitionUtil.isOpeningType(mode)) {
+ if (isOpening) {
+ t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
+ if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
+ // if transferred, it should be left visible.
+ t.setAlpha(leash, 0.f);
+ }
+ } else {
+ // put on bottom and leave it visible
+ t.setLayer(leash, zSplitLine - layer);
+ }
+ } else if (TransitionUtil.isClosingType(mode)) {
+ if (isOpening) {
+ // put on bottom and leave visible
+ t.setLayer(leash, zSplitLine - layer);
+ } else {
+ // put on top
+ t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
+ }
+ } else { // CHANGE
+ t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private static SurfaceControl createLeash(TransitionInfo info, TransitionInfo.Change change,
+ int order, SurfaceControl.Transaction t) {
+ // TODO: once we can properly sync transactions across process, then get rid of this leash.
+ if (change.getParent() != null && (change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+ // Special case for wallpaper atm. Normally these are left alone; but, a quirk of
+ // making leashes means we have to handle them specially.
+ return change.getLeash();
+ }
+ final int rootIdx = TransitionUtil.rootIndexFor(change, info);
+ SurfaceControl leashSurface = new SurfaceControl.Builder()
+ .setName(change.getLeash().toString() + "_transition-leash")
+ .setContainerLayer()
+ // Initial the surface visible to respect the visibility of the original surface.
+ .setHidden(false)
+ .setParent(info.getRoot(rootIdx).getLeash())
+ .build();
+ // Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
+ setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
+ t.reparent(change.getLeash(), leashSurface);
+ t.setAlpha(change.getLeash(), 1.0f);
+ t.show(change.getLeash());
+ if (!isDividerBar(change)) {
+ // For divider, don't modify its inner leash position when creating the outer leash
+ // for the transition. In case the position being wrong after the transition finished.
+ t.setPosition(change.getLeash(), 0, 0);
+ }
+ t.setLayer(change.getLeash(), 0);
+ return leashSurface;
+ }
+
+ /**
+ * Creates a new RemoteAnimationTarget from the provided change info
+ */
+ public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+ TransitionInfo info, SurfaceControl.Transaction t,
+ @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+ return newTarget(change, order, false /* forceTranslucent */, info, t, leashMap);
+ }
+
+ /**
+ * Creates a new RemoteAnimationTarget from the provided change info
+ */
+ public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+ boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t,
+ @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+ final SurfaceControl leash = createLeash(info, change, order, t);
+ if (leashMap != null) {
+ leashMap.put(change.getLeash(), leash);
+ }
+ return newTarget(change, order, leash, forceTranslucent);
+ }
+
+ /**
+ * Creates a new RemoteAnimationTarget from the provided change and leash
+ */
+ public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+ SurfaceControl leash) {
+ return newTarget(change, order, leash, false /* forceTranslucent */);
+ }
+
+ /**
+ * Creates a new RemoteAnimationTarget from the provided change and leash
+ */
+ public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+ SurfaceControl leash, boolean forceTranslucent) {
+ if (isDividerBar(change)) {
+ return getDividerTarget(change, leash);
+ }
+
+ int taskId;
+ boolean isNotInRecents;
+ ActivityManager.RunningTaskInfo taskInfo;
+ WindowConfiguration windowConfiguration;
+
+ taskInfo = change.getTaskInfo();
+ if (taskInfo != null) {
+ taskId = taskInfo.taskId;
+ isNotInRecents = !taskInfo.isRunning;
+ windowConfiguration = taskInfo.configuration.windowConfiguration;
+ } else {
+ taskId = INVALID_TASK_ID;
+ isNotInRecents = true;
+ windowConfiguration = new WindowConfiguration();
+ }
+
+ Rect localBounds = new Rect(change.getEndAbsBounds());
+ localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
+
+ RemoteAnimationTarget target = new RemoteAnimationTarget(
+ taskId,
+ newModeToLegacyMode(change.getMode()),
+ // TODO: once we can properly sync transactions across process,
+ // then get rid of this leash.
+ leash,
+ forceTranslucent || (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
+ null,
+ // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+ new Rect(0, 0, 0, 0),
+ order,
+ null,
+ localBounds,
+ new Rect(change.getEndAbsBounds()),
+ windowConfiguration,
+ isNotInRecents,
+ null,
+ new Rect(change.getStartAbsBounds()),
+ taskInfo,
+ change.getAllowEnterPip(),
+ INVALID_WINDOW_TYPE
+ );
+ target.setWillShowImeOnTarget(
+ (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
+ target.setRotationChange(change.getEndRotation() - change.getStartRotation());
+ target.backgroundColor = change.getBackgroundColor();
+ return target;
+ }
+
+ private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change,
+ SurfaceControl leash) {
+ return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
+ leash, false /* isTranslucent */, null /* clipRect */,
+ null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+ new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
+ change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
+ null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
+ TYPE_DOCK_DIVIDER);
+ }
+
+ /**
+ * Finds the "correct" root idx for a change. The change's end display is prioritized, then
+ * the start display. If there is no display, it will fallback on the 0th root in the
+ * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op).
+ */
+ public static int rootIndexFor(@NonNull TransitionInfo.Change change,
+ @NonNull TransitionInfo info) {
+ int rootIdx = info.findRootIndex(change.getEndDisplayId());
+ if (rootIdx >= 0) return rootIdx;
+ rootIdx = info.findRootIndex(change.getStartDisplayId());
+ if (rootIdx >= 0) return rootIdx;
+ return 0;
+ }
+
+ /**
+ * Gets the {@link TransitionInfo.Root} for the given {@link TransitionInfo.Change}.
+ * @see #rootIndexFor(TransitionInfo.Change, TransitionInfo)
+ */
+ @NonNull
+ public static TransitionInfo.Root getRootFor(@NonNull TransitionInfo.Change change,
+ @NonNull TransitionInfo info) {
+ return info.getRoot(rootIndexFor(change, info));
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 6b7ca421f5ed..39fb7936747e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -23,11 +23,13 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.os.Handler;
+import android.os.IBinder;
import android.util.SparseArray;
import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
+import android.window.TransitionInfo;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -72,6 +74,16 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
}
@Override
+ public void onTransitionReady(IBinder transition, TransitionInfo info,
+ TransitionInfo.Change change) {}
+
+ @Override
+ public void onTransitionMerged(IBinder merged, IBinder playing) {}
+
+ @Override
+ public void onTransitionFinished(IBinder transition) {}
+
+ @Override
public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
}
@@ -115,7 +127,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
if (decoration == null) {
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
- decoration.relayout(taskInfo, startT, finishT);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
}
}
@@ -127,7 +139,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
- decoration.relayout(taskInfo, startT, finishT);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
}
@Override
@@ -173,14 +185,16 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
mSyncQueue);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
- final TaskPositioner taskPositioner =
- new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController);
+ final DragPositioningCallback dragPositioningCallback =
+ new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController,
+ null /* disallowedAreaForEndBounds */);
final CaptionTouchEventListener touchEventListener =
- new CaptionTouchEventListener(taskInfo, taskPositioner);
+ new CaptionTouchEventListener(taskInfo, dragPositioningCallback);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
- windowDecoration.setDragPositioningCallback(taskPositioner);
+ windowDecoration.setDragPositioningCallback(dragPositioningCallback);
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
- windowDecoration.relayout(taskInfo, startT, finishT);
+ windowDecoration.relayout(taskInfo, startT, finishT,
+ false /* applyStartTransactionOnDraw */);
setupCaptionColor(taskInfo, windowDecoration);
}
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 060dc4e05b46..116af7094e13 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
@@ -90,15 +90,15 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
@Override
void relayout(RunningTaskInfo taskInfo) {
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- relayout(taskInfo, t, t);
- mSyncQueue.runInSync(transaction -> {
- transaction.merge(t);
- t.close();
- });
+ // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is
+ // synced with the buffer transaction (that draws the View). Both will be shown on screen
+ // at the same, whereas applying them independently causes flickering. See b/270202228.
+ relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */);
}
void relayout(RunningTaskInfo taskInfo,
- SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
+ boolean applyStartTransactionOnDraw) {
final int shadowRadiusID = taskInfo.isFocused
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
@@ -110,19 +110,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
- final int outsetLeftId = R.dimen.freeform_resize_handle;
- final int outsetTopId = R.dimen.freeform_resize_handle;
- final int outsetRightId = R.dimen.freeform_resize_handle;
- final int outsetBottomId = R.dimen.freeform_resize_handle;
-
mRelayoutParams.reset();
mRelayoutParams.mRunningTaskInfo = taskInfo;
mRelayoutParams.mLayoutResId = R.layout.caption_window_decor;
mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
mRelayoutParams.mShadowRadiusId = shadowRadiusID;
- if (isDragResizeable) {
- mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
- }
+ mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
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 dee5f8fa74d8..9fd57d7e1201 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,14 +22,23 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
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.desktopmode.EnterDesktopTaskTransitionHandler.DRAG_FREEFORM_SCALE;
+import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE;
+import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityTaskManager;
import android.content.Context;
+import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Looper;
import android.util.SparseArray;
import android.view.Choreographer;
@@ -39,23 +48,31 @@ import android.view.InputEventReceiver;
import android.view.InputMonitor;
import android.view.MotionEvent;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
import android.view.View;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
import android.window.WindowContainerToken;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopModeController;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.TaskCornersListener;
import java.util.Optional;
+import java.util.function.Supplier;
/**
* View model for the window decoration with a caption and shadows. Works with
@@ -64,6 +81,7 @@ import java.util.Optional;
public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private static final String TAG = "DesktopModeWindowDecorViewModel";
+
private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
private final ActivityTaskManager mActivityTaskManager;
private final ShellTaskOrganizer mTaskOrganizer;
@@ -78,14 +96,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
+ private final TaskCornersListener mCornersListener = new TaskCornersListenerImpl();
+
private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId =
new SparseArray<>();
private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
private final InputMonitorFactory mInputMonitorFactory;
private TaskOperations mTaskOperations;
+ private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
+ private final Transitions mTransitions;
private Optional<SplitScreenController> mSplitScreenController;
+ private ValueAnimator mDragToDesktopValueAnimator;
+ private final Rect mDragToDesktopAnimationStartBounds = new Rect();
+ private boolean mDragToDesktopAnimationStarted;
+
public DesktopModeWindowDecorViewModel(
Context context,
Handler mainHandler,
@@ -93,6 +119,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue,
+ Transitions transitions,
Optional<DesktopModeController> desktopModeController,
Optional<DesktopTasksController> desktopTasksController,
Optional<SplitScreenController> splitScreenController) {
@@ -103,11 +130,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
taskOrganizer,
displayController,
syncQueue,
+ transitions,
desktopModeController,
desktopTasksController,
splitScreenController,
new DesktopModeWindowDecoration.Factory(),
- new InputMonitorFactory());
+ new InputMonitorFactory(),
+ SurfaceControl.Transaction::new);
}
@VisibleForTesting
@@ -118,11 +147,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue,
+ Transitions transitions,
Optional<DesktopModeController> desktopModeController,
Optional<DesktopTasksController> desktopTasksController,
Optional<SplitScreenController> splitScreenController,
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
- InputMonitorFactory inputMonitorFactory) {
+ InputMonitorFactory inputMonitorFactory,
+ Supplier<SurfaceControl.Transaction> transactionFactory) {
mContext = context;
mMainHandler = mainHandler;
mMainChoreographer = mainChoreographer;
@@ -131,11 +162,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDisplayController = displayController;
mSplitScreenController = splitScreenController;
mSyncQueue = syncQueue;
+ mTransitions = transitions;
mDesktopModeController = desktopModeController;
mDesktopTasksController = desktopTasksController;
mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
mInputMonitorFactory = inputMonitorFactory;
+ mTransactionFactory = transactionFactory;
}
@Override
@@ -155,6 +188,34 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
@Override
+ public void onTransitionReady(
+ @NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change) {
+ if (change.getMode() == WindowManager.TRANSIT_CHANGE
+ && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE)) {
+ mWindowDecorByTaskId.get(change.getTaskInfo().taskId)
+ .addTransitionPausingRelayout(transition);
+ }
+ }
+
+ @Override
+ public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+ for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
+ final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
+ decor.mergeTransitionPausingRelayout(merged, playing);
+ }
+ }
+
+ @Override
+ public void onTransitionFinished(@NonNull IBinder transition) {
+ for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
+ final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
+ decor.removeTransitionPausingRelayout(transition);
+ }
+ }
+
+ @Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
@@ -166,7 +227,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
decoration.relayout(taskInfo);
- setupCaptionColor(taskInfo, decoration);
}
@Override
@@ -187,7 +247,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
if (decoration == null) {
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
- decoration.relayout(taskInfo, startT, finishT);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
}
}
@@ -199,7 +259,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
- decoration.relayout(taskInfo, startT, finishT);
+ decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */);
}
@Override
@@ -252,8 +312,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
} else if (id == R.id.back_button) {
mTaskOperations.injectBackKey();
- } else if (id == R.id.caption_handle) {
- decoration.createHandleMenu();
+ } else if (id == R.id.caption_handle || id == R.id.open_menu_button) {
+ if (!decoration.isHandleMenuActive()) {
+ moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
+ decoration.createHandleMenu();
+ } else {
+ decoration.closeHandleMenu();
+ }
} else if (id == R.id.desktop_button) {
mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
mDesktopTasksController.ifPresent(c -> c.moveToDesktop(mTaskId));
@@ -262,21 +327,36 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId));
decoration.closeHandleMenu();
- decoration.setButtonVisibility(false);
} else if (id == R.id.collapse_menu_button) {
decoration.closeHandleMenu();
+ } else if (id == R.id.select_button) {
+ if (DesktopModeStatus.IS_DISPLAY_CHANGE_ENABLED) {
+ // TODO(b/278084491): dev option to enable display switching
+ // remove when select is implemented
+ mDesktopTasksController.ifPresent(c -> c.moveToNextDisplay(mTaskId));
+ decoration.closeHandleMenu();
+ }
}
}
@Override
public boolean onTouch(View v, MotionEvent e) {
final int id = v.getId();
- if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) {
+ if (id != R.id.caption_handle && id != R.id.desktop_mode_caption
+ && id != R.id.open_menu_button && id != R.id.close_window) {
return false;
}
+ moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
return mDragDetector.onMotionEvent(e);
}
+ private void moveTaskToFront(RunningTaskInfo taskInfo) {
+ if (!taskInfo.isFocused) {
+ mDesktopTasksController.ifPresent(c -> c.moveTaskToFront(taskInfo));
+ mDesktopModeController.ifPresent(c -> c.moveTaskToFront(taskInfo));
+ }
+ }
+
/**
* @param e {@link MotionEvent} to process
* @return {@code true} if the motion event is handled.
@@ -297,12 +377,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
case MotionEvent.ACTION_DOWN: {
mDragPointerId = e.getPointerId(0);
mDragPositioningCallback.onDragPositioningStart(
- 0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
+ 0 /* ctrlType */, e.getRawX(0),
+ e.getRawY(0));
mIsDragging = false;
return false;
}
case MotionEvent.ACTION_MOVE: {
+ final DesktopModeWindowDecoration decoration =
+ mWindowDecorByTaskId.get(mTaskId);
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo,
+ decoration.mTaskSurface, e.getRawY(dragPointerIdx)));
mDragPositioningCallback.onDragPositioningMove(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
mIsDragging = true;
@@ -311,18 +396,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
- final int statusBarHeight = mDisplayController
- .getDisplayLayout(taskInfo.displayId).stableInsets().top;
+ // Position of the task is calculated by subtracting the raw location of the
+ // motion event (the location of the motion relative to the display) by the
+ // location of the motion event relative to the task's bounds
+ final Point position = new Point(
+ (int) (e.getRawX(dragPointerIdx) - e.getX(dragPointerIdx)),
+ (int) (e.getRawY(dragPointerIdx) - e.getY(dragPointerIdx)));
mDragPositioningCallback.onDragPositioningEnd(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
- if (e.getRawY(dragPointerIdx) <= statusBarHeight) {
- if (DesktopModeStatus.isProto2Enabled()
- && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
- // Switch a single task to fullscreen
- mDesktopTasksController.ifPresent(
- c -> c.moveToFullscreen(taskInfo));
- }
- }
+ mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo,
+ position));
final boolean wasDragging = mIsDragging;
mIsDragging = false;
return wasDragging;
@@ -409,7 +492,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev);
if (DesktopModeStatus.isProto2Enabled()) {
if (relevantDecor == null
- || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+ || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM
+ || mTransitionDragActive) {
handleCaptionThroughStatusBar(ev, relevantDecor);
}
}
@@ -452,6 +536,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
case MotionEvent.ACTION_DOWN: {
// Begin drag through status bar if applicable.
if (relevantDecor != null) {
+ mDragToDesktopAnimationStartBounds.set(
+ relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
boolean dragFromStatusBarAllowed = false;
if (DesktopModeStatus.isProto2Enabled()) {
// In proto2 any full screen task can be dragged to freeform
@@ -467,33 +553,150 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
case MotionEvent.ACTION_UP: {
if (relevantDecor == null) {
+ mDragToDesktopAnimationStarted = false;
mTransitionDragActive = false;
return;
}
if (mTransitionDragActive) {
mTransitionDragActive = false;
- final int statusBarHeight = mDisplayController
- .getDisplayLayout(relevantDecor.mTaskInfo.displayId).stableInsets().top;
- if (ev.getY() > statusBarHeight) {
+ final int statusBarHeight = getStatusBarHeight(
+ relevantDecor.mTaskInfo.displayId);
+ if (ev.getY() > 2 * statusBarHeight) {
if (DesktopModeStatus.isProto2Enabled()) {
- mDesktopTasksController.ifPresent(
- c -> c.moveToDesktop(relevantDecor.mTaskInfo));
+ animateToDesktop(relevantDecor, ev);
} else if (DesktopModeStatus.isProto1Enabled()) {
mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
}
-
+ mDragToDesktopAnimationStarted = false;
+ return;
+ } else if (mDragToDesktopAnimationStarted) {
+ Point position = new Point((int) ev.getX(), (int) ev.getY());
+ mDesktopTasksController.ifPresent(
+ c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo,
+ position));
+ mDragToDesktopAnimationStarted = false;
return;
}
}
relevantDecor.checkClickEvent(ev);
break;
}
+
+ case MotionEvent.ACTION_MOVE: {
+ if (relevantDecor == null) {
+ return;
+ }
+ if (mTransitionDragActive) {
+ mDesktopTasksController.ifPresent(
+ c -> c.onDragPositioningMoveThroughStatusBar(
+ relevantDecor.mTaskInfo,
+ relevantDecor.mTaskSurface, ev.getY()));
+ final int statusBarHeight = getStatusBarHeight(
+ relevantDecor.mTaskInfo.displayId);
+ if (ev.getY() > statusBarHeight) {
+ if (!mDragToDesktopAnimationStarted) {
+ mDragToDesktopAnimationStarted = true;
+ mDesktopTasksController.ifPresent(
+ c -> c.moveToFreeform(relevantDecor.mTaskInfo,
+ mDragToDesktopAnimationStartBounds));
+ startAnimation(relevantDecor);
+ }
+ }
+ if (mDragToDesktopAnimationStarted) {
+ Transaction t = mTransactionFactory.get();
+ float width = (float) mDragToDesktopValueAnimator.getAnimatedValue()
+ * mDragToDesktopAnimationStartBounds.width();
+ float x = ev.getX() - (width / 2);
+ t.setPosition(relevantDecor.mTaskSurface, x, ev.getY());
+ t.apply();
+ }
+ }
+ break;
+ }
+
case MotionEvent.ACTION_CANCEL: {
mTransitionDragActive = false;
+ mDragToDesktopAnimationStarted = false;
}
}
}
+ /**
+ * Gets bounds of a scaled window centered relative to the screen bounds
+ * @param scale the amount to scale to relative to the Screen Bounds
+ */
+ private Rect calculateFreeformBounds(int displayId, float scale) {
+ final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId);
+ final int screenWidth = displayLayout.width();
+ final int screenHeight = displayLayout.height();
+
+ final float adjustmentPercentage = (1f - scale) / 2;
+ final Rect endBounds = new Rect((int) (screenWidth * adjustmentPercentage),
+ (int) (screenHeight * adjustmentPercentage),
+ (int) (screenWidth * (adjustmentPercentage + scale)),
+ (int) (screenHeight * (adjustmentPercentage + scale)));
+ return endBounds;
+ }
+
+ /**
+ * Blocks relayout until transition is finished and transitions to Desktop
+ */
+ private void animateToDesktop(DesktopModeWindowDecoration relevantDecor,
+ MotionEvent ev) {
+ relevantDecor.incrementRelayoutBlock();
+ centerAndMoveToDesktopWithAnimation(relevantDecor, ev);
+ }
+
+ /**
+ * Animates a window to the center, grows to freeform size, and transitions to Desktop Mode.
+ * @param relevantDecor the window decor of the task to be animated
+ * @param ev the motion event that triggers the animation
+ */
+ private void centerAndMoveToDesktopWithAnimation(DesktopModeWindowDecoration relevantDecor,
+ MotionEvent ev) {
+ ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+ animator.setDuration(FREEFORM_ANIMATION_DURATION);
+ final SurfaceControl sc = relevantDecor.mTaskSurface;
+ final Rect endBounds = calculateFreeformBounds(ev.getDisplayId(), DRAG_FREEFORM_SCALE);
+ final Transaction t = mTransactionFactory.get();
+ final float diffX = endBounds.centerX() - ev.getX();
+ final float diffY = endBounds.top - ev.getY();
+ final float startingX = ev.getX() - DRAG_FREEFORM_SCALE
+ * mDragToDesktopAnimationStartBounds.width() / 2;
+
+ animator.addUpdateListener(animation -> {
+ final float animatorValue = (float) animation.getAnimatedValue();
+ final float x = startingX + diffX * animatorValue;
+ final float y = ev.getY() + diffY * animatorValue;
+ t.setPosition(sc, x, y);
+ t.apply();
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mDesktopTasksController.ifPresent(
+ c -> c.onDragPositioningEndThroughStatusBar(
+ relevantDecor.mTaskInfo,
+ calculateFreeformBounds(ev.getDisplayId(), FINAL_FREEFORM_SCALE)));
+ }
+ });
+ animator.start();
+ }
+
+ private void startAnimation(@NonNull DesktopModeWindowDecoration focusedDecor) {
+ mDragToDesktopValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE);
+ mDragToDesktopValueAnimator.setDuration(FREEFORM_ANIMATION_DURATION);
+ final Transaction t = mTransactionFactory.get();
+ mDragToDesktopValueAnimator.addUpdateListener(animation -> {
+ final float animatorValue = (float) animation.getAnimatedValue();
+ SurfaceControl sc = focusedDecor.mTaskSurface;
+ t.setScale(sc, animatorValue, animatorValue);
+ t.apply();
+ });
+
+ mDragToDesktopValueAnimator.start();
+ }
+
@Nullable
private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
if (mSplitScreenController.isPresent()
@@ -540,8 +743,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
return focusedDecor;
}
+ private int getStatusBarHeight(int displayId) {
+ return mDisplayController.getDisplayLayout(displayId).stableInsets().top;
+ }
+
private void createInputChannel(int displayId) {
- final InputManager inputManager = InputManager.getInstance();
+ final InputManager inputManager = mContext.getSystemService(InputManager.class);
final InputMonitor inputMonitor =
mInputMonitorFactory.create(inputManager, mContext);
final EventReceiver eventReceiver = new EventReceiver(inputMonitor,
@@ -556,15 +763,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
}
- private void setupCaptionColor(RunningTaskInfo taskInfo,
- DesktopModeWindowDecoration decoration) {
- if (taskInfo == null || taskInfo.taskDescription == null) return;
- final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
- decoration.setCaptionColor(statusBarColor);
- }
-
private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
+ if (mSplitScreenController.isPresent()
+ && mSplitScreenController.get().isTaskRootOrStageRoot(taskInfo.taskId)) {
+ return false;
+ }
return DesktopModeStatus.isProto2Enabled()
&& taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
&& mDisplayController.getDisplayContext(taskInfo.displayId)
@@ -593,20 +797,39 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mSyncQueue);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
- final TaskPositioner taskPositioner =
- new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController,
- mDragStartListener);
+ final DragPositioningCallback dragPositioningCallback = createDragPositioningCallback(
+ windowDecoration, taskInfo);
final DesktopModeTouchEventListener touchEventListener =
- new DesktopModeTouchEventListener(taskInfo, taskPositioner);
+ new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);
+
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
- windowDecoration.setDragPositioningCallback(taskPositioner);
+ windowDecoration.setCornersListener(mCornersListener);
+ windowDecoration.setDragPositioningCallback(dragPositioningCallback);
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
- windowDecoration.relayout(taskInfo, startT, finishT);
- setupCaptionColor(taskInfo, windowDecoration);
+ windowDecoration.relayout(taskInfo, startT, finishT,
+ false /* applyStartTransactionOnDraw */);
incrementEventReceiverTasks(taskInfo.displayId);
}
+ private DragPositioningCallback createDragPositioningCallback(
+ @NonNull DesktopModeWindowDecoration windowDecoration,
+ @NonNull RunningTaskInfo taskInfo) {
+ final int screenWidth = mDisplayController.getDisplayLayout(taskInfo.displayId).width();
+ final Rect disallowedAreaForEndBounds = new Rect(0, 0, screenWidth,
+ getStatusBarHeight(taskInfo.displayId));
+ if (!DesktopModeStatus.isVeiledResizeEnabled()) {
+ return new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration,
+ mDisplayController, disallowedAreaForEndBounds, mDragStartListener,
+ mTransactionFactory);
+ } else {
+ windowDecoration.createResizeVeil();
+ return new VeiledResizeTaskPositioner(mTaskOrganizer, windowDecoration,
+ mDisplayController, disallowedAreaForEndBounds, mDragStartListener,
+ mTransitions);
+ }
+ }
- private class DragStartListenerImpl implements TaskPositioner.DragStartListener {
+ private class DragStartListenerImpl
+ implements DragPositioningCallbackUtility.DragStartListener {
@Override
public void onDragStart(int taskId) {
mWindowDecorByTaskId.get(taskId).closeHandleMenu();
@@ -618,6 +841,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
return inputManager.monitorGestureInput("caption-touch", context.getDisplayId());
}
}
+
+ private class TaskCornersListenerImpl
+ implements DesktopModeWindowDecoration.TaskCornersListener {
+
+ @Override
+ public void onTaskCornersChanged(int taskId, Region corner) {
+ mDesktopModeController.ifPresent(d -> d.onTaskCornersChanged(taskId, corner));
+ mDesktopTasksController.ifPresent(d -> d.onTaskCornersChanged(taskId, corner));
+ }
+
+ @Override
+ public void onTaskCornersRemoved(int taskId) {
+ mDesktopModeController.ifPresent(d -> d.removeCornersForTask(taskId));
+ mDesktopTasksController.ifPresent(d -> d.removeCornersForTask(taskId));
+ }
+ }
}
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 3c0ef965f4f5..ce11b2604559 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
@@ -22,46 +22,51 @@ import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.res.ColorStateList;
import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Color;
+import android.content.res.TypedArray;
import android.graphics.Point;
import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.Region;
import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
+import android.os.IBinder;
import android.util.Log;
import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewConfiguration;
-import android.widget.ImageView;
-import android.widget.TextView;
import android.window.WindowContainerTransaction;
+import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder;
+import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder;
+import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder;
+
+import java.util.HashSet;
+import java.util.Set;
/**
* Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
- * {@link DesktopModeWindowDecorViewModel}. The caption bar contains a handle, back button, and
- * close button.
+ * {@link DesktopModeWindowDecorViewModel}.
*
* The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
*/
public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
private static final String TAG = "DesktopModeWindowDecoration";
+
private final Handler mHandler;
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
+ private DesktopModeWindowDecorationViewHolder mWindowDecorViewHolder;
private View.OnClickListener mOnCaptionButtonClickListener;
private View.OnTouchListener mOnCaptionTouchListener;
private DragPositioningCallback mDragPositioningCallback;
@@ -69,16 +74,21 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private DragDetector mDragDetector;
private RelayoutParams mRelayoutParams = new RelayoutParams();
- private final int mCaptionMenuHeightId = R.dimen.freeform_decor_caption_menu_height;
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
- private boolean mDesktopActive;
- private AdditionalWindow mHandleMenu;
- private final int mHandleMenuWidthId = R.dimen.freeform_decor_caption_menu_width;
- private final int mHandleMenuShadowRadiusId = R.dimen.caption_menu_shadow_radius;
- private final int mHandleMenuCornerRadiusId = R.dimen.caption_menu_corner_radius;
- private PointF mHandleMenuPosition = new PointF();
+ private final Point mPositionInParent = new Point();
+ private HandleMenu mHandleMenu;
+
+ private ResizeVeil mResizeVeil;
+
+ private Drawable mAppIcon;
+ private CharSequence mAppName;
+
+ private TaskCornersListener mCornersListener;
+
+ private final Set<IBinder> mTransitionsPausingRelayout = new HashSet<>();
+ private int mRelayoutBlock;
DesktopModeWindowDecoration(
Context context,
@@ -94,7 +104,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mHandler = handler;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
- mDesktopActive = DesktopModeStatus.isActive(mContext);
+
+ loadAppInfo();
}
@Override
@@ -115,6 +126,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mOnCaptionTouchListener = onCaptionTouchListener;
}
+ void setCornersListener(TaskCornersListener cornersListener) {
+ mCornersListener = cornersListener;
+ }
+
void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) {
mDragPositioningCallback = dragPositioningCallback;
}
@@ -126,16 +141,23 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
@Override
void relayout(ActivityManager.RunningTaskInfo taskInfo) {
+ // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell
+ // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl
+ // and interferes with the transition animation that is playing at the same time.
+ if (mRelayoutBlock > 0) {
+ return;
+ }
+
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- relayout(taskInfo, t, t);
- mSyncQueue.runInSync(transaction -> {
- transaction.merge(t);
- t.close();
- });
+ // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is
+ // synced with the buffer transaction (that draws the View). Both will be shown on screen
+ // at the same, whereas applying them independently causes flickering. See b/270202228.
+ relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */);
}
void relayout(ActivityManager.RunningTaskInfo taskInfo,
- SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
+ boolean applyStartTransactionOnDraw) {
final int shadowRadiusID = taskInfo.isFocused
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
@@ -143,23 +165,27 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
+ if (isHandleMenuActive()) {
+ mHandleMenu.relayout(startT);
+ }
+
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
- final int outsetLeftId = R.dimen.freeform_resize_handle;
- final int outsetTopId = R.dimen.freeform_resize_handle;
- final int outsetRightId = R.dimen.freeform_resize_handle;
- final int outsetBottomId = R.dimen.freeform_resize_handle;
-
+ final int windowDecorLayoutId = getDesktopModeWindowDecorLayoutId(
+ taskInfo.getWindowingMode());
mRelayoutParams.reset();
mRelayoutParams.mRunningTaskInfo = taskInfo;
- mRelayoutParams.mLayoutResId = R.layout.desktop_mode_window_decor;
+ mRelayoutParams.mLayoutResId = windowDecorLayoutId;
mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
mRelayoutParams.mShadowRadiusId = shadowRadiusID;
- if (isDragResizeable) {
- mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
- }
+ mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
+
+ final TypedArray ta = mContext.obtainStyledAttributes(
+ new int[]{android.R.attr.dialogCornerRadius});
+ mRelayoutParams.mCornerRadius = ta.getDimensionPixelSize(0, 0);
+ ta.recycle();
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
@@ -172,23 +198,29 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
return;
}
if (oldRootView != mResult.mRootView) {
- setupRootView();
+ if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) {
+ mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder(
+ mResult.mRootView,
+ mOnCaptionTouchListener,
+ mOnCaptionButtonClickListener
+ );
+ } else if (mRelayoutParams.mLayoutResId
+ == R.layout.desktop_mode_app_controls_window_decor) {
+ mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder(
+ mResult.mRootView,
+ mOnCaptionTouchListener,
+ mOnCaptionButtonClickListener,
+ mAppName,
+ mAppIcon
+ );
+ } else {
+ throw new IllegalArgumentException("Unexpected layout resource id");
+ }
}
+ mWindowDecorViewHolder.bindData(mTaskInfo);
- // If this task is not focused, do not show caption.
- setCaptionVisibility(mTaskInfo.isFocused);
-
- if (mTaskInfo.isFocused) {
- if (DesktopModeStatus.isProto2Enabled()) {
- updateButtonVisibility();
- } else if (DesktopModeStatus.isProto1Enabled()) {
- // Only handle should show if Desktop Mode is inactive.
- boolean desktopCurrentStatus = DesktopModeStatus.isActive(mContext);
- if (mDesktopActive != desktopCurrentStatus) {
- mDesktopActive = desktopCurrentStatus;
- setButtonVisibility(mDesktopActive);
- }
- }
+ if (!mTaskInfo.isFocused) {
+ closeHandleMenu();
}
if (!isDragResizeable) {
@@ -215,174 +247,93 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
.getDimensionPixelSize(R.dimen.freeform_resize_handle);
final int resize_corner = mResult.mRootView.getResources()
.getDimensionPixelSize(R.dimen.freeform_resize_corner);
- mDragResizeListener.setGeometry(
- mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
- }
- /**
- * Sets up listeners when a new root view is created.
- */
- private void setupRootView() {
- final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
- caption.setOnTouchListener(mOnCaptionTouchListener);
- final View handle = caption.findViewById(R.id.caption_handle);
- handle.setOnTouchListener(mOnCaptionTouchListener);
- handle.setOnClickListener(mOnCaptionButtonClickListener);
- if (DesktopModeStatus.isProto1Enabled()) {
- final View back = caption.findViewById(R.id.back_button);
- back.setOnClickListener(mOnCaptionButtonClickListener);
- final View close = caption.findViewById(R.id.close_window);
- close.setOnClickListener(mOnCaptionButtonClickListener);
+ // If either task geometry or position have changed, update this task's cornersListener
+ if (mDragResizeListener.setGeometry(
+ mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop)
+ || !mTaskInfo.positionInParent.equals(mPositionInParent)) {
+ mCornersListener.onTaskCornersChanged(mTaskInfo.taskId, getGlobalCornersRegion());
}
- updateButtonVisibility();
- }
-
- private void setupHandleMenu() {
- final View menu = mHandleMenu.mWindowViewHost.getView();
- final View fullscreen = menu.findViewById(R.id.fullscreen_button);
- fullscreen.setOnClickListener(mOnCaptionButtonClickListener);
- final View desktop = menu.findViewById(R.id.desktop_button);
- if (DesktopModeStatus.isProto2Enabled()) {
- desktop.setOnClickListener(mOnCaptionButtonClickListener);
- } else if (DesktopModeStatus.isProto1Enabled()) {
- desktop.setVisibility(View.GONE);
- }
- final View split = menu.findViewById(R.id.split_screen_button);
- split.setOnClickListener(mOnCaptionButtonClickListener);
- final View close = menu.findViewById(R.id.close_button);
- close.setOnClickListener(mOnCaptionButtonClickListener);
- final View collapse = menu.findViewById(R.id.collapse_menu_button);
- collapse.setOnClickListener(mOnCaptionButtonClickListener);
- menu.setOnTouchListener(mOnCaptionTouchListener);
-
- String packageName = mTaskInfo.baseActivity.getPackageName();
+ mPositionInParent.set(mTaskInfo.positionInParent);
+ }
+
+ boolean isHandleMenuActive() {
+ return mHandleMenu != null;
+ }
+
+ private void loadAppInfo() {
+ String packageName = mTaskInfo.realActivity.getPackageName();
PackageManager pm = mContext.getApplicationContext().getPackageManager();
- // TODO(b/268363572): Use IconProvider or BaseIconCache to set drawable/name.
try {
+ IconProvider provider = new IconProvider(mContext);
+ mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
+ PackageManager.ComponentInfoFlags.of(0)));
ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
PackageManager.ApplicationInfoFlags.of(0));
- final ImageView appIcon = menu.findViewById(R.id.application_icon);
- appIcon.setImageDrawable(pm.getApplicationIcon(applicationInfo));
- final TextView appName = menu.findViewById(R.id.application_name);
- appName.setText(pm.getApplicationLabel(applicationInfo));
+ mAppName = pm.getApplicationLabel(applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Package not found: " + packageName, e);
}
}
- /**
- * Sets caption visibility based on task focus.
- * Note: Only applicable to Desktop Proto 1; Proto 2 only closes handle menu on focus loss
- * @param visible whether or not the caption should be visible
- */
- private void setCaptionVisibility(boolean visible) {
- if (!visible) closeHandleMenu();
- if (!DesktopModeStatus.isProto1Enabled()) return;
- final int v = visible ? View.VISIBLE : View.GONE;
- final View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
- captionView.setVisibility(v);
-
+ private void closeDragResizeListener() {
+ if (mDragResizeListener == null) {
+ return;
+ }
+ mDragResizeListener.close();
+ mDragResizeListener = null;
}
/**
- * Sets the visibility of buttons and color of caption based on desktop mode status
+ * Create the resize veil for this task. Note the veil's visibility is View.GONE by default
+ * until a resize event calls showResizeVeil below.
*/
- void updateButtonVisibility() {
- if (DesktopModeStatus.isProto2Enabled()) {
- setButtonVisibility(mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM);
- } else if (DesktopModeStatus.isProto1Enabled()) {
- mDesktopActive = DesktopModeStatus.isActive(mContext);
- setButtonVisibility(mDesktopActive);
- }
+ void createResizeVeil() {
+ mResizeVeil = new ResizeVeil(mContext, mAppIcon, mTaskInfo,
+ mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier);
}
/**
- * Show or hide buttons
+ * Fade in the resize veil
*/
- void setButtonVisibility(boolean visible) {
- final int visibility = visible && DesktopModeStatus.isProto1Enabled()
- ? View.VISIBLE : View.GONE;
- final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
- final View back = caption.findViewById(R.id.back_button);
- final View close = caption.findViewById(R.id.close_window);
- back.setVisibility(visibility);
- close.setVisibility(visibility);
- final int buttonTintColorRes =
- mDesktopActive ? R.color.decor_button_dark_color
- : R.color.decor_button_light_color;
- final ColorStateList buttonTintColor =
- caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
- final View handle = caption.findViewById(R.id.caption_handle);
- final VectorDrawable handleBackground = (VectorDrawable) handle.getBackground();
- handleBackground.setTintList(buttonTintColor);
+ void showResizeVeil(Rect taskBounds) {
+ mResizeVeil.showVeil(mTaskSurface, taskBounds);
}
- boolean isHandleMenuActive() {
- return mHandleMenu != null;
+ /**
+ * Set new bounds for the resize veil
+ */
+ void updateResizeVeil(Rect newBounds) {
+ mResizeVeil.updateResizeVeil(newBounds);
}
- void setCaptionColor(int captionColor) {
- if (mResult.mRootView == null) {
- return;
- }
-
- final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
- final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground();
- captionDrawable.setColor(captionColor);
-
- final int buttonTintColorRes =
- Color.valueOf(captionColor).luminance() < 0.5
- ? R.color.decor_button_light_color
- : R.color.decor_button_dark_color;
- final ColorStateList buttonTintColor =
- caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
-
- final View handle = caption.findViewById(R.id.caption_handle);
- final Drawable handleBackground = handle.getBackground();
- handleBackground.setTintList(buttonTintColor);
- if (DesktopModeStatus.isProto1Enabled()) {
- final View back = caption.findViewById(R.id.back_button);
- final Drawable backBackground = back.getBackground();
- backBackground.setTintList(buttonTintColor);
- final View close = caption.findViewById(R.id.close_window);
- final Drawable closeBackground = close.getBackground();
- closeBackground.setTintList(buttonTintColor);
- }
+ /**
+ * Fade the resize veil out.
+ */
+ void hideResizeVeil() {
+ mResizeVeil.hideVeil();
}
- private void closeDragResizeListener() {
- if (mDragResizeListener == null) {
- return;
- }
- mDragResizeListener.close();
- mDragResizeListener = null;
+ private void disposeResizeVeil() {
+ if (mResizeVeil == null) return;
+ mResizeVeil.dispose();
+ mResizeVeil = null;
}
/**
* Create and display handle menu window
*/
void createHandleMenu() {
- final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- final Resources resources = mDecorWindowContext.getResources();
- final int captionWidth = mTaskInfo.getConfiguration()
- .windowConfiguration.getBounds().width();
- final int menuWidth = loadDimensionPixelSize(resources, mHandleMenuWidthId);
- final int menuHeight = loadDimensionPixelSize(resources, mCaptionMenuHeightId);
- final int shadowRadius = loadDimensionPixelSize(resources, mHandleMenuShadowRadiusId);
- final int cornerRadius = loadDimensionPixelSize(resources, mHandleMenuCornerRadiusId);
-
- final int x = mRelayoutParams.mCaptionX + (captionWidth / 2) - (menuWidth / 2)
- - mResult.mDecorContainerOffsetX;
- final int y = mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY;
- mHandleMenuPosition.set(x, y);
- String namePrefix = "Caption Menu";
- mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t, x, y,
- menuWidth, menuHeight, shadowRadius, cornerRadius);
- mSyncQueue.runInSync(transaction -> {
- transaction.merge(t);
- t.close();
- });
- setupHandleMenu();
+ mHandleMenu = new HandleMenu.Builder(this)
+ .setAppIcon(mAppIcon)
+ .setAppName(mAppName)
+ .setOnClickListener(mOnCaptionButtonClickListener)
+ .setOnTouchListener(mOnCaptionTouchListener)
+ .setLayoutId(mRelayoutParams.mLayoutResId)
+ .setCaptionPosition(mRelayoutParams.mCaptionX, mRelayoutParams.mCaptionY)
+ .setWindowingButtonsVisible(DesktopModeStatus.isProto2Enabled())
+ .build();
+ mHandleMenu.show();
}
/**
@@ -390,7 +341,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
void closeHandleMenu() {
if (!isHandleMenuActive()) return;
- mHandleMenu.releaseView();
+ mHandleMenu.close();
mHandleMenu = null;
}
@@ -408,14 +359,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
void closeHandleMenuIfNeeded(MotionEvent ev) {
if (!isHandleMenuActive()) return;
- // When this is called before the layout is fully inflated, width will be 0.
- // Menu is not visible in this scenario, so skip the check if that is the case.
- if (mHandleMenu.mWindowViewHost.getView().getWidth() == 0) return;
-
PointF inputPoint = offsetCaptionLocation(ev);
- if (!pointInView(mHandleMenu.mWindowViewHost.getView(),
- inputPoint.x - mHandleMenuPosition.x - mResult.mDecorContainerOffsetX,
- inputPoint.y - mHandleMenuPosition.y - mResult.mDecorContainerOffsetY)) {
+
+ // If this is called before open_menu_button's onClick, we don't want to close
+ // the menu since it will just reopen in onClick.
+ final boolean pointInOpenMenuButton = pointInView(
+ mResult.mRootView.findViewById(R.id.open_menu_button),
+ inputPoint.x,
+ inputPoint.y);
+
+ if (!mHandleMenu.isValidMenuInput(inputPoint) && !pointInOpenMenuButton) {
closeHandleMenu();
}
}
@@ -472,14 +425,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final View handle = caption.findViewById(R.id.caption_handle);
clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle);
} else {
- final View menu = mHandleMenu.mWindowViewHost.getView();
- final int captionWidth = mTaskInfo.getConfiguration().windowConfiguration
- .getBounds().width();
- final int menuX = mRelayoutParams.mCaptionX + (captionWidth / 2)
- - (menu.getWidth() / 2);
- final PointF inputPoint = new PointF(ev.getX() - menuX, ev.getY());
- final View collapse = menu.findViewById(R.id.collapse_menu_button);
- if (clickIfPointInView(inputPoint, collapse)) return;
+ mHandleMenu.checkClickEvent(ev);
}
}
@@ -491,7 +437,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
return false;
}
- private boolean pointInView(View v, float x, float y) {
+ boolean pointInView(View v, float x, float y) {
return v != null && v.getLeft() <= x && v.getRight() >= x
&& v.getTop() <= y && v.getBottom() >= y;
}
@@ -500,9 +446,63 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
public void close() {
closeDragResizeListener();
closeHandleMenu();
+ mCornersListener.onTaskCornersRemoved(mTaskInfo.taskId);
+ disposeResizeVeil();
super.close();
}
+ private int getDesktopModeWindowDecorLayoutId(int windowingMode) {
+ if (DesktopModeStatus.isProto1Enabled()) {
+ return R.layout.desktop_mode_app_controls_window_decor;
+ }
+ return windowingMode == WINDOWING_MODE_FREEFORM
+ ? R.layout.desktop_mode_app_controls_window_decor
+ : R.layout.desktop_mode_focused_window_decor;
+ }
+
+ /**
+ * Create a new region out of the corner rects of this task.
+ */
+ Region getGlobalCornersRegion() {
+ Region cornersRegion = mDragResizeListener.getCornersRegion();
+ cornersRegion.translate(mPositionInParent.x, mPositionInParent.y);
+ return cornersRegion;
+ }
+
+ /**
+ * If transition exists in mTransitionsPausingRelayout, remove the transition and decrement
+ * mRelayoutBlock
+ */
+ void removeTransitionPausingRelayout(IBinder transition) {
+ if (mTransitionsPausingRelayout.remove(transition)) {
+ mRelayoutBlock--;
+ }
+ }
+
+ /**
+ * Add transition to mTransitionsPausingRelayout
+ */
+ void addTransitionPausingRelayout(IBinder transition) {
+ mTransitionsPausingRelayout.add(transition);
+ }
+
+ /**
+ * If two transitions merge and the merged transition is in mTransitionsPausingRelayout,
+ * remove the merged transition from the set and add the transition it was merged into.
+ */
+ public void mergeTransitionPausingRelayout(IBinder merged, IBinder playing) {
+ if (mTransitionsPausingRelayout.remove(merged)) {
+ mTransitionsPausingRelayout.add(playing);
+ }
+ }
+
+ /**
+ * Increase mRelayoutBlock, stopping relayout if mRelayoutBlock is now greater than 0.
+ */
+ public void incrementRelayoutBlock() {
+ mRelayoutBlock++;
+ }
+
static class Factory {
DesktopModeWindowDecoration create(
@@ -525,4 +525,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
syncQueue);
}
}
+
+ interface TaskCornersListener {
+ /** Inform the implementing class of this task's change in corner resize handles */
+ void onTaskCornersChanged(int taskId, Region corner);
+
+ /** Inform the implementing class that this task no longer needs its corners tracked,
+ * likely due to it closing. */
+ void onTaskCornersRemoved(int taskId);
+ }
}
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 0191c609a8b2..4e98f0c3a48a 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
@@ -16,19 +16,29 @@
package com.android.wm.shell.windowdecor;
+import android.annotation.IntDef;
+
/**
* Callback called when receiving drag-resize or drag-move related input events.
*/
public interface DragPositioningCallback {
+ @IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM})
+ @interface CtrlType {}
+
+ int CTRL_TYPE_UNDEFINED = 0;
+ int CTRL_TYPE_LEFT = 1;
+ int CTRL_TYPE_RIGHT = 2;
+ int CTRL_TYPE_TOP = 4;
+ int CTRL_TYPE_BOTTOM = 8;
/**
* Called when a drag-resize or drag-move starts.
*
- * @param ctrlType {@link TaskPositioner.CtrlType} indicating the direction of resizing, use
+ * @param ctrlType {@link CtrlType} indicating the direction of resizing, use
* {@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
*/
- void onDragPositioningStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);
+ void 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/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
new file mode 100644
index 000000000000..09e29bcbcf9f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -0,0 +1,187 @@
+/*
+ * 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.windowdecor;
+
+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;
+import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
+import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+
+/**
+ * Utility class that contains logic common to classes implementing {@link DragPositioningCallback}
+ * Specifically, this class contains logic for determining changed bounds from a drag input
+ * and applying that change to the task bounds when applicable.
+ */
+public class DragPositioningCallbackUtility {
+
+ /**
+ * Determine the delta between input's current point and the input start point.
+ * @param inputX current input x coordinate
+ * @param inputY current input y coordinate
+ * @param repositionStartPoint initial input coordinate
+ * @return delta between these two points
+ */
+ static PointF calculateDelta(float inputX, float inputY, PointF repositionStartPoint) {
+ final float deltaX = inputX - repositionStartPoint.x;
+ final float deltaY = inputY - repositionStartPoint.y;
+ return new PointF(deltaX, deltaY);
+ }
+
+ /**
+ * Based on type of resize and delta provided, calculate the new bounds to display for this
+ * task.
+ * @param ctrlType type of drag being performed
+ * @param repositionTaskBounds the bounds the task is being repositioned to
+ * @param taskBoundsAtDragStart the bounds of the task on the first drag input event
+ * @param stableBounds bounds that represent the resize limit of this task
+ * @param delta difference between start input and current input in x/y coordinates
+ * @param displayController task's display controller
+ * @param windowDecoration window decoration of the task being dragged
+ * @return whether this method changed repositionTaskBounds
+ */
+ static boolean changeBounds(int ctrlType, Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
+ Rect stableBounds, PointF delta, DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ // If task is being dragged rather than resized, return since this method only handles
+ // with resizing
+ if (ctrlType == CTRL_TYPE_UNDEFINED) {
+ return false;
+ }
+
+ final int oldLeft = repositionTaskBounds.left;
+ final int oldTop = repositionTaskBounds.top;
+ final int oldRight = repositionTaskBounds.right;
+ final int oldBottom = repositionTaskBounds.bottom;
+
+
+ repositionTaskBounds.set(taskBoundsAtDragStart);
+
+ // Make sure the new resizing destination in any direction falls within the stable bounds.
+ // If not, set the bounds back to the old location that was valid to avoid conflicts with
+ // some regions such as the gesture area.
+ displayController.getDisplayLayout(windowDecoration.mDisplay.getDisplayId())
+ .getStableBounds(stableBounds);
+ if ((ctrlType & CTRL_TYPE_LEFT) != 0) {
+ final int candidateLeft = repositionTaskBounds.left + (int) delta.x;
+ repositionTaskBounds.left = (candidateLeft > stableBounds.left)
+ ? candidateLeft : oldLeft;
+ }
+ if ((ctrlType & CTRL_TYPE_RIGHT) != 0) {
+ final int candidateRight = repositionTaskBounds.right + (int) delta.x;
+ repositionTaskBounds.right = (candidateRight < stableBounds.right)
+ ? candidateRight : oldRight;
+ }
+ if ((ctrlType & CTRL_TYPE_TOP) != 0) {
+ final int candidateTop = repositionTaskBounds.top + (int) delta.y;
+ repositionTaskBounds.top = (candidateTop > stableBounds.top)
+ ? candidateTop : oldTop;
+ }
+ if ((ctrlType & CTRL_TYPE_BOTTOM) != 0) {
+ final int candidateBottom = repositionTaskBounds.bottom + (int) delta.y;
+ repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom)
+ ? candidateBottom : oldBottom;
+ }
+ // If width or height are negative or less than the minimum width or height, revert the
+ // respective bounds to use previous bound dimensions.
+ if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
+ repositionTaskBounds.right = oldRight;
+ repositionTaskBounds.left = oldLeft;
+ }
+ if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
+ repositionTaskBounds.top = oldTop;
+ repositionTaskBounds.bottom = oldBottom;
+ }
+ // If there are no changes to the bounds after checking new bounds against minimum width
+ // and height, do not set bounds and return false
+ if (oldLeft == repositionTaskBounds.left && oldTop == repositionTaskBounds.top
+ && oldRight == repositionTaskBounds.right
+ && oldBottom == repositionTaskBounds.bottom) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set bounds using a {@link SurfaceControl.Transaction}.
+ */
+ static void setPositionOnDrag(WindowDecoration decoration, Rect repositionTaskBounds,
+ Rect taskBoundsAtDragStart, PointF repositionStartPoint, SurfaceControl.Transaction t,
+ float x, float y) {
+ updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint, x, y);
+ t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left,
+ repositionTaskBounds.top);
+ }
+
+ static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
+ PointF repositionStartPoint, float x, float y) {
+ final float deltaX = x - repositionStartPoint.x;
+ final float deltaY = y - repositionStartPoint.y;
+ repositionTaskBounds.set(taskBoundsAtDragStart);
+ repositionTaskBounds.offset((int) deltaX, (int) deltaY);
+ }
+
+ /**
+ * Apply a bounds change to a task.
+ * @param windowDecoration decor of task we are changing bounds for
+ * @param taskBounds new bounds of this task
+ * @param taskOrganizer applies the provided WindowContainerTransaction
+ */
+ static void applyTaskBoundsChange(WindowContainerTransaction wct,
+ WindowDecoration windowDecoration, Rect taskBounds, ShellTaskOrganizer taskOrganizer) {
+ wct.setBounds(windowDecoration.mTaskInfo.token, taskBounds);
+ taskOrganizer.applyTransaction(wct);
+ }
+
+ private static float getMinWidth(DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize(displayController,
+ windowDecoration)
+ : windowDecoration.mTaskInfo.minWidth;
+ }
+
+ private static float getMinHeight(DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize(displayController,
+ windowDecoration)
+ : windowDecoration.mTaskInfo.minHeight;
+ }
+
+ private static float getDefaultMinSize(DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
+ .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ return windowDecoration.mTaskInfo.defaultMinSize * density;
+ }
+
+ interface DragStartListener {
+ /**
+ * Inform the implementing class that a drag resize has started
+ * @param taskId id of this positioner's {@link WindowDecoration}
+ */
+ void onDragStart(int taskId);
+ }
+}
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 81c4176b0f39..287d86187288 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
@@ -21,6 +21,11 @@ import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+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;
+import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
+
import android.content.Context;
import android.graphics.Rect;
import android.graphics.Region;
@@ -64,8 +69,8 @@ class DragResizeInputListener implements AutoCloseable {
private final TaskResizeInputEventReceiver mInputEventReceiver;
private final DragPositioningCallback mCallback;
- private int mWidth;
- private int mHeight;
+ private int mTaskWidth;
+ private int mTaskHeight;
private int mResizeHandleThickness;
private int mCornerSize;
@@ -103,7 +108,9 @@ class DragResizeInputListener implements AutoCloseable {
null /* hostInputToken */,
FLAG_NOT_FOCUSABLE,
PRIVATE_FLAG_TRUSTED_OVERLAY,
+ 0 /* inputFeatures */,
TYPE_APPLICATION,
+ null /* windowToken */,
mFocusGrantToken,
TAG + " of " + decorationSurface.toString(),
mInputChannel);
@@ -126,78 +133,85 @@ class DragResizeInputListener implements AutoCloseable {
* This is also used to update the touch regions of this handler every event dispatched here is
* a potential resize request.
*
- * @param width The width of the drag resize handler in pixels, including resize handle
- * thickness. That is task width + 2 * resize handle thickness.
- * @param height The height of the drag resize handler in pixels, including resize handle
- * thickness. That is task height + 2 * resize handle thickness.
+ * @param taskWidth The width of the task.
+ * @param taskHeight The height of the task.
* @param resizeHandleThickness The thickness of the resize handle in pixels.
* @param cornerSize The size of the resize handle centered in each corner.
* @param touchSlop The distance in pixels user has to drag with touch for it to register as
* a resize action.
+ * @return whether the geometry has changed or not
*/
- void setGeometry(int width, int height, int resizeHandleThickness, int cornerSize,
+ boolean setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize,
int touchSlop) {
- if (mWidth == width && mHeight == height
+ if (mTaskWidth == taskWidth && mTaskHeight == taskHeight
&& mResizeHandleThickness == resizeHandleThickness
&& mCornerSize == cornerSize) {
- return;
+ return false;
}
- mWidth = width;
- mHeight = height;
+ mTaskWidth = taskWidth;
+ mTaskHeight = taskHeight;
mResizeHandleThickness = resizeHandleThickness;
mCornerSize = cornerSize;
mDragDetector.setTouchSlop(touchSlop);
Region touchRegion = new Region();
- final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness);
+ final Rect topInputBounds = new Rect(
+ -mResizeHandleThickness,
+ -mResizeHandleThickness,
+ mTaskWidth + mResizeHandleThickness,
+ 0);
touchRegion.union(topInputBounds);
- final Rect leftInputBounds = new Rect(0, mResizeHandleThickness,
- mResizeHandleThickness, mHeight - mResizeHandleThickness);
+ final Rect leftInputBounds = new Rect(
+ -mResizeHandleThickness,
+ 0,
+ 0,
+ mTaskHeight);
touchRegion.union(leftInputBounds);
final Rect rightInputBounds = new Rect(
- mWidth - mResizeHandleThickness, mResizeHandleThickness,
- mWidth, mHeight - mResizeHandleThickness);
+ mTaskWidth,
+ 0,
+ mTaskWidth + mResizeHandleThickness,
+ mTaskHeight);
touchRegion.union(rightInputBounds);
- final Rect bottomInputBounds = new Rect(0, mHeight - mResizeHandleThickness,
- mWidth, mHeight);
+ final Rect bottomInputBounds = new Rect(
+ -mResizeHandleThickness,
+ mTaskHeight,
+ mTaskWidth + mResizeHandleThickness,
+ mTaskHeight + mResizeHandleThickness);
touchRegion.union(bottomInputBounds);
// Set up touch areas in each corner.
int cornerRadius = mCornerSize / 2;
mLeftTopCornerBounds = new Rect(
- mResizeHandleThickness - cornerRadius,
- mResizeHandleThickness - cornerRadius,
- mResizeHandleThickness + cornerRadius,
- mResizeHandleThickness + cornerRadius
- );
+ -cornerRadius,
+ -cornerRadius,
+ cornerRadius,
+ cornerRadius);
touchRegion.union(mLeftTopCornerBounds);
mRightTopCornerBounds = new Rect(
- mWidth - mResizeHandleThickness - cornerRadius,
- mResizeHandleThickness - cornerRadius,
- mWidth - mResizeHandleThickness + cornerRadius,
- mResizeHandleThickness + cornerRadius
- );
+ mTaskWidth - cornerRadius,
+ -cornerRadius,
+ mTaskWidth + cornerRadius,
+ cornerRadius);
touchRegion.union(mRightTopCornerBounds);
mLeftBottomCornerBounds = new Rect(
- mResizeHandleThickness - cornerRadius,
- mHeight - mResizeHandleThickness - cornerRadius,
- mResizeHandleThickness + cornerRadius,
- mHeight - mResizeHandleThickness + cornerRadius
- );
+ -cornerRadius,
+ mTaskHeight - cornerRadius,
+ cornerRadius,
+ mTaskHeight + cornerRadius);
touchRegion.union(mLeftBottomCornerBounds);
mRightBottomCornerBounds = new Rect(
- mWidth - mResizeHandleThickness - cornerRadius,
- mHeight - mResizeHandleThickness - cornerRadius,
- mWidth - mResizeHandleThickness + cornerRadius,
- mHeight - mResizeHandleThickness + cornerRadius
- );
+ mTaskWidth - cornerRadius,
+ mTaskHeight - cornerRadius,
+ mTaskWidth + cornerRadius,
+ mTaskHeight + cornerRadius);
touchRegion.union(mRightBottomCornerBounds);
try {
@@ -207,10 +221,24 @@ class DragResizeInputListener implements AutoCloseable {
mDecorationSurface,
FLAG_NOT_FOCUSABLE,
PRIVATE_FLAG_TRUSTED_OVERLAY,
+ 0 /* inputFeatures */,
touchRegion);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
+ return true;
+ }
+
+ /**
+ * Generate a Region that encapsulates all 4 corner handles
+ */
+ Region getCornersRegion() {
+ Region region = new Region();
+ region.union(mLeftTopCornerBounds);
+ region.union(mLeftBottomCornerBounds);
+ region.union(mRightTopCornerBounds);
+ region.union(mRightBottomCornerBounds);
+ return region;
}
@Override
@@ -344,7 +372,7 @@ class DragResizeInputListener implements AutoCloseable {
return calculateResizeHandlesCtrlType(x, y) != 0;
}
- @TaskPositioner.CtrlType
+ @DragPositioningCallback.CtrlType
private int calculateCtrlType(boolean isTouch, float x, float y) {
if (isTouch) {
return calculateCornersCtrlType(x, y);
@@ -352,62 +380,62 @@ class DragResizeInputListener implements AutoCloseable {
return calculateResizeHandlesCtrlType(x, y);
}
- @TaskPositioner.CtrlType
+ @DragPositioningCallback.CtrlType
private int calculateResizeHandlesCtrlType(float x, float y) {
int ctrlType = 0;
- if (x < mResizeHandleThickness) {
- ctrlType |= TaskPositioner.CTRL_TYPE_LEFT;
+ if (x < 0) {
+ ctrlType |= CTRL_TYPE_LEFT;
}
- if (x > mWidth - mResizeHandleThickness) {
- ctrlType |= TaskPositioner.CTRL_TYPE_RIGHT;
+ if (x > mTaskWidth) {
+ ctrlType |= CTRL_TYPE_RIGHT;
}
- if (y < mResizeHandleThickness) {
- ctrlType |= TaskPositioner.CTRL_TYPE_TOP;
+ if (y < 0) {
+ ctrlType |= CTRL_TYPE_TOP;
}
- if (y > mHeight - mResizeHandleThickness) {
- ctrlType |= TaskPositioner.CTRL_TYPE_BOTTOM;
+ if (y > mTaskHeight) {
+ ctrlType |= CTRL_TYPE_BOTTOM;
}
return ctrlType;
}
- @TaskPositioner.CtrlType
+ @DragPositioningCallback.CtrlType
private int calculateCornersCtrlType(float x, float y) {
int xi = (int) x;
int yi = (int) y;
if (mLeftTopCornerBounds.contains(xi, yi)) {
- return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP;
+ return CTRL_TYPE_LEFT | CTRL_TYPE_TOP;
}
if (mLeftBottomCornerBounds.contains(xi, yi)) {
- return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM;
+ return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM;
}
if (mRightTopCornerBounds.contains(xi, yi)) {
- return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP;
+ return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP;
}
if (mRightBottomCornerBounds.contains(xi, yi)) {
- return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM;
+ return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM;
}
return 0;
}
private void updateCursorType(float x, float y) {
- @TaskPositioner.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y);
+ @DragPositioningCallback.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y);
int cursorType = PointerIcon.TYPE_DEFAULT;
switch (ctrlType) {
- case TaskPositioner.CTRL_TYPE_LEFT:
- case TaskPositioner.CTRL_TYPE_RIGHT:
+ case CTRL_TYPE_LEFT:
+ case CTRL_TYPE_RIGHT:
cursorType = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
break;
- case TaskPositioner.CTRL_TYPE_TOP:
- case TaskPositioner.CTRL_TYPE_BOTTOM:
+ case CTRL_TYPE_TOP:
+ case CTRL_TYPE_BOTTOM:
cursorType = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
break;
- case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP:
- case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM:
+ case CTRL_TYPE_LEFT | CTRL_TYPE_TOP:
+ case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM:
cursorType = PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
break;
- case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM:
- case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP:
+ case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM:
+ case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP:
cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
break;
}
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
new file mode 100644
index 000000000000..9082323452c9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -0,0 +1,143 @@
+/*
+ * 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.windowdecor;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+
+import java.util.function.Supplier;
+
+/**
+ * A task positioner that resizes/relocates task contents as it is dragged.
+ * Utilizes {@link DragPositioningCallbackUtility} to determine new task bounds.
+ */
+class FluidResizeTaskPositioner implements DragPositioningCallback {
+ private final ShellTaskOrganizer mTaskOrganizer;
+ private final WindowDecoration mWindowDecoration;
+ private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+ private DisplayController mDisplayController;
+ private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+ private final Rect mStableBounds = new Rect();
+ private final Rect mTaskBoundsAtDragStart = new Rect();
+ private final PointF mRepositionStartPoint = new PointF();
+ private final Rect mRepositionTaskBounds = new Rect();
+ // If a task move (not resize) finishes in this region, the positioner will not attempt to
+ // finalize the bounds there using WCT#setBounds
+ private final Rect mDisallowedAreaForEndBounds;
+ private boolean mHasDragResized;
+ private int mCtrlType;
+
+ FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
+ DisplayController displayController, @Nullable Rect disallowedAreaForEndBounds) {
+ this(taskOrganizer, windowDecoration, displayController, disallowedAreaForEndBounds,
+ dragStartListener -> {}, SurfaceControl.Transaction::new);
+ }
+
+ FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
+ DisplayController displayController, @Nullable Rect disallowedAreaForEndBounds,
+ DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ Supplier<SurfaceControl.Transaction> supplier) {
+ mTaskOrganizer = taskOrganizer;
+ mWindowDecoration = windowDecoration;
+ mDisplayController = displayController;
+ mDisallowedAreaForEndBounds = new Rect(disallowedAreaForEndBounds);
+ mDragStartListener = dragStartListener;
+ mTransactionSupplier = supplier;
+ }
+
+ @Override
+ public void onDragPositioningStart(int ctrlType, float x, float y) {
+ mCtrlType = ctrlType;
+ mTaskBoundsAtDragStart.set(
+ mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
+ mRepositionStartPoint.set(x, y);
+ mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
+ if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mTaskInfo.isFocused) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(mWindowDecoration.mTaskInfo.token, true);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
+ }
+
+ @Override
+ public void onDragPositioningMove(float x, float y) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint);
+ if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType,
+ mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
+ mDisplayController, mWindowDecoration)) {
+ // The task is being resized, send the |dragResizing| hint to core with the first
+ // bounds-change wct.
+ if (!mHasDragResized) {
+ // This is the first bounds change since drag resize operation started.
+ wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
+ }
+ DragPositioningCallbackUtility.applyTaskBoundsChange(wct, mWindowDecoration,
+ mRepositionTaskBounds, mTaskOrganizer);
+ mHasDragResized = true;
+ } else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
+ final SurfaceControl.Transaction t = mTransactionSupplier.get();
+ DragPositioningCallbackUtility.setPositionOnDrag(mWindowDecoration,
+ mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t, x, y);
+ t.apply();
+ }
+ }
+
+ @Override
+ public void onDragPositioningEnd(float x, float y) {
+ // If task has been resized or task was dragged into area outside of
+ // mDisallowedAreaForEndBounds, apply WCT to finish it.
+ if (isResizing() && mHasDragResized) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
+ PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y,
+ mRepositionStartPoint);
+ if (DragPositioningCallbackUtility.changeBounds(mCtrlType, mRepositionTaskBounds,
+ mTaskBoundsAtDragStart, mStableBounds, delta, mDisplayController,
+ mWindowDecoration)) {
+ wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+ }
+ mTaskOrganizer.applyTransaction(wct);
+ } else if (mCtrlType == CTRL_TYPE_UNDEFINED
+ && !mDisallowedAreaForEndBounds.contains((int) x, (int) y)) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
+ mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
+ wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+
+ mTaskBoundsAtDragStart.setEmpty();
+ mRepositionStartPoint.set(0, 0);
+ mCtrlType = CTRL_TYPE_UNDEFINED;
+ mHasDragResized = false;
+ }
+
+ private boolean isResizing() {
+ return (mCtrlType & CTRL_TYPE_TOP) != 0 || (mCtrlType & CTRL_TYPE_BOTTOM) != 0
+ || (mCtrlType & CTRL_TYPE_LEFT) != 0 || (mCtrlType & CTRL_TYPE_RIGHT) != 0;
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
new file mode 100644
index 000000000000..ac4a597c15d1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
@@ -0,0 +1,404 @@
+/*
+ * 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.windowdecor;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.drawable.Drawable;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.window.SurfaceSyncGroup;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
+
+/**
+ * Handle menu opened when the appropriate button is clicked on.
+ *
+ * Displays up to 3 pills that show the following:
+ * App Info: App name, app icon, and collapse button to close the menu.
+ * Windowing Options(Proto 2 only): Buttons to change windowing modes.
+ * Additional Options: Miscellaneous functions including screenshot and closing task.
+ */
+class HandleMenu {
+ private static final String TAG = "HandleMenu";
+ private final Context mContext;
+ private final WindowDecoration mParentDecor;
+ private WindowDecoration.AdditionalWindow mAppInfoPill;
+ private WindowDecoration.AdditionalWindow mWindowingPill;
+ private WindowDecoration.AdditionalWindow mMoreActionsPill;
+ private final PointF mAppInfoPillPosition = new PointF();
+ private final PointF mWindowingPillPosition = new PointF();
+ private final PointF mMoreActionsPillPosition = new PointF();
+ private final boolean mShouldShowWindowingPill;
+ private final Drawable mAppIcon;
+ private final CharSequence mAppName;
+ private final View.OnClickListener mOnClickListener;
+ private final View.OnTouchListener mOnTouchListener;
+ private final RunningTaskInfo mTaskInfo;
+ private final int mLayoutResId;
+ private final int mCaptionX;
+ private final int mCaptionY;
+ private int mMarginMenuTop;
+ private int mMarginMenuStart;
+ private int mMarginMenuSpacing;
+ private int mMenuWidth;
+ private int mAppInfoPillHeight;
+ private int mWindowingPillHeight;
+ private int mMoreActionsPillHeight;
+ private int mShadowRadius;
+ private int mCornerRadius;
+
+
+ HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY,
+ View.OnClickListener onClickListener, View.OnTouchListener onTouchListener,
+ Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill) {
+ mParentDecor = parentDecor;
+ mContext = mParentDecor.mDecorWindowContext;
+ mTaskInfo = mParentDecor.mTaskInfo;
+ mLayoutResId = layoutResId;
+ mCaptionX = captionX;
+ mCaptionY = captionY;
+ mOnClickListener = onClickListener;
+ mOnTouchListener = onTouchListener;
+ mAppIcon = appIcon;
+ mAppName = appName;
+ mShouldShowWindowingPill = shouldShowWindowingPill;
+ loadHandleMenuDimensions();
+ updateHandleMenuPillPositions();
+ }
+
+ void show() {
+ final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG);
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+
+ createAppInfoPill(t, ssg);
+ if (mShouldShowWindowingPill) {
+ createWindowingPill(t, ssg);
+ }
+ createMoreActionsPill(t, ssg);
+ ssg.addTransaction(t);
+ ssg.markSyncReady();
+ setupHandleMenu();
+ }
+
+ private void createAppInfoPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
+ final int x = (int) mAppInfoPillPosition.x;
+ final int y = (int) mAppInfoPillPosition.y;
+ mAppInfoPill = mParentDecor.addWindow(
+ R.layout.desktop_mode_window_decor_handle_menu_app_info_pill,
+ "Menu's app info pill",
+ t, ssg, x, y, mMenuWidth, mAppInfoPillHeight, mShadowRadius, mCornerRadius);
+ }
+
+ private void createWindowingPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
+ final int x = (int) mWindowingPillPosition.x;
+ final int y = (int) mWindowingPillPosition.y;
+ mWindowingPill = mParentDecor.addWindow(
+ R.layout.desktop_mode_window_decor_handle_menu_windowing_pill,
+ "Menu's windowing pill",
+ t, ssg, x, y, mMenuWidth, mWindowingPillHeight, mShadowRadius, mCornerRadius);
+ }
+
+ private void createMoreActionsPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
+ final int x = (int) mMoreActionsPillPosition.x;
+ final int y = (int) mMoreActionsPillPosition.y;
+ mMoreActionsPill = mParentDecor.addWindow(
+ R.layout.desktop_mode_window_decor_handle_menu_more_actions_pill,
+ "Menu's more actions pill",
+ t, ssg, x, y, mMenuWidth, mMoreActionsPillHeight, mShadowRadius, mCornerRadius);
+ }
+
+ /**
+ * Set up interactive elements and color of this handle menu
+ */
+ private void setupHandleMenu() {
+ // App Info pill setup.
+ final View appInfoPillView = mAppInfoPill.mWindowViewHost.getView();
+ final ImageButton collapseBtn = appInfoPillView.findViewById(R.id.collapse_menu_button);
+ final ImageView appIcon = appInfoPillView.findViewById(R.id.application_icon);
+ final TextView appName = appInfoPillView.findViewById(R.id.application_name);
+ collapseBtn.setOnClickListener(mOnClickListener);
+ appInfoPillView.setOnTouchListener(mOnTouchListener);
+ appIcon.setImageDrawable(mAppIcon);
+ appName.setText(mAppName);
+
+ // Windowing pill setup.
+ if (mShouldShowWindowingPill) {
+ final View windowingPillView = mWindowingPill.mWindowViewHost.getView();
+ final ImageButton fullscreenBtn = windowingPillView.findViewById(
+ R.id.fullscreen_button);
+ final ImageButton splitscreenBtn = windowingPillView.findViewById(
+ R.id.split_screen_button);
+ final ImageButton floatingBtn = windowingPillView.findViewById(R.id.floating_button);
+ final ImageButton desktopBtn = windowingPillView.findViewById(R.id.desktop_button);
+ fullscreenBtn.setOnClickListener(mOnClickListener);
+ splitscreenBtn.setOnClickListener(mOnClickListener);
+ floatingBtn.setOnClickListener(mOnClickListener);
+ desktopBtn.setOnClickListener(mOnClickListener);
+ // The button corresponding to the windowing mode that the task is currently in uses a
+ // different color than the others.
+ final ColorStateList activeColorStateList = ColorStateList.valueOf(
+ mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_active));
+ final ColorStateList inActiveColorStateList = ColorStateList.valueOf(
+ mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_inactive));
+ fullscreenBtn.setImageTintList(
+ mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+ ? activeColorStateList : inActiveColorStateList);
+ splitscreenBtn.setImageTintList(
+ mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
+ ? activeColorStateList : inActiveColorStateList);
+ floatingBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_PINNED
+ ? activeColorStateList : inActiveColorStateList);
+ desktopBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
+ ? activeColorStateList : inActiveColorStateList);
+ }
+
+ // More Actions pill setup.
+ final View moreActionsPillView = mMoreActionsPill.mWindowViewHost.getView();
+ final Button closeBtn = moreActionsPillView.findViewById(R.id.close_button);
+ closeBtn.setOnClickListener(mOnClickListener);
+ final Button selectBtn = moreActionsPillView.findViewById(R.id.select_button);
+ selectBtn.setOnClickListener(mOnClickListener);
+ }
+
+ /**
+ * Updates the handle menu pills' position variables to reflect their next positions
+ */
+ private void updateHandleMenuPillPositions() {
+ final int menuX, menuY;
+ final int captionWidth = mTaskInfo.getConfiguration()
+ .windowConfiguration.getBounds().width();
+ if (mLayoutResId
+ == R.layout.desktop_mode_app_controls_window_decor) {
+ // Align the handle menu to the left of the caption.
+ menuX = mCaptionX + mMarginMenuStart;
+ menuY = mCaptionY + mMarginMenuTop;
+ } else {
+ // Position the handle menu at the center of the caption.
+ menuX = mCaptionX + (captionWidth / 2) - (mMenuWidth / 2);
+ menuY = mCaptionY + mMarginMenuStart;
+ }
+
+ // App Info pill setup.
+ final int appInfoPillY = menuY;
+ mAppInfoPillPosition.set(menuX, appInfoPillY);
+
+ final int windowingPillY, moreActionsPillY;
+ if (mShouldShowWindowingPill) {
+ windowingPillY = appInfoPillY + mAppInfoPillHeight + mMarginMenuSpacing;
+ mWindowingPillPosition.set(menuX, windowingPillY);
+ moreActionsPillY = windowingPillY + mWindowingPillHeight + mMarginMenuSpacing;
+ mMoreActionsPillPosition.set(menuX, moreActionsPillY);
+ } else {
+ // Just start after the end of the app info pill + margins.
+ moreActionsPillY = appInfoPillY + mAppInfoPillHeight + mMarginMenuSpacing;
+ mMoreActionsPillPosition.set(menuX, moreActionsPillY);
+ }
+ }
+
+ /**
+ * Update pill layout, in case task changes have caused positioning to change.
+ * @param t
+ */
+ void relayout(SurfaceControl.Transaction t) {
+ if (mAppInfoPill != null) {
+ updateHandleMenuPillPositions();
+ t.setPosition(mAppInfoPill.mWindowSurface,
+ mAppInfoPillPosition.x, mAppInfoPillPosition.y);
+ // Only show windowing buttons in proto2. Proto1 uses a system-level mode only.
+ final boolean shouldShowWindowingPill = DesktopModeStatus.isProto2Enabled();
+ if (shouldShowWindowingPill) {
+ t.setPosition(mWindowingPill.mWindowSurface,
+ mWindowingPillPosition.x, mWindowingPillPosition.y);
+ }
+ t.setPosition(mMoreActionsPill.mWindowSurface,
+ mMoreActionsPillPosition.x, mMoreActionsPillPosition.y);
+ }
+ }
+ /**
+ * Check a passed MotionEvent if a click has occurred on any button on this caption
+ * Note this should only be called when a regular onClick is not possible
+ * (i.e. the button was clicked through status bar layer)
+ * @param ev the MotionEvent to compare against.
+ */
+ void checkClickEvent(MotionEvent ev) {
+ final View appInfoPill = mAppInfoPill.mWindowViewHost.getView();
+ final ImageButton collapse = appInfoPill.findViewById(R.id.collapse_menu_button);
+ // Translate the input point from display coordinates to the same space as the collapse
+ // button, meaning its parent (app info pill view).
+ final PointF inputPoint = new PointF(ev.getX() - mAppInfoPillPosition.x,
+ ev.getY() - mAppInfoPillPosition.y);
+ if (pointInView(collapse, inputPoint.x, inputPoint.y)) {
+ mOnClickListener.onClick(collapse);
+ }
+ }
+
+ /**
+ * A valid menu input is one of the following:
+ * An input that happens in the menu views.
+ * Any input before the views have been laid out.
+ * @param inputPoint the input to compare against.
+ */
+ boolean isValidMenuInput(PointF inputPoint) {
+ if (!viewsLaidOut()) return true;
+ final boolean pointInAppInfoPill = pointInView(
+ mAppInfoPill.mWindowViewHost.getView(),
+ inputPoint.x - mAppInfoPillPosition.x,
+ inputPoint.y - mAppInfoPillPosition.y);
+ boolean pointInWindowingPill = false;
+ if (mWindowingPill != null) {
+ pointInWindowingPill = pointInView(
+ mWindowingPill.mWindowViewHost.getView(),
+ inputPoint.x - mWindowingPillPosition.x,
+ inputPoint.y - mWindowingPillPosition.y);
+ }
+ final boolean pointInMoreActionsPill = pointInView(
+ mMoreActionsPill.mWindowViewHost.getView(),
+ inputPoint.x - mMoreActionsPillPosition.x,
+ inputPoint.y - mMoreActionsPillPosition.y);
+
+ return pointInAppInfoPill || pointInWindowingPill || pointInMoreActionsPill;
+ }
+
+ private boolean pointInView(View v, float x, float y) {
+ return v != null && v.getLeft() <= x && v.getRight() >= x
+ && v.getTop() <= y && v.getBottom() >= y;
+ }
+
+ /**
+ * Check if the views for handle menu can be seen.
+ * @return
+ */
+ private boolean viewsLaidOut() {
+ return mAppInfoPill.mWindowViewHost.getView().isLaidOut();
+ }
+
+
+ private void loadHandleMenuDimensions() {
+ final Resources resources = mContext.getResources();
+ mMenuWidth = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_width);
+ mMarginMenuTop = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_margin_top);
+ mMarginMenuStart = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_margin_start);
+ mMarginMenuSpacing = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_pill_spacing_margin);
+ mAppInfoPillHeight = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_app_info_pill_height);
+ mWindowingPillHeight = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_windowing_pill_height);
+ mMoreActionsPillHeight = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_more_actions_pill_height);
+ mShadowRadius = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_shadow_radius);
+ mCornerRadius = loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_corner_radius);
+ }
+
+ private int loadDimensionPixelSize(Resources resources, int resourceId) {
+ if (resourceId == Resources.ID_NULL) {
+ return 0;
+ }
+ return resources.getDimensionPixelSize(resourceId);
+ }
+
+ void close() {
+ mAppInfoPill.releaseView();
+ mAppInfoPill = null;
+ if (mWindowingPill != null) {
+ mWindowingPill.releaseView();
+ mWindowingPill = null;
+ }
+ mMoreActionsPill.releaseView();
+ mMoreActionsPill = null;
+ }
+
+ static final class Builder {
+ private final WindowDecoration mParent;
+ private CharSequence mName;
+ private Drawable mAppIcon;
+ private View.OnClickListener mOnClickListener;
+ private View.OnTouchListener mOnTouchListener;
+ private int mLayoutId;
+ private int mCaptionX;
+ private int mCaptionY;
+ private boolean mShowWindowingPill;
+
+
+ Builder(@NonNull WindowDecoration parent) {
+ mParent = parent;
+ }
+
+ Builder setAppName(@Nullable CharSequence name) {
+ mName = name;
+ return this;
+ }
+
+ Builder setAppIcon(@Nullable Drawable appIcon) {
+ mAppIcon = appIcon;
+ return this;
+ }
+
+ Builder setOnClickListener(@Nullable View.OnClickListener onClickListener) {
+ mOnClickListener = onClickListener;
+ return this;
+ }
+
+ Builder setOnTouchListener(@Nullable View.OnTouchListener onTouchListener) {
+ mOnTouchListener = onTouchListener;
+ return this;
+ }
+
+ Builder setLayoutId(int layoutId) {
+ mLayoutId = layoutId;
+ return this;
+ }
+
+ Builder setCaptionPosition(int captionX, int captionY) {
+ mCaptionX = captionX;
+ mCaptionY = captionY;
+ return this;
+ }
+
+ Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) {
+ mShowWindowingPill = windowingButtonsVisible;
+ return this;
+ }
+
+ HandleMenu build() {
+ return new HandleMenu(mParent, mLayoutId, mCaptionX, mCaptionY, mOnClickListener,
+ mOnTouchListener, mAppIcon, mName, mShowWindowingPill);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
new file mode 100644
index 000000000000..82771095cd82
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
@@ -0,0 +1,203 @@
+/*
+ * 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.windowdecor;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.ColorRes;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.widget.ImageView;
+import android.window.TaskConstants;
+
+import com.android.wm.shell.R;
+
+import java.util.function.Supplier;
+
+/**
+ * Creates and updates a veil that covers task contents on resize.
+ */
+public class ResizeVeil {
+ private static final int RESIZE_ALPHA_DURATION = 100;
+ private final Context mContext;
+ private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
+ private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
+ private final Drawable mAppIcon;
+ private SurfaceControl mParentSurface;
+ private SurfaceControl mVeilSurface;
+ private final RunningTaskInfo mTaskInfo;
+ private SurfaceControlViewHost mViewHost;
+ private final Display mDisplay;
+
+ public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo,
+ Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display,
+ Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) {
+ mContext = context;
+ mAppIcon = appIcon;
+ mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
+ mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
+ mTaskInfo = taskInfo;
+ mDisplay = display;
+ setupResizeVeil();
+ }
+
+ /**
+ * Create the veil in its default invisible state.
+ */
+ private void setupResizeVeil() {
+ SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
+ mVeilSurface = builder
+ .setName("Resize veil of Task= " + mTaskInfo.taskId)
+ .setContainerLayer()
+ .build();
+ View v = LayoutInflater.from(mContext)
+ .inflate(R.layout.desktop_mode_resize_veil, null);
+
+ t.setPosition(mVeilSurface, 0, 0)
+ .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL)
+ .apply();
+ Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
+ final WindowManager.LayoutParams lp =
+ new WindowManager.LayoutParams(taskBounds.width(),
+ taskBounds.height(),
+ WindowManager.LayoutParams.TYPE_APPLICATION,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSPARENT);
+ lp.setTitle("Resize veil of Task=" + mTaskInfo.taskId);
+ lp.setTrustedOverlay();
+ WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration,
+ mVeilSurface, null /* hostInputToken */);
+ mViewHost = new SurfaceControlViewHost(mContext, mDisplay, windowManager, "ResizeVeil");
+ mViewHost.setView(v, lp);
+
+ final ImageView appIcon = mViewHost.getView().findViewById(R.id.veil_application_icon);
+ appIcon.setImageDrawable(mAppIcon);
+ }
+
+ /**
+ * Animate veil's alpha to 1, fading it in.
+ */
+ public void showVeil(SurfaceControl parentSurface, Rect taskBounds) {
+ // Parent surface can change, ensure it is up to date.
+ SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ if (!parentSurface.equals(mParentSurface)) {
+ t.reparent(mVeilSurface, parentSurface);
+ mParentSurface = parentSurface;
+ }
+
+ int backgroundColorId = getBackgroundColorId();
+ mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId));
+
+ final ValueAnimator animator = new ValueAnimator();
+ animator.setFloatValues(0f, 1f);
+ animator.setDuration(RESIZE_ALPHA_DURATION);
+ animator.addUpdateListener(animation -> {
+ t.setAlpha(mVeilSurface, animator.getAnimatedFraction());
+ t.apply();
+ });
+
+ relayout(taskBounds, t);
+ t.show(mVeilSurface)
+ .addTransactionCommittedListener(mContext.getMainExecutor(), () -> animator.start())
+ .setAlpha(mVeilSurface, 0);
+ mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
+ }
+
+ /**
+ * Update veil bounds to match bounds changes.
+ * @param newBounds bounds to update veil to.
+ */
+ private void relayout(Rect newBounds, SurfaceControl.Transaction t) {
+ mViewHost.relayout(newBounds.width(), newBounds.height());
+ t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height());
+ t.setPosition(mParentSurface, newBounds.left, newBounds.top);
+ t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height());
+ }
+
+ /**
+ * Calls relayout to update task and veil bounds.
+ * @param newBounds bounds to update veil to.
+ */
+ public void updateResizeVeil(Rect newBounds) {
+ SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ relayout(newBounds, t);
+ mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
+ }
+
+ /**
+ * Animate veil's alpha to 0, fading it out.
+ */
+ public void hideVeil() {
+ final ValueAnimator animator = new ValueAnimator();
+ animator.setFloatValues(1, 0);
+ animator.setDuration(RESIZE_ALPHA_DURATION);
+ animator.addUpdateListener(animation -> {
+ SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ t.setAlpha(mVeilSurface, 1 - animator.getAnimatedFraction());
+ t.apply();
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ t.hide(mVeilSurface);
+ t.apply();
+ }
+ });
+ animator.start();
+ }
+
+ @ColorRes
+ private int getBackgroundColorId() {
+ Configuration configuration = mContext.getResources().getConfiguration();
+ if ((configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ == Configuration.UI_MODE_NIGHT_YES) {
+ return R.color.desktop_mode_resize_veil_dark;
+ } else {
+ return R.color.desktop_mode_resize_veil_light;
+ }
+ }
+
+ /**
+ * Dispose of veil when it is no longer needed, likely on close of its container decor.
+ */
+ void dispose() {
+ if (mViewHost != null) {
+ mViewHost.release();
+ mViewHost = null;
+ }
+ if (mVeilSurface != null) {
+ final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ t.remove(mVeilSurface);
+ mVeilSurface = null;
+ t.apply();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
index aea340464304..d0fcd8651481 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
@@ -65,7 +65,7 @@ class TaskOperations {
InputDevice.SOURCE_KEYBOARD);
ev.setDisplayId(mContext.getDisplay().getDisplayId());
- if (!InputManager.getInstance()
+ if (!mContext.getSystemService(InputManager.class)
.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
Log.e(TAG, "Inject input event fail");
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
deleted file mode 100644
index 0bce3acecb3c..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.windowdecor;
-
-import android.annotation.IntDef;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.util.DisplayMetrics;
-import android.window.WindowContainerTransaction;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayController;
-
-class TaskPositioner implements DragPositioningCallback {
-
- @IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM})
- @interface CtrlType {}
-
- static final int CTRL_TYPE_UNDEFINED = 0;
- static final int CTRL_TYPE_LEFT = 1;
- static final int CTRL_TYPE_RIGHT = 2;
- static final int CTRL_TYPE_TOP = 4;
- static final int CTRL_TYPE_BOTTOM = 8;
-
- private final ShellTaskOrganizer mTaskOrganizer;
- private final DisplayController mDisplayController;
- private final WindowDecoration mWindowDecoration;
-
- private final Rect mTempBounds = new Rect();
- private final Rect mTaskBoundsAtDragStart = new Rect();
- private final PointF mRepositionStartPoint = new PointF();
- private final Rect mRepositionTaskBounds = new Rect();
- private boolean mHasMoved = false;
-
- private int mCtrlType;
- private DragStartListener mDragStartListener;
-
- TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
- DisplayController displayController) {
- this(taskOrganizer, windowDecoration, displayController, dragStartListener -> {});
- }
-
- TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
- DisplayController displayController, DragStartListener dragStartListener) {
- mTaskOrganizer = taskOrganizer;
- mWindowDecoration = windowDecoration;
- mDisplayController = displayController;
- mDragStartListener = dragStartListener;
- }
-
- @Override
- public void onDragPositioningStart(int ctrlType, float x, float y) {
- mHasMoved = false;
-
- mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
- mCtrlType = ctrlType;
-
- mTaskBoundsAtDragStart.set(
- mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
- mRepositionStartPoint.set(x, y);
- }
-
- @Override
- public void onDragPositioningMove(float x, float y) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- if (changeBounds(wct, x, y)) {
- // The task is being resized, send the |dragResizing| hint to core with the first
- // bounds-change wct.
- if (!mHasMoved && mCtrlType != CTRL_TYPE_UNDEFINED) {
- // This is the first bounds change since drag resize operation started.
- wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
- }
- mTaskOrganizer.applyTransaction(wct);
- mHasMoved = true;
- }
- }
-
- @Override
- public void onDragPositioningEnd(float x, float y) {
- // |mHasMoved| being false means there is no real change to the task bounds in WM core, so
- // we don't need a WCT to finish it.
- if (mHasMoved) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
- changeBounds(wct, x, y);
- mTaskOrganizer.applyTransaction(wct);
- }
-
- mCtrlType = CTRL_TYPE_UNDEFINED;
- mTaskBoundsAtDragStart.setEmpty();
- mRepositionStartPoint.set(0, 0);
- mHasMoved = false;
- }
-
- private boolean changeBounds(WindowContainerTransaction wct, float x, float y) {
- // |mRepositionTaskBounds| is the bounds last reported if |mHasMoved| is true. If it's not
- // true, we can compare it against |mTaskBoundsAtDragStart|.
- final int oldLeft = mHasMoved ? mRepositionTaskBounds.left : mTaskBoundsAtDragStart.left;
- final int oldTop = mHasMoved ? mRepositionTaskBounds.top : mTaskBoundsAtDragStart.top;
- final int oldRight = mHasMoved ? mRepositionTaskBounds.right : mTaskBoundsAtDragStart.right;
- final int oldBottom =
- mHasMoved ? mRepositionTaskBounds.bottom : mTaskBoundsAtDragStart.bottom;
-
- final float deltaX = x - mRepositionStartPoint.x;
- final float deltaY = y - mRepositionStartPoint.y;
- mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
-
- final Rect stableBounds = mTempBounds;
- // Make sure the new resizing destination in any direction falls within the stable bounds.
- // If not, set the bounds back to the old location that was valid to avoid conflicts with
- // some regions such as the gesture area.
- mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId())
- .getStableBounds(stableBounds);
- if ((mCtrlType & CTRL_TYPE_LEFT) != 0) {
- final int candidateLeft = mRepositionTaskBounds.left + (int) deltaX;
- mRepositionTaskBounds.left = (candidateLeft > stableBounds.left)
- ? candidateLeft : oldLeft;
- }
- if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) {
- final int candidateRight = mRepositionTaskBounds.right + (int) deltaX;
- mRepositionTaskBounds.right = (candidateRight < stableBounds.right)
- ? candidateRight : oldRight;
- }
- if ((mCtrlType & CTRL_TYPE_TOP) != 0) {
- final int candidateTop = mRepositionTaskBounds.top + (int) deltaY;
- mRepositionTaskBounds.top = (candidateTop > stableBounds.top)
- ? candidateTop : oldTop;
- }
- if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) {
- final int candidateBottom = mRepositionTaskBounds.bottom + (int) deltaY;
- mRepositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom)
- ? candidateBottom : oldBottom;
- }
- if (mCtrlType == CTRL_TYPE_UNDEFINED) {
- mRepositionTaskBounds.offset((int) deltaX, (int) deltaY);
- }
-
- // If width or height are negative or less than the minimum width or height, revert the
- // respective bounds to use previous bound dimensions.
- if (mRepositionTaskBounds.width() < getMinWidth()) {
- mRepositionTaskBounds.right = oldRight;
- mRepositionTaskBounds.left = oldLeft;
- }
- if (mRepositionTaskBounds.height() < getMinHeight()) {
- mRepositionTaskBounds.top = oldTop;
- mRepositionTaskBounds.bottom = oldBottom;
- }
- // If there are no changes to the bounds after checking new bounds against minimum width
- // and height, do not set bounds and return false
- if (oldLeft == mRepositionTaskBounds.left && oldTop == mRepositionTaskBounds.top
- && oldRight == mRepositionTaskBounds.right
- && oldBottom == mRepositionTaskBounds.bottom) {
- return false;
- }
-
- wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
- return true;
- }
-
- private float getMinWidth() {
- return mWindowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize()
- : mWindowDecoration.mTaskInfo.minWidth;
- }
-
- private float getMinHeight() {
- return mWindowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize()
- : mWindowDecoration.mTaskInfo.minHeight;
- }
-
- private float getDefaultMinSize() {
- float density = mDisplayController.getDisplayLayout(mWindowDecoration.mTaskInfo.displayId)
- .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE;
- return mWindowDecoration.mTaskInfo.defaultMinSize * density;
- }
-
- interface DragStartListener {
- /**
- * Inform the implementing class that a drag resize has started
- * @param taskId id of this positioner's {@link WindowDecoration}
- */
- void onDragStart(int taskId);
- }
-}
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
new file mode 100644
index 000000000000..58c78e6a5b9f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -0,0 +1,180 @@
+/*
+ * 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.windowdecor;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.function.Supplier;
+
+/**
+ * A task positioner that also takes into account resizing a
+ * {@link com.android.wm.shell.windowdecor.ResizeVeil}.
+ * If the drag is resizing the task, we resize the veil instead.
+ * If the drag is repositioning, we update in the typical manner.
+ */
+public class VeiledResizeTaskPositioner implements DragPositioningCallback,
+ Transitions.TransitionHandler {
+
+ private DesktopModeWindowDecoration mDesktopWindowDecoration;
+ private ShellTaskOrganizer mTaskOrganizer;
+ private DisplayController mDisplayController;
+ private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+ private final Transitions mTransitions;
+ private final Rect mStableBounds = new Rect();
+ private final Rect mTaskBoundsAtDragStart = new Rect();
+ private final PointF mRepositionStartPoint = new PointF();
+ private final Rect mRepositionTaskBounds = new Rect();
+ // If a task move (not resize) finishes in this region, the positioner will not attempt to
+ // finalize the bounds there using WCT#setBounds
+ private final Rect mDisallowedAreaForEndBounds = new Rect();
+ private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+ private int mCtrlType;
+
+ public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
+ DesktopModeWindowDecoration windowDecoration, DisplayController displayController,
+ Rect disallowedAreaForEndBounds,
+ DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ Transitions transitions) {
+ this(taskOrganizer, windowDecoration, displayController, disallowedAreaForEndBounds,
+ dragStartListener, SurfaceControl.Transaction::new, transitions);
+ }
+
+ public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
+ DesktopModeWindowDecoration windowDecoration, DisplayController displayController,
+ Rect disallowedAreaForEndBounds,
+ DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ Supplier<SurfaceControl.Transaction> supplier, Transitions transitions) {
+ mTaskOrganizer = taskOrganizer;
+ mDesktopWindowDecoration = windowDecoration;
+ mDisplayController = displayController;
+ mDragStartListener = dragStartListener;
+ mDisallowedAreaForEndBounds.set(disallowedAreaForEndBounds);
+ mTransactionSupplier = supplier;
+ mTransitions = transitions;
+ }
+
+ @Override
+ public void onDragPositioningStart(int ctrlType, float x, float y) {
+ mCtrlType = ctrlType;
+ mTaskBoundsAtDragStart.set(
+ mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
+ mRepositionStartPoint.set(x, y);
+ if (isResizing()) {
+ mDesktopWindowDecoration.showResizeVeil(mTaskBoundsAtDragStart);
+ if (!mDesktopWindowDecoration.mTaskInfo.isFocused) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true);
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ }
+ mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
+ mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
+ }
+
+ @Override
+ public void onDragPositioningMove(float x, float y) {
+ PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint);
+ if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType,
+ mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
+ mDisplayController, mDesktopWindowDecoration)) {
+ mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds);
+ } else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
+ final SurfaceControl.Transaction t = mTransactionSupplier.get();
+ DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration,
+ mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t,
+ x, y);
+ t.apply();
+ }
+ }
+
+ @Override
+ public void onDragPositioningEnd(float x, float y) {
+ PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y,
+ mRepositionStartPoint);
+ if (isResizing()) {
+ if (!mTaskBoundsAtDragStart.equals(mRepositionTaskBounds)) {
+ DragPositioningCallbackUtility.changeBounds(
+ mCtrlType, mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds,
+ delta, mDisplayController, mDesktopWindowDecoration);
+ mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
+ } else {
+ mTaskOrganizer.applyTransaction(wct);
+ }
+ } else {
+ // If bounds haven't changed, perform necessary veil reset here as startAnimation
+ // won't be called.
+ mDesktopWindowDecoration.hideResizeVeil();
+ }
+ } else if (!mDisallowedAreaForEndBounds.contains((int) x, (int) y)) {
+ DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
+ mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
+ DragPositioningCallbackUtility.applyTaskBoundsChange(new WindowContainerTransaction(),
+ mDesktopWindowDecoration, mRepositionTaskBounds, mTaskOrganizer);
+ }
+
+ mCtrlType = CTRL_TYPE_UNDEFINED;
+ mTaskBoundsAtDragStart.setEmpty();
+ mRepositionStartPoint.set(0, 0);
+ }
+
+ private boolean isResizing() {
+ return (mCtrlType & CTRL_TYPE_TOP) != 0 || (mCtrlType & CTRL_TYPE_BOTTOM) != 0
+ || (mCtrlType & CTRL_TYPE_LEFT) != 0 || (mCtrlType & CTRL_TYPE_RIGHT) != 0;
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ startTransaction.apply();
+ mDesktopWindowDecoration.hideResizeVeil();
+ mCtrlType = CTRL_TYPE_UNDEFINED;
+ finishCallback.onTransitionFinished(null, null);
+ return true;
+ }
+
+ /**
+ * We should never reach this as this handler's transitions are only started from shell
+ * explicitly.
+ */
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index 3734487e8f4b..9f03d9aec166 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -17,7 +17,9 @@
package com.android.wm.shell.windowdecor;
import android.app.ActivityManager;
+import android.os.IBinder;
import android.view.SurfaceControl;
+import android.window.TransitionInfo;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
@@ -95,4 +97,34 @@ public interface WindowDecorViewModel {
* @param taskInfo the info of the task
*/
void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo);
+
+ /**
+ * Notifies that a shell transition is about to start. If the transition is of type
+ * TRANSIT_ENTER_DESKTOP, it will save that transition to unpause relayout for the transitioning
+ * task after the transition has ended.
+ *
+ * @param transition the ready transaction
+ * @param info of Transition to check if relayout needs to be paused for a task
+ * @param change a change in the given transition
+ */
+ default void onTransitionReady(IBinder transition, TransitionInfo info,
+ TransitionInfo.Change change) {}
+
+ /**
+ * Notifies that a shell transition is about to merge with another to give the window
+ * decoration a chance to prepare for this merge.
+ *
+ * @param merged the transaction being merged
+ * @param playing the transaction being merged into
+ */
+ default void onTransitionMerged(IBinder merged, IBinder playing) {}
+
+ /**
+ * Notifies that a shell transition is about to finish to give the window decoration a chance
+ * to clean up.
+ *
+ * @param transaction
+ */
+ default void onTransitionFinished(IBinder transaction) {}
+
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index f8e6ecc4499a..ac5ff2075901 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.windowdecor;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.res.Configuration;
@@ -24,15 +26,17 @@ import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
+import android.os.Binder;
import android.view.Display;
-import android.view.InsetsState;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.ViewRootImpl;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
+import android.window.SurfaceSyncGroup;
import android.window.TaskConstants;
import android.window.WindowContainerTransaction;
@@ -58,7 +62,6 @@ import java.util.function.Supplier;
*/
public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
implements AutoCloseable {
- private static final int[] CAPTION_INSETS_TYPES = { InsetsState.ITYPE_CAPTION_BAR };
/**
* System-wide context. Only used to create context with overridden configurations.
@@ -84,19 +87,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
};
RunningTaskInfo mTaskInfo;
+ int mLayoutResId;
final SurfaceControl mTaskSurface;
Display mDisplay;
Context mDecorWindowContext;
SurfaceControl mDecorationContainerSurface;
- SurfaceControl mTaskBackgroundSurface;
SurfaceControl mCaptionContainerSurface;
private WindowlessWindowManager mCaptionWindowManager;
private SurfaceControlViewHost mViewHost;
+ private final Binder mOwner = new Binder();
private final Rect mCaptionInsetsRect = new Rect();
- private final Rect mTaskSurfaceCrop = new Rect();
private final float[] mTmpColor = new float[3];
WindowDecoration(
@@ -162,6 +165,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
if (params.mRunningTaskInfo != null) {
mTaskInfo = params.mRunningTaskInfo;
}
+ final int oldLayoutResId = mLayoutResId;
+ mLayoutResId = params.mLayoutResId;
if (!mTaskInfo.isVisible) {
releaseViews();
@@ -178,7 +183,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
final Configuration taskConfig = getConfigurationWithOverrides(mTaskInfo);
if (oldTaskConfig.densityDpi != taskConfig.densityDpi
|| mDisplay == null
- || mDisplay.getDisplayId() != mTaskInfo.displayId) {
+ || mDisplay.getDisplayId() != mTaskInfo.displayId
+ || oldLayoutResId != mLayoutResId) {
releaseViews();
if (!obtainDisplayOrRegisterListener()) {
@@ -188,15 +194,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mDecorWindowContext = mContext.createConfigurationContext(taskConfig);
if (params.mLayoutResId != 0) {
outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
- .inflate(params.mLayoutResId, null);
+ .inflate(params.mLayoutResId, null);
}
}
if (outResult.mRootView == null) {
outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
- .inflate(params.mLayoutResId , null);
+ .inflate(params.mLayoutResId, null);
}
+ final Resources resources = mDecorWindowContext.getResources();
+ final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
+ outResult.mWidth = taskBounds.width();
+ outResult.mHeight = taskBounds.height();
+
// DecorationContainerSurface
if (mDecorationContainerSurface == null) {
final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
@@ -211,46 +222,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
TaskConstants.TASK_CHILD_LAYER_WINDOW_DECORATIONS);
}
- final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
- final Resources resources = mDecorWindowContext.getResources();
- outResult.mDecorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
- outResult.mDecorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
- outResult.mWidth = taskBounds.width()
- + loadDimensionPixelSize(resources, params.mOutsetRightId)
- - outResult.mDecorContainerOffsetX;
- outResult.mHeight = taskBounds.height()
- + loadDimensionPixelSize(resources, params.mOutsetBottomId)
- - outResult.mDecorContainerOffsetY;
- startT.setPosition(
- mDecorationContainerSurface,
- outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY)
- .setWindowCrop(mDecorationContainerSurface,
- outResult.mWidth, outResult.mHeight)
+ startT.setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
.show(mDecorationContainerSurface);
- // TaskBackgroundSurface
- if (mTaskBackgroundSurface == null) {
- final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
- mTaskBackgroundSurface = builder
- .setName("Background of Task=" + mTaskInfo.taskId)
- .setEffectLayer()
- .setParent(mTaskSurface)
- .build();
-
- startT.setLayer(mTaskBackgroundSurface, TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND);
- }
-
- float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
- int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
- mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
- mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
- mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
- startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(),
- taskBounds.height())
- .setShadowRadius(mTaskBackgroundSurface, shadowRadius)
- .setColor(mTaskBackgroundSurface, mTmpColor)
- .show(mTaskBackgroundSurface);
-
// CaptionContainerSurface, CaptionWindowManager
if (mCaptionContainerSurface == null) {
final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
@@ -263,14 +237,39 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
final int captionWidth = taskBounds.width();
-
- startT.setPosition(
- mCaptionContainerSurface,
- -outResult.mDecorContainerOffsetX + params.mCaptionX,
- -outResult.mDecorContainerOffsetY + params.mCaptionY)
- .setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight)
+ startT.setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight)
.show(mCaptionContainerSurface);
+ if (ViewRootImpl.CAPTION_ON_SHELL) {
+ outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
+
+ // Caption insets
+ mCaptionInsetsRect.set(taskBounds);
+ mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight + params.mCaptionY;
+ wct.addInsetsSource(mTaskInfo.token,
+ mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect);
+ } else {
+ startT.hide(mCaptionContainerSurface);
+ }
+
+ // Task surface itself
+ float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
+ int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
+ mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
+ mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
+ mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
+ final Point taskPosition = mTaskInfo.positionInParent;
+ startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight)
+ .setShadowRadius(mTaskSurface, shadowRadius)
+ .setColor(mTaskSurface, mTmpColor)
+ .show(mTaskSurface);
+ finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
+ .setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
+ if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ startT.setCornerRadius(mTaskSurface, params.mCornerRadius);
+ finishT.setCornerRadius(mTaskSurface, params.mCornerRadius);
+ }
+
if (mCaptionWindowManager == null) {
// Put caption under a container surface because ViewRootImpl sets the destination frame
// of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
@@ -290,34 +289,16 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
if (mViewHost == null) {
mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
mCaptionWindowManager);
+ if (params.mApplyStartTransactionOnDraw) {
+ mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT);
+ }
mViewHost.setView(outResult.mRootView, lp);
} else {
+ if (params.mApplyStartTransactionOnDraw) {
+ mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT);
+ }
mViewHost.relayout(lp);
}
-
- if (ViewRootImpl.CAPTION_ON_SHELL) {
- outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
-
- // Caption insets
- mCaptionInsetsRect.set(taskBounds);
- mCaptionInsetsRect.bottom =
- mCaptionInsetsRect.top + captionHeight + params.mCaptionY;
- wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect,
- CAPTION_INSETS_TYPES);
- } else {
- startT.hide(mCaptionContainerSurface);
- }
-
- // Task surface itself
- Point taskPosition = mTaskInfo.positionInParent;
- mTaskSurfaceCrop.set(
- outResult.mDecorContainerOffsetX,
- outResult.mDecorContainerOffsetY,
- outResult.mWidth + outResult.mDecorContainerOffsetX,
- outResult.mHeight + outResult.mDecorContainerOffsetY);
- startT.show(mTaskSurface);
- finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
- .setCrop(mTaskSurface, mTaskSurfaceCrop);
}
/**
@@ -357,18 +338,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
released = true;
}
- if (mTaskBackgroundSurface != null) {
- t.remove(mTaskBackgroundSurface);
- mTaskBackgroundSurface = null;
- released = true;
- }
-
if (released) {
t.apply();
}
final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
- wct.removeInsetsProvider(mTaskInfo.token, CAPTION_INSETS_TYPES);
+ wct.removeInsetsSource(mTaskInfo.token,
+ mOwner, 0 /* index */, WindowInsets.Type.captionBar());
mTaskOrganizer.applyTransaction(wct);
}
@@ -395,18 +371,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
/**
* Create a window associated with this WindowDecoration.
* Note that subclass must dispose of this when the task is hidden/closed.
- * @param layoutId layout to make the window from
- * @param t the transaction to apply
- * @param xPos x position of new window
- * @param yPos y position of new window
- * @param width width of new window
- * @param height height of new window
+ *
+ * @param layoutId layout to make the window from
+ * @param t the transaction to apply
+ * @param xPos x position of new window
+ * @param yPos y position of new window
+ * @param width width of new window
+ * @param height height of new window
* @param shadowRadius radius of the shadow of the new window
* @param cornerRadius radius of the corners of the new window
* @return the {@link AdditionalWindow} that was added.
*/
AdditionalWindow addWindow(int layoutId, String namePrefix, SurfaceControl.Transaction t,
- int xPos, int yPos, int width, int height, int shadowRadius, int cornerRadius) {
+ SurfaceSyncGroup ssg, int xPos, int yPos, int width, int height, int shadowRadius,
+ int cornerRadius) {
final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
SurfaceControl windowSurfaceControl = builder
.setName(namePrefix + " of Task=" + mTaskInfo.taskId)
@@ -430,37 +408,24 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
windowSurfaceControl, null /* hostInputToken */);
SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory
.create(mDecorWindowContext, mDisplay, windowManager);
- viewHost.setView(v, lp);
+ ssg.add(viewHost.getSurfacePackage(), () -> viewHost.setView(v, lp));
return new AdditionalWindow(windowSurfaceControl, viewHost,
mSurfaceControlTransactionSupplier);
}
- static class RelayoutParams{
+ static class RelayoutParams {
RunningTaskInfo mRunningTaskInfo;
int mLayoutResId;
int mCaptionHeightId;
int mCaptionWidthId;
int mShadowRadiusId;
- int mOutsetTopId;
- int mOutsetBottomId;
- int mOutsetLeftId;
- int mOutsetRightId;
+ int mCornerRadius;
int mCaptionX;
int mCaptionY;
- void setOutsets(int leftId, int topId, int rightId, int bottomId) {
- mOutsetLeftId = leftId;
- mOutsetTopId = topId;
- mOutsetRightId = rightId;
- mOutsetBottomId = bottomId;
- }
-
- void setCaptionPosition(int left, int top) {
- mCaptionX = left;
- mCaptionY = top;
- }
+ boolean mApplyStartTransactionOnDraw;
void reset() {
mLayoutResId = Resources.ID_NULL;
@@ -468,13 +433,12 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mCaptionWidthId = Resources.ID_NULL;
mShadowRadiusId = Resources.ID_NULL;
- mOutsetTopId = Resources.ID_NULL;
- mOutsetBottomId = Resources.ID_NULL;
- mOutsetLeftId = Resources.ID_NULL;
- mOutsetRightId = Resources.ID_NULL;
+ mCornerRadius = 0;
mCaptionX = 0;
mCaptionY = 0;
+
+ mApplyStartTransactionOnDraw = false;
}
}
@@ -482,21 +446,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
int mWidth;
int mHeight;
T mRootView;
- int mDecorContainerOffsetX;
- int mDecorContainerOffsetY;
void reset() {
mWidth = 0;
mHeight = 0;
- mDecorContainerOffsetX = 0;
- mDecorContainerOffsetY = 0;
mRootView = null;
}
}
interface SurfaceControlViewHostFactory {
default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
- return new SurfaceControlViewHost(c, d, wmm);
+ return new SurfaceControlViewHost(c, d, wmm, "WindowDecoration");
}
}
@@ -508,7 +468,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
SurfaceControlViewHost mWindowViewHost;
Supplier<SurfaceControl.Transaction> mTransactionSupplier;
- private AdditionalWindow(SurfaceControl surfaceControl,
+ AdditionalWindow(SurfaceControl surfaceControl,
SurfaceControlViewHost surfaceControlViewHost,
Supplier<SurfaceControl.Transaction> transactionSupplier) {
mWindowSurface = surfaceControl;
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
new file mode 100644
index 000000000000..b67acd5c15bb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
@@ -0,0 +1,84 @@
+package com.android.wm.shell.windowdecor.viewholder
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.view.View
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.wm.shell.R
+
+/**
+ * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
+ * finer controls such as a close window button and an "app info" section to pull up additional
+ * controls.
+ */
+internal class DesktopModeAppControlsWindowDecorationViewHolder(
+ rootView: View,
+ onCaptionTouchListener: View.OnTouchListener,
+ onCaptionButtonClickListener: View.OnClickListener,
+ appName: CharSequence,
+ appIcon: Drawable
+) : DesktopModeWindowDecorationViewHolder(rootView) {
+
+ private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption)
+ private val captionHandle: View = rootView.findViewById(R.id.caption_handle)
+ private val openMenuButton: View = rootView.findViewById(R.id.open_menu_button)
+ private val closeWindowButton: ImageButton = rootView.findViewById(R.id.close_window)
+ private val expandMenuButton: ImageButton = rootView.findViewById(R.id.expand_menu_button)
+ private val appNameTextView: TextView = rootView.findViewById(R.id.application_name)
+ private val appIconImageView: ImageView = rootView.findViewById(R.id.application_icon)
+
+ init {
+ captionView.setOnTouchListener(onCaptionTouchListener)
+ captionHandle.setOnTouchListener(onCaptionTouchListener)
+ openMenuButton.setOnClickListener(onCaptionButtonClickListener)
+ openMenuButton.setOnTouchListener(onCaptionTouchListener)
+ closeWindowButton.setOnClickListener(onCaptionButtonClickListener)
+ closeWindowButton.setOnTouchListener(onCaptionTouchListener)
+ appNameTextView.text = appName
+ appIconImageView.setImageDrawable(appIcon)
+ }
+
+ override fun bindData(taskInfo: RunningTaskInfo) {
+
+ val captionDrawable = captionView.background as GradientDrawable
+ captionDrawable.setColor(taskInfo.taskDescription.statusBarColor)
+
+ closeWindowButton.imageTintList = ColorStateList.valueOf(
+ getCaptionCloseButtonColor(taskInfo))
+ expandMenuButton.imageTintList = ColorStateList.valueOf(
+ getCaptionExpandButtonColor(taskInfo))
+ appNameTextView.setTextColor(getCaptionAppNameTextColor(taskInfo))
+ }
+
+ 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)
+ }
+ }
+
+ 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)
+ }
+ }
+
+ 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)
+ }
+ }
+
+ companion object {
+ private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder"
+ }
+}
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
new file mode 100644
index 000000000000..47a12a0cb71c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
@@ -0,0 +1,44 @@
+package com.android.wm.shell.windowdecor.viewholder
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.res.ColorStateList
+import android.graphics.drawable.GradientDrawable
+import android.view.View
+import android.widget.ImageButton
+import com.android.wm.shell.R
+
+/**
+ * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen). It
+ * hosts a simple handle bar from which to initiate a drag motion to enter desktop mode.
+ */
+internal class DesktopModeFocusedWindowDecorationViewHolder(
+ rootView: View,
+ onCaptionTouchListener: View.OnTouchListener,
+ onCaptionButtonClickListener: View.OnClickListener
+) : DesktopModeWindowDecorationViewHolder(rootView) {
+
+ private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption)
+ private val captionHandle: ImageButton = rootView.findViewById(R.id.caption_handle)
+
+ init {
+ captionView.setOnTouchListener(onCaptionTouchListener)
+ captionHandle.setOnTouchListener(onCaptionTouchListener)
+ captionHandle.setOnClickListener(onCaptionButtonClickListener)
+ }
+
+ override fun bindData(taskInfo: RunningTaskInfo) {
+ val captionColor = taskInfo.taskDescription.statusBarColor
+ val captionDrawable = captionView.background as GradientDrawable
+ captionDrawable.setColor(captionColor)
+
+ captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
+ }
+
+ private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
+ return if (shouldUseLightCaptionColors(taskInfo)) {
+ context.getColor(R.color.desktop_mode_caption_handle_bar_light)
+ } else {
+ context.getColor(R.color.desktop_mode_caption_handle_bar_dark)
+ }
+ }
+}
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
new file mode 100644
index 000000000000..514ea52cb8ae
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
@@ -0,0 +1,28 @@
+package com.android.wm.shell.windowdecor.viewholder
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.graphics.Color
+import android.view.View
+
+/**
+ * Encapsulates the root [View] of a window decoration and its children to facilitate looking up
+ * children (via findViewById) and updating to the latest data from [RunningTaskInfo].
+ */
+internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) {
+ val context: Context = rootView.context
+
+ /**
+ * A signal to the view holder that new data is available and that the views should be updated
+ * to reflect it.
+ */
+ 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 Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 3ca5b9c38aff..b6696c70dbb1 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -41,6 +41,8 @@ android_test {
static_libs: [
"androidx.test.ext.junit",
"flickerlib",
+ "flickerlib-apphelpers",
+ "flickerlib-helpers",
"truth-prebuilt",
"app-helpers-core",
"launcher-helper-lib",
@@ -48,6 +50,6 @@ android_test {
"wm-flicker-common-assertions",
"wm-flicker-common-app-helpers",
"platform-test-annotations",
- "wmshell-flicker-test-components",
+ "flickertestapplib",
],
}
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
index 06df9568e01a..4721741611cf 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
@@ -46,7 +46,7 @@
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
<!-- Allow the test to write directly to /sdcard/ -->
- <application android:requestLegacyExternalStorage="true">
+ <application android:requestLegacyExternalStorage="true" android:largeHeap="true">
<uses-library android:name="android.test.runner"/>
<service android:name=".NotificationListener"
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
index 574a9f4da627..b5937ae80f0a 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
@@ -13,13 +13,31 @@
<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" />
+ <!-- Ensure output directory is empty at the start -->
+ <option name="run-command" value="rm -rf /sdcard/flicker" />
+ <!-- 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" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <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="WMShellFlickerTests.apk"/>
- <option name="test-file-name" value="WMShellFlickerTestApp.apk" />
+ <option name="test-file-name" value="FlickerTestApp.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="package" value="com.android.wm.shell.flicker"/>
@@ -32,4 +50,4 @@
<option name="collect-on-run-ended-only" value="true" />
<option name="clean-up" value="true" />
</metrics_collector>
-</configuration> \ No newline at end of file
+</configuration>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt
new file mode 100644
index 000000000000..e06e074ee98a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.flicker
+
+import android.app.Instrumentation
+import android.tools.device.flicker.junit.FlickerBuilderProvider
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.tapl.LauncherInstrumentation
+
+abstract class BaseBenchmarkTest
+@JvmOverloads
+constructor(
+ protected open val flicker: FlickerTest,
+ protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+ protected val tapl: LauncherInstrumentation = LauncherInstrumentation()
+) {
+ /** Specification of the test transition to execute */
+ abstract val transition: FlickerBuilder.() -> Unit
+
+ /**
+ * Entry point for the test runner. It will use this method to initialize and cache flicker
+ * executions
+ */
+ @FlickerBuilderProvider
+ fun buildFlicker(): FlickerBuilder {
+ return FlickerBuilder(instrumentation).apply {
+ setup { flicker.scenario.setIsTablet(tapl.isTablet) }
+ transition()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
new file mode 100644
index 000000000000..c98c5a0ad1a6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.app.Instrumentation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.tapl.LauncherInstrumentation
+
+/**
+ * Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR],
+ * [ComponentNameMatcher.TASK_BAR], [ComponentNameMatcher.STATUS_BAR], and general assertions
+ * (layers visible in consecutive states, entire screen covered, etc.)
+ */
+abstract class BaseTest
+@JvmOverloads
+constructor(
+ override val flicker: FlickerTest,
+ instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+ tapl: LauncherInstrumentation = LauncherInstrumentation()
+) : BaseBenchmarkTest(flicker, instrumentation, tapl), ICommonAssertions
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index cba396a82a87..798cc95c020f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -15,27 +15,26 @@
*/
@file:JvmName("CommonAssertions")
+
package com.android.wm.shell.flicker
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.server.wm.traces.common.region.Region
+import android.tools.common.Rotation
+import android.tools.common.datatypes.Region
+import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject
+import android.tools.common.flicker.subject.layers.LayersTraceSubject
+import android.tools.common.traces.component.IComponentMatcher
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.helpers.WindowUtils
-fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() {
- assertLayersEnd {
- this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT)
- }
+fun FlickerTest.appPairsDividerIsVisibleAtEnd() {
+ assertLayersEnd { this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) }
}
-fun FlickerTestParameter.appPairsDividerIsInvisibleAtEnd() {
- assertLayersEnd {
- this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT)
- }
+fun FlickerTest.appPairsDividerIsInvisibleAtEnd() {
+ assertLayersEnd { this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT) }
}
-fun FlickerTestParameter.appPairsDividerBecomesVisible() {
+fun FlickerTest.appPairsDividerBecomesVisible() {
assertLayers {
this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
.then()
@@ -43,91 +42,313 @@ fun FlickerTestParameter.appPairsDividerBecomesVisible() {
}
}
-fun FlickerTestParameter.splitScreenDividerBecomesVisible() {
- layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+fun FlickerTest.splitScreenEntered(
+ component1: IComponentMatcher,
+ component2: IComponentMatcher,
+ fromOtherApp: Boolean,
+ appExistAtStart: Boolean = true
+) {
+ if (fromOtherApp) {
+ if (appExistAtStart) {
+ appWindowIsInvisibleAtStart(component1)
+ } else {
+ appWindowIsNotContainAtStart(component1)
+ }
+ } else {
+ appWindowIsVisibleAtStart(component1)
+ }
+ if (appExistAtStart) {
+ appWindowIsInvisibleAtStart(component2)
+ } else {
+ appWindowIsNotContainAtStart(component2)
+ }
+ splitScreenDividerIsInvisibleAtStart()
+
+ appWindowIsVisibleAtEnd(component1)
+ appWindowIsVisibleAtEnd(component2)
+ splitScreenDividerIsVisibleAtEnd()
}
-fun FlickerTestParameter.layerBecomesVisible(
- component: FlickerComponentName
+fun FlickerTest.splitScreenDismissed(
+ component1: IComponentMatcher,
+ component2: IComponentMatcher,
+ toHome: Boolean
) {
+ appWindowIsVisibleAtStart(component1)
+ appWindowIsVisibleAtStart(component2)
+ splitScreenDividerIsVisibleAtStart()
+
+ appWindowIsInvisibleAtEnd(component1)
+ if (toHome) {
+ appWindowIsInvisibleAtEnd(component2)
+ } else {
+ appWindowIsVisibleAtEnd(component2)
+ }
+ splitScreenDividerIsInvisibleAtEnd()
+}
+
+fun FlickerTest.splitScreenDividerIsVisibleAtStart() {
+ assertLayersStart { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+}
+
+fun FlickerTest.splitScreenDividerIsVisibleAtEnd() {
+ assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+}
+
+fun FlickerTest.splitScreenDividerIsInvisibleAtStart() {
+ assertLayersStart { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+}
+
+fun FlickerTest.splitScreenDividerIsInvisibleAtEnd() {
+ assertLayersEnd { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+}
+
+fun FlickerTest.splitScreenDividerBecomesVisible() {
+ layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+}
+
+fun FlickerTest.splitScreenDividerBecomesInvisible() {
assertLayers {
- this.isInvisible(component)
+ this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
.then()
- .isVisible(component)
+ .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
}
}
-fun FlickerTestParameter.layerIsVisibleAtEnd(
- component: FlickerComponentName
+fun FlickerTest.layerBecomesVisible(component: IComponentMatcher) {
+ assertLayers { this.isInvisible(component).then().isVisible(component) }
+}
+
+fun FlickerTest.layerBecomesInvisible(component: IComponentMatcher) {
+ assertLayers { this.isVisible(component).then().isInvisible(component) }
+}
+
+fun FlickerTest.layerIsVisibleAtEnd(component: IComponentMatcher) {
+ assertLayersEnd { this.isVisible(component) }
+}
+
+fun FlickerTest.layerKeepVisible(component: IComponentMatcher) {
+ assertLayers { this.isVisible(component) }
+}
+
+fun FlickerTest.splitAppLayerBoundsBecomesVisible(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
- assertLayersEnd {
- this.isVisible(component)
+ assertLayers {
+ this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component), isOptional = true)
+ .then()
+ .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component))
+ .then()
+ .splitAppLayerBoundsSnapToDivider(
+ component,
+ landscapePosLeft,
+ portraitPosTop,
+ scenario.endRotation
+ )
+ }
+}
+
+fun FlickerTest.splitAppLayerBoundsBecomesVisibleByDrag(component: IComponentMatcher) {
+ assertLayers {
+ this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component), isOptional = true)
+ .then()
+ .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component))
+ .then()
+ // TODO(b/245472831): Verify the component should snap to divider.
+ .isVisible(component)
}
}
-fun FlickerTestParameter.splitAppLayerBoundsBecomesVisible(
- rotation: Int,
- component: FlickerComponentName,
- splitLeftTop: Boolean
+fun FlickerTest.splitAppLayerBoundsBecomesInvisible(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
assertLayers {
- val dividerRegion = this.last().layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
- this.isInvisible(component)
+ this.splitAppLayerBoundsSnapToDivider(
+ component,
+ landscapePosLeft,
+ portraitPosTop,
+ scenario.endRotation
+ )
.then()
- .invoke("splitAppLayerBoundsBecomesVisible") {
- it.visibleRegion(component).overlaps(
- if (splitLeftTop) {
- getSplitLeftTopRegion(dividerRegion, rotation)
- } else {
- getSplitRightBottomRegion(dividerRegion, rotation)
- }
- )
- }
+ .isVisible(component, true)
+ .then()
+ .isInvisible(component)
}
}
-fun FlickerTestParameter.splitAppLayerBoundsIsVisibleAtEnd(
- rotation: Int,
- component: FlickerComponentName,
- splitLeftTop: Boolean
+fun FlickerTest.splitAppLayerBoundsIsVisibleAtEnd(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
assertLayersEnd {
- val dividerRegion = layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(component).overlaps(
- if (splitLeftTop) {
- getSplitLeftTopRegion(dividerRegion, rotation)
- } else {
- getSplitRightBottomRegion(dividerRegion, rotation)
- }
+ splitAppLayerBoundsSnapToDivider(
+ component,
+ landscapePosLeft,
+ portraitPosTop,
+ scenario.endRotation
+ )
+ }
+}
+
+fun FlickerTest.splitAppLayerBoundsKeepVisible(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
+) {
+ assertLayers {
+ splitAppLayerBoundsSnapToDivider(
+ component,
+ landscapePosLeft,
+ portraitPosTop,
+ scenario.endRotation
)
}
}
-fun FlickerTestParameter.appWindowBecomesVisible(
- component: FlickerComponentName
+fun FlickerTest.splitAppLayerBoundsChanges(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
+ assertLayers {
+ if (landscapePosLeft) {
+ splitAppLayerBoundsSnapToDivider(
+ component,
+ landscapePosLeft,
+ portraitPosTop,
+ scenario.endRotation
+ )
+ } else {
+ splitAppLayerBoundsSnapToDivider(
+ component,
+ landscapePosLeft,
+ portraitPosTop,
+ scenario.endRotation
+ )
+ .then()
+ .isInvisible(component)
+ .then()
+ .splitAppLayerBoundsSnapToDivider(
+ component,
+ landscapePosLeft,
+ portraitPosTop,
+ scenario.endRotation
+ )
+ }
+ }
+}
+
+fun LayersTraceSubject.splitAppLayerBoundsSnapToDivider(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean,
+ rotation: Rotation
+): LayersTraceSubject {
+ return invoke("splitAppLayerBoundsSnapToDivider") {
+ it.splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, rotation)
+ }
+}
+
+fun LayerTraceEntrySubject.splitAppLayerBoundsSnapToDivider(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean,
+ rotation: Rotation
+): LayerTraceEntrySubject {
+ val displayBounds = WindowUtils.getDisplayBounds(rotation)
+ return invoke {
+ val dividerRegion =
+ layer(SPLIT_SCREEN_DIVIDER_COMPONENT)?.visibleRegion?.region
+ ?: error("$SPLIT_SCREEN_DIVIDER_COMPONENT component not found")
+ visibleRegion(component)
+ .coversAtMost(
+ if (displayBounds.width > displayBounds.height) {
+ if (landscapePosLeft) {
+ Region.from(
+ 0,
+ 0,
+ (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
+ displayBounds.bounds.bottom
+ )
+ } else {
+ Region.from(
+ (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
+ 0,
+ displayBounds.bounds.right,
+ displayBounds.bounds.bottom
+ )
+ }
+ } else {
+ if (portraitPosTop) {
+ Region.from(
+ 0,
+ 0,
+ displayBounds.bounds.right,
+ (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2
+ )
+ } else {
+ Region.from(
+ 0,
+ (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2,
+ displayBounds.bounds.right,
+ displayBounds.bounds.bottom
+ )
+ }
+ }
+ )
+ }
+}
+
+fun FlickerTest.appWindowBecomesVisible(component: IComponentMatcher) {
assertWm {
this.isAppWindowInvisible(component)
.then()
+ .notContains(component, isOptional = true)
+ .then()
+ .isAppWindowInvisible(component, isOptional = true)
+ .then()
.isAppWindowVisible(component)
}
}
-fun FlickerTestParameter.appWindowIsVisibleAtEnd(
- component: FlickerComponentName
-) {
- assertWmEnd {
- this.isAppWindowVisible(component)
- }
+fun FlickerTest.appWindowBecomesInvisible(component: IComponentMatcher) {
+ assertWm { this.isAppWindowVisible(component).then().isAppWindowInvisible(component) }
}
-fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() {
- assertLayersEnd {
- this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
- }
+fun FlickerTest.appWindowIsVisibleAtStart(component: IComponentMatcher) {
+ assertWmStart { this.isAppWindowVisible(component) }
+}
+
+fun FlickerTest.appWindowIsVisibleAtEnd(component: IComponentMatcher) {
+ assertWmEnd { this.isAppWindowVisible(component) }
}
-fun FlickerTestParameter.dockedStackDividerBecomesVisible() {
+fun FlickerTest.appWindowIsInvisibleAtStart(component: IComponentMatcher) {
+ assertWmStart { this.isAppWindowInvisible(component) }
+}
+
+fun FlickerTest.appWindowIsInvisibleAtEnd(component: IComponentMatcher) {
+ assertWmEnd { this.isAppWindowInvisible(component) }
+}
+
+fun FlickerTest.appWindowIsNotContainAtStart(component: IComponentMatcher) {
+ assertWmStart { this.notContains(component) }
+}
+
+fun FlickerTest.appWindowKeepVisible(component: IComponentMatcher) {
+ assertWm { this.isAppWindowVisible(component) }
+}
+
+fun FlickerTest.dockedStackDividerIsVisibleAtEnd() {
+ assertLayersEnd { this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) }
+}
+
+fun FlickerTest.dockedStackDividerBecomesVisible() {
assertLayers {
this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
.then()
@@ -135,7 +356,7 @@ fun FlickerTestParameter.dockedStackDividerBecomesVisible() {
}
}
-fun FlickerTestParameter.dockedStackDividerBecomesInvisible() {
+fun FlickerTest.dockedStackDividerBecomesInvisible() {
assertLayers {
this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
.then()
@@ -143,105 +364,91 @@ fun FlickerTestParameter.dockedStackDividerBecomesInvisible() {
}
}
-fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() {
- assertLayersEnd {
- this.notContains(DOCKED_STACK_DIVIDER_COMPONENT)
- }
+fun FlickerTest.dockedStackDividerNotExistsAtEnd() {
+ assertLayersEnd { this.notContains(DOCKED_STACK_DIVIDER_COMPONENT) }
}
-fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd(
- rotation: Int,
- primaryComponent: FlickerComponentName
+fun FlickerTest.appPairsPrimaryBoundsIsVisibleAtEnd(
+ rotation: Rotation,
+ primaryComponent: IComponentMatcher
) {
assertLayersEnd {
- val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(primaryComponent)
- .overlaps(getPrimaryRegion(dividerRegion, rotation))
+ val dividerRegion =
+ layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT)?.visibleRegion?.region
+ ?: error("$APP_PAIR_SPLIT_DIVIDER_COMPONENT component not found")
+ visibleRegion(primaryComponent).overlaps(getPrimaryRegion(dividerRegion, rotation))
}
}
-fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd(
- rotation: Int,
- primaryComponent: FlickerComponentName
+fun FlickerTest.dockedStackPrimaryBoundsIsVisibleAtEnd(
+ rotation: Rotation,
+ primaryComponent: IComponentMatcher
) {
assertLayersEnd {
- val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(primaryComponent)
- .overlaps(getPrimaryRegion(dividerRegion, rotation))
+ val dividerRegion =
+ layer(DOCKED_STACK_DIVIDER_COMPONENT)?.visibleRegion?.region
+ ?: error("$DOCKED_STACK_DIVIDER_COMPONENT component not found")
+ visibleRegion(primaryComponent).overlaps(getPrimaryRegion(dividerRegion, rotation))
}
}
-fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd(
- rotation: Int,
- secondaryComponent: FlickerComponentName
+fun FlickerTest.appPairsSecondaryBoundsIsVisibleAtEnd(
+ rotation: Rotation,
+ secondaryComponent: IComponentMatcher
) {
assertLayersEnd {
- val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(secondaryComponent)
- .overlaps(getSecondaryRegion(dividerRegion, rotation))
+ val dividerRegion =
+ layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT)?.visibleRegion?.region
+ ?: error("$APP_PAIR_SPLIT_DIVIDER_COMPONENT component not found")
+ visibleRegion(secondaryComponent).overlaps(getSecondaryRegion(dividerRegion, rotation))
}
}
-fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd(
- rotation: Int,
- secondaryComponent: FlickerComponentName
+fun FlickerTest.dockedStackSecondaryBoundsIsVisibleAtEnd(
+ rotation: Rotation,
+ secondaryComponent: IComponentMatcher
) {
assertLayersEnd {
- val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(secondaryComponent)
- .overlaps(getSecondaryRegion(dividerRegion, rotation))
+ val dividerRegion =
+ layer(DOCKED_STACK_DIVIDER_COMPONENT)?.visibleRegion?.region
+ ?: error("$DOCKED_STACK_DIVIDER_COMPONENT component not found")
+ visibleRegion(secondaryComponent).overlaps(getSecondaryRegion(dividerRegion, rotation))
}
}
-fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region {
+fun getPrimaryRegion(dividerRegion: Region, rotation: Rotation): Region {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
- Region.from(
- 0, 0, displayBounds.bounds.right,
- dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset
- )
- } else {
+ return if (rotation.isRotated()) {
Region.from(
- 0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
+ 0,
+ 0,
+ dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
displayBounds.bounds.bottom
)
- }
-}
-
-fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region {
- val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
- Region.from(
- 0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
- displayBounds.bounds.right, displayBounds.bounds.bottom
- )
} else {
Region.from(
- dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0,
- displayBounds.bounds.right, displayBounds.bounds.bottom
+ 0,
+ 0,
+ displayBounds.bounds.right,
+ dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset
)
}
}
-fun getSplitLeftTopRegion(dividerRegion: Region, rotation: Int): Region {
- val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return if (displayBounds.width > displayBounds.height) {
- Region.from(0, 0, dividerRegion.bounds.left, displayBounds.bounds.bottom)
- } else {
- Region.from(0, 0, displayBounds.bounds.right, dividerRegion.bounds.top)
- }
-}
-
-fun getSplitRightBottomRegion(dividerRegion: Region, rotation: Int): Region {
+fun getSecondaryRegion(dividerRegion: Region, rotation: Rotation): Region {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return if (displayBounds.width > displayBounds.height) {
+ return if (rotation.isRotated()) {
Region.from(
- dividerRegion.bounds.right, 0, displayBounds.bounds.right,
+ dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset,
+ 0,
+ displayBounds.bounds.right,
displayBounds.bounds.bottom
)
} else {
Region.from(
- 0, dividerRegion.bounds.bottom, displayBounds.bounds.right,
+ 0,
+ dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
+ displayBounds.bounds.right,
displayBounds.bounds.bottom
)
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
index f56eb6e783aa..3bc1e2acd015 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
@@ -15,11 +15,21 @@
*/
@file:JvmName("CommonConstants")
+
package com.android.wm.shell.flicker
-import com.android.server.wm.traces.common.FlickerComponentName
+import android.tools.common.traces.component.ComponentNameMatcher
const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
-val APP_PAIR_SPLIT_DIVIDER_COMPONENT = FlickerComponentName("", "AppPairSplitDivider#")
-val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#")
-val SPLIT_SCREEN_DIVIDER_COMPONENT = FlickerComponentName("", "StageCoordinatorSplitDivider#")
+const val LAUNCHER_UI_PACKAGE_NAME = "com.google.android.apps.nexuslauncher"
+val APP_PAIR_SPLIT_DIVIDER_COMPONENT = ComponentNameMatcher("", "AppPairSplitDivider#")
+val DOCKED_STACK_DIVIDER_COMPONENT = ComponentNameMatcher("", "DockedStackDivider#")
+val SPLIT_SCREEN_DIVIDER_COMPONENT = ComponentNameMatcher("", "StageCoordinatorSplitDivider#")
+val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
+
+enum class Direction {
+ UP,
+ DOWN,
+ LEFT,
+ RIGHT
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
new file mode 100644
index 000000000000..02d9a056afbf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.flicker
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.legacy.FlickerTest
+import com.android.server.wm.flicker.entireScreenCovered
+import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd
+import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
+import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.statusBarLayerIsVisibleAtStartAndEnd
+import com.android.server.wm.flicker.statusBarLayerPositionAtStartAndEnd
+import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
+import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd
+import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible
+import org.junit.Assume
+import org.junit.Test
+
+interface ICommonAssertions {
+ val flicker: FlickerTest
+
+ /** Checks that all parts of the screen are covered during the transition */
+ @Presubmit @Test fun entireScreenCovered() = flicker.entireScreenCovered()
+
+ /**
+ * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition
+ */
+ @Presubmit
+ @Test
+ fun navBarLayerIsVisibleAtStartAndEnd() {
+ Assume.assumeFalse(flicker.scenario.isTablet)
+ flicker.navBarLayerIsVisibleAtStartAndEnd()
+ }
+
+ /**
+ * Checks the position of the [ComponentNameMatcher.NAV_BAR] at the start and end of the
+ * transition
+ */
+ @Presubmit
+ @Test
+ fun navBarLayerPositionAtStartAndEnd() {
+ Assume.assumeFalse(flicker.scenario.isTablet)
+ flicker.navBarLayerPositionAtStartAndEnd()
+ }
+
+ /**
+ * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible during the whole transition
+ *
+ * Note: Phones only
+ */
+ @Presubmit
+ @Test
+ fun navBarWindowIsAlwaysVisible() {
+ Assume.assumeFalse(flicker.scenario.isTablet)
+ flicker.navBarWindowIsAlwaysVisible()
+ }
+
+ /**
+ * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible during the whole transition
+ */
+ @Presubmit
+ @Test
+ fun taskBarLayerIsVisibleAtStartAndEnd() {
+ Assume.assumeTrue(flicker.scenario.isTablet)
+ flicker.taskBarLayerIsVisibleAtStartAndEnd()
+ }
+
+ /**
+ * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole transition
+ *
+ * Note: Large screen only
+ */
+ @Presubmit
+ @Test
+ fun taskBarWindowIsAlwaysVisible() {
+ Assume.assumeTrue(flicker.scenario.isTablet)
+ flicker.taskBarWindowIsAlwaysVisible()
+ }
+
+ /**
+ * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible during the whole
+ * transition
+ */
+ @Presubmit
+ @Test
+ fun statusBarLayerIsVisibleAtStartAndEnd() = flicker.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /**
+ * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
+ * transition
+ */
+ @Presubmit
+ @Test
+ fun statusBarLayerPositionAtStartAndEnd() = flicker.statusBarLayerPositionAtStartAndEnd()
+
+ /**
+ * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole
+ * transition
+ */
+ @Presubmit @Test fun statusBarWindowIsAlwaysVisible() = flicker.statusBarWindowIsAlwaysVisible()
+
+ /**
+ * Checks that all layers that are visible on the trace, are visible for at least 2 consecutive
+ * entries.
+ */
+ @Presubmit
+ @Test
+ fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+ flicker.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() }
+ }
+
+ /**
+ * Checks that all windows that are visible on the trace, are visible for at least 2 consecutive
+ * entries.
+ */
+ @Presubmit
+ @Test
+ fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+ flicker.assertWm { this.visibleWindowsShownMoreThanOneConsecutiveEntry() }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt
new file mode 100644
index 000000000000..87b94ff8668b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.app.Instrumentation
+import android.content.Context
+import android.provider.Settings
+import android.util.Log
+import com.android.compatibility.common.util.SystemUtil
+import java.io.IOException
+
+object MultiWindowUtils {
+ private fun executeShellCommand(instrumentation: Instrumentation, cmd: String) {
+ try {
+ SystemUtil.runShellCommand(instrumentation, cmd)
+ } catch (e: IOException) {
+ Log.e(MultiWindowUtils::class.simpleName, "executeShellCommand error! $e")
+ }
+ }
+
+ fun getDevEnableNonResizableMultiWindow(context: Context): Int =
+ Settings.Global.getInt(
+ context.contentResolver,
+ Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW
+ )
+
+ fun setDevEnableNonResizableMultiWindow(context: Context, configValue: Int) =
+ Settings.Global.putInt(
+ context.contentResolver,
+ Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW,
+ configValue
+ )
+
+ fun setSupportsNonResizableMultiWindow(instrumentation: Instrumentation, configValue: Int) =
+ executeShellCommand(
+ instrumentation,
+ createConfigSupportsNonResizableMultiWindowCommand(configValue)
+ )
+
+ fun resetMultiWindowConfig(instrumentation: Instrumentation) =
+ executeShellCommand(instrumentation, resetMultiWindowConfigCommand)
+
+ private fun createConfigSupportsNonResizableMultiWindowCommand(configValue: Int): String =
+ "wm set-multi-window-config --supportsNonResizable $configValue"
+
+ private const val resetMultiWindowConfigCommand: String = "wm reset-multi-window-config"
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt
index 51f7a18f60dd..e0ef92457f58 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt
@@ -51,7 +51,7 @@ class NotificationListener : NotificationListenerService() {
private const val CMD_NOTIFICATION_ALLOW_LISTENER = "cmd notification allow_listener %s"
private const val CMD_NOTIFICATION_DISALLOW_LISTENER =
- "cmd notification disallow_listener %s"
+ "cmd notification disallow_listener %s"
private const val COMPONENT_NAME = "com.android.wm.shell.flicker/.NotificationListener"
private var instance: NotificationListener? = null
@@ -79,25 +79,23 @@ class NotificationListener : NotificationListenerService() {
): StatusBarNotification? {
instance?.run {
return notifications.values.firstOrNull(predicate)
- } ?: throw IllegalStateException("NotificationListenerService is not connected")
+ }
+ ?: throw IllegalStateException("NotificationListenerService is not connected")
}
fun waitForNotificationToAppear(
predicate: (StatusBarNotification) -> Boolean
): StatusBarNotification? {
instance?.let {
- return waitForResult(extractor = {
- it.notifications.values.firstOrNull(predicate)
- }).second
- } ?: throw IllegalStateException("NotificationListenerService is not connected")
+ return waitForResult(extractor = { it.notifications.values.firstOrNull(predicate) })
+ .second
+ }
+ ?: throw IllegalStateException("NotificationListenerService is not connected")
}
- fun waitForNotificationToDisappear(
- predicate: (StatusBarNotification) -> Boolean
- ): Boolean {
- return instance?.let {
- wait { it.notifications.values.none(predicate) }
- } ?: throw IllegalStateException("NotificationListenerService is not connected")
+ fun waitForNotificationToDisappear(predicate: (StatusBarNotification) -> Boolean): Boolean {
+ return instance?.let { wait { it.notifications.values.none(predicate) } }
+ ?: throw IllegalStateException("NotificationListenerService is not connected")
}
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
index 4d87ec9e872f..556cb06f3ca1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
@@ -15,6 +15,7 @@
*/
@file:JvmName("WaitUtils")
+
package com.android.wm.shell.flicker
import android.os.SystemClock
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
new file mode 100644
index 000000000000..61781565270b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.flicker.appcompat
+
+import android.content.Context
+import android.system.helpers.CommandsHelper
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.helpers.LetterboxAppHelper
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.IFlickerTestData
+import com.android.wm.shell.flicker.BaseTest
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.layerKeepVisible
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.runners.Parameterized
+
+abstract class BaseAppCompat(flicker: FlickerTest) : BaseTest(flicker) {
+ protected val context: Context = instrumentation.context
+ protected val letterboxApp = LetterboxAppHelper(instrumentation)
+ lateinit var cmdHelper: CommandsHelper
+ private lateinit var letterboxStyle: HashMap<String, String>
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ setStartRotation()
+ letterboxApp.launchViaIntent(wmHelper)
+ setEndRotation()
+ }
+ teardown {
+ letterboxApp.exit(wmHelper)
+ }
+ }
+
+ @Before
+ fun before() {
+ cmdHelper = CommandsHelper.getInstance(instrumentation)
+ Assume.assumeTrue(tapl.isTablet && isIgnoreOrientationRequest())
+ letterboxStyle = mapLetterboxStyle()
+ setLetterboxEducationEnabled(false)
+ }
+
+ @After
+ fun after() {
+ resetLetterboxEducationEnabled()
+ }
+
+ private fun mapLetterboxStyle(): HashMap<String, String> {
+ val res = cmdHelper.executeShellCommand("wm get-letterbox-style")
+ val lines = res.lines()
+ val map = HashMap<String, String>()
+ for (line in lines) {
+ val keyValuePair = line.split(":")
+ if (keyValuePair.size == 2) {
+ val key = keyValuePair[0].trim()
+ map[key] = keyValuePair[1].trim()
+ }
+ }
+ return map
+ }
+
+ private fun getLetterboxStyle(): HashMap<String, String> {
+ if (!::letterboxStyle.isInitialized) {
+ letterboxStyle = mapLetterboxStyle()
+ }
+ return letterboxStyle
+ }
+
+ private fun resetLetterboxEducationEnabled() {
+ val enabled = getLetterboxStyle().getValue("Is education enabled")
+ cmdHelper.executeShellCommand("wm set-letterbox-style --isEducationEnabled $enabled")
+ }
+
+ private fun setLetterboxEducationEnabled(enabled: Boolean) {
+ cmdHelper.executeShellCommand("wm set-letterbox-style --isEducationEnabled $enabled")
+ }
+
+ private fun isIgnoreOrientationRequest(): Boolean {
+ val res = cmdHelper.executeShellCommand("wm get-ignore-orientation-request")
+ return res != null && res.contains("true")
+ }
+
+ fun IFlickerTestData.setStartRotation() = setRotation(flicker.scenario.startRotation)
+
+ fun IFlickerTestData.setEndRotation() = setRotation(flicker.scenario.endRotation)
+
+ /** Checks that app entering letterboxed state have rounded corners */
+ fun assertLetterboxAppAtStartHasRoundedCorners() {
+ assumeLetterboxRoundedCornersEnabled()
+ flicker.assertLayersStart { this.hasRoundedCorners(letterboxApp) }
+ }
+
+ fun assertLetterboxAppAtEndHasRoundedCorners() {
+ assumeLetterboxRoundedCornersEnabled()
+ flicker.assertLayersEnd { this.hasRoundedCorners(letterboxApp) }
+ }
+
+ /** Only run on tests with config_letterboxActivityCornersRadius != 0 in devices */
+ private fun assumeLetterboxRoundedCornersEnabled() {
+ Assume.assumeTrue(getLetterboxStyle().getValue("Corner radius") != "0")
+ }
+
+ fun assertLetterboxAppVisibleAtStartAndEnd() {
+ flicker.appWindowIsVisibleAtStart(letterboxApp)
+ flicker.appWindowIsVisibleAtEnd(letterboxApp)
+ }
+
+ fun assertAppLetterboxedAtEnd() =
+ flicker.assertLayersEnd { isVisible(ComponentNameMatcher.LETTERBOX) }
+
+ fun assertAppLetterboxedAtStart() =
+ flicker.assertLayersStart { isVisible(ComponentNameMatcher.LETTERBOX) }
+
+ fun assertLetterboxAppLayerKeepVisible() = flicker.layerKeepVisible(letterboxApp)
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.rotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.rotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
new file mode 100644
index 000000000000..c2141a370f10
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.flicker.appcompat
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching app in size compat mode.
+ *
+ * To run this test: `atest WMShellFlickerTests:OpenAppInSizeCompatModeTest`
+ *
+ * Actions:
+ * ```
+ * Rotate non resizable portrait only app to opposite orientation to trigger size compat mode
+ * ```
+ *
+ * Notes:
+ * ```
+ * Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [BaseTest]
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class OpenAppInSizeCompatModeTest(flicker: FlickerTest) : BaseAppCompat(flicker) {
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ setStartRotation()
+ letterboxApp.launchViaIntent(wmHelper)
+ }
+ transitions { setEndRotation() }
+ teardown { letterboxApp.exit(wmHelper) }
+ }
+
+ /**
+ * Windows maybe recreated when rotated. Checks that the focus does not change or if it does,
+ * focus returns to [letterboxApp]
+ */
+ @Postsubmit
+ @Test
+ fun letterboxAppFocusedAtEnd() = flicker.assertEventLog { focusChanges(letterboxApp.`package`) }
+
+ @Postsubmit
+ @Test
+ fun letterboxedAppHasRoundedCorners() = assertLetterboxAppAtEndHasRoundedCorners()
+
+ @Postsubmit
+ @Test
+ fun appIsLetterboxedAtEnd() = assertAppLetterboxedAtEnd()
+
+ /**
+ * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't
+ * flicker, and disappears before the transition is complete
+ */
+ @Postsubmit
+ @Test
+ fun rotationLayerAppearsAndVanishes() {
+ flicker.assertLayers {
+ this.isVisible(letterboxApp)
+ .then()
+ .isVisible(ComponentNameMatcher.ROTATION)
+ .then()
+ .isVisible(letterboxApp)
+ .isInvisible(ComponentNameMatcher.ROTATION)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
new file mode 100644
index 000000000000..b0e1a42306df
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.flicker.appcompat
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.helpers.WindowUtils
+import androidx.test.filters.RequiresDevice
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test restarting app in size compat mode.
+ *
+ * To run this test: `atest WMShellFlickerTests:RestartAppInSizeCompatModeTest`
+ *
+ * Actions:
+ * ```
+ * Rotate app to opposite orientation to trigger size compat mode
+ * Press restart button and wait for letterboxed app to resize
+ * ```
+ *
+ * Notes:
+ * ```
+ * Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [BaseTest]
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class RestartAppInSizeCompatModeTest(flicker: FlickerTest) : BaseAppCompat(flicker) {
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ super.transition(this)
+ transitions { letterboxApp.clickRestart(wmHelper) }
+ }
+
+ @Postsubmit @Test fun appVisibleAtStartAndEnd() = assertLetterboxAppVisibleAtStartAndEnd()
+
+ @Postsubmit
+ @Test
+ fun appWindowVisibilityChanges() {
+ flicker.assertWm {
+ this.isAppWindowVisible(letterboxApp)
+ .then()
+ .isAppWindowInvisible(letterboxApp) // animatingExit true
+ .then()
+ .isAppWindowVisible(letterboxApp) // Activity finish relaunching
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun appLayerKeepVisible() = assertLetterboxAppLayerKeepVisible()
+
+ @Postsubmit
+ @Test
+ fun appIsLetterboxedAtStart() = assertAppLetterboxedAtStart()
+
+ @Postsubmit
+ @Test
+ fun letterboxedAppHasRoundedCorners() = assertLetterboxAppAtStartHasRoundedCorners()
+
+ /** Checks that the visible region of [letterboxApp] is still within display bounds */
+ @Postsubmit
+ @Test
+ fun appWindowRemainInsideVisibleBounds() {
+ val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
+ flicker.assertLayersEnd { visibleRegion(letterboxApp).coversAtMost(displayBounds) }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
index 278ba9b0f4db..bab81d79c804 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
@@ -17,40 +17,38 @@
package com.android.wm.shell.flicker.bubble
import android.app.INotificationManager
-import android.app.Instrumentation
import android.app.NotificationManager
import android.content.Context
+import android.content.pm.PackageManager
import android.os.ServiceManager
-import android.view.Surface
-import androidx.test.platform.app.InstrumentationRegistry
+import android.tools.common.Rotation
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.IFlickerTestData
+import android.tools.device.helpers.SYSTEMUI_PACKAGE
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.Flicker
-import com.android.server.wm.flicker.FlickerBuilderProvider
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE
-import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper
+import com.android.server.wm.flicker.helpers.LaunchBubbleHelper
+import com.android.wm.shell.flicker.BaseTest
import org.junit.runners.Parameterized
-/**
- * Base configurations for Bubble flicker tests
- */
-abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) {
+/** Base configurations for Bubble flicker tests */
+abstract class BaseBubbleScreen(flicker: FlickerTest) : BaseTest(flicker) {
- protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
protected val context: Context = instrumentation.context
protected val testApp = LaunchBubbleHelper(instrumentation)
- protected val notifyManager = INotificationManager.Stub.asInterface(
- ServiceManager.getService(Context.NOTIFICATION_SERVICE))
-
- protected val uid = context.packageManager.getApplicationInfo(
- testApp.component.packageName, 0).uid
+ private val notifyManager =
+ INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE)
+ )
- protected abstract val transition: FlickerBuilder.() -> Unit
+ private val uid =
+ context.packageManager
+ .getApplicationInfo(testApp.`package`, PackageManager.ApplicationInfoFlags.of(0))
+ .uid
@JvmOverloads
protected open fun buildTransition(
@@ -58,46 +56,41 @@ abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) {
): FlickerBuilder.() -> Unit {
return {
setup {
- test {
- notifyManager.setBubblesAllowed(testApp.component.packageName,
- uid, NotificationManager.BUBBLE_PREFERENCE_ALL)
- testApp.launchViaIntent(wmHelper)
- waitAndGetAddBubbleBtn()
- waitAndGetCancelAllBtn()
- }
+ notifyManager.setBubblesAllowed(
+ testApp.`package`,
+ uid,
+ NotificationManager.BUBBLE_PREFERENCE_ALL
+ )
+ testApp.launchViaIntent(wmHelper)
+ waitAndGetAddBubbleBtn()
+ waitAndGetCancelAllBtn()
}
teardown {
- test {
- notifyManager.setBubblesAllowed(testApp.component.packageName,
- uid, NotificationManager.BUBBLE_PREFERENCE_NONE)
- testApp.exit()
- }
+ notifyManager.setBubblesAllowed(
+ testApp.`package`,
+ uid,
+ NotificationManager.BUBBLE_PREFERENCE_NONE
+ )
+ testApp.exit()
}
extraSpec(this)
}
}
- protected fun Flicker.waitAndGetAddBubbleBtn(): UiObject2? = device.wait(Until.findObject(
- By.text("Add Bubble")), FIND_OBJECT_TIMEOUT)
- protected fun Flicker.waitAndGetCancelAllBtn(): UiObject2? = device.wait(Until.findObject(
- By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT)
-
- @FlickerBuilderProvider
- fun buildFlicker(): FlickerBuilder {
- return FlickerBuilder(instrumentation).apply {
- transition(this)
- }
- }
+ protected fun IFlickerTestData.waitAndGetAddBubbleBtn(): UiObject2? =
+ device.wait(Until.findObject(By.text("Add Bubble")), FIND_OBJECT_TIMEOUT)
+ protected fun IFlickerTestData.waitAndGetCancelAllBtn(): UiObject2? =
+ device.wait(Until.findObject(By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT)
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3)
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
}
const val FIND_OBJECT_TIMEOUT = 2000L
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
index 8d1e315e2d5e..2474ecf74cf9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
@@ -17,19 +17,17 @@
package com.android.wm.shell.flicker.bubble
import android.os.SystemClock
+import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
import androidx.test.filters.RequiresDevice
import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import org.junit.Assume
-import org.junit.Before
-import org.junit.runner.RunWith
import org.junit.Test
+import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
@@ -38,38 +36,42 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTests:MultiBubblesScreen`
*
* Actions:
+ * ```
* Switch in different bubble notifications
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
-
- @Before
- open fun before() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- }
-
+@FlakyTest(bugId = 217777115)
+open class ChangeActiveActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+ /** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = buildTransition {
setup {
- test {
- for (i in 1..3) {
- val addBubbleBtn = waitAndGetAddBubbleBtn()
- addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found")
- }
- val showBubble = device.wait(Until.findObject(
- By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT)
- showBubble?.run { showBubble.click() } ?: error("Show bubble not found")
+ for (i in 1..3) {
+ val addBubbleBtn = waitAndGetAddBubbleBtn() ?: error("Add Bubble not found")
+ addBubbleBtn.click()
SystemClock.sleep(1000)
}
+ val showBubble =
+ device.wait(
+ Until.findObject(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
+ FIND_OBJECT_TIMEOUT
+ )
+ ?: error("Show bubble not found")
+ showBubble.click()
+ SystemClock.sleep(1000)
}
transitions {
- val bubbles = device.wait(Until.findObjects(
- By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT)
+ val bubbles: List<UiObject2> =
+ device.wait(
+ Until.findObjects(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
+ FIND_OBJECT_TIMEOUT
+ )
+ ?: error("No bubbles found")
for (entry in bubbles) {
- entry?.run { entry.click() } ?: error("Bubble not found")
+ entry.click()
SystemClock.sleep(1000)
}
}
@@ -78,8 +80,6 @@ open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen
@Presubmit
@Test
open fun testAppIsAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(testApp.component)
- }
+ flicker.assertLayers { this.isVisible(testApp) }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
index ddebb6fed636..bdfdad59c600 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -16,27 +16,12 @@
package com.android.wm.shell.flicker.bubble
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import org.junit.Assume
-import org.junit.Before
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-@FlakyTest(bugId = 217777115)
-class MultiBubblesScreenShellTransit(
- testSpec: FlickerTestParameter
-) : MultiBubblesScreen(testSpec) {
- @Before
- override fun before() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- }
-} \ No newline at end of file
+open class ChangeActiveActivityFromBubbleTestCfArm(flicker: FlickerTest) :
+ ChangeActiveActivityFromBubbleTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
index b137e92881a5..8474ce0e64e5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
@@ -19,17 +19,16 @@ package com.android.wm.shell.flicker.bubble
import android.content.Context
import android.graphics.Point
import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.test.filters.RequiresDevice
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import org.junit.runner.RunWith
import org.junit.Test
+import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
@@ -38,30 +37,33 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTests:DismissBubbleScreen`
*
* Actions:
+ * ```
* Dismiss a bubble notification
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+open class DragToDismissBubbleScreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
private val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val displaySize = DisplayMetrics()
+ /** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = buildTransition {
setup {
- eachRun {
- val addBubbleBtn = waitAndGetAddBubbleBtn()
- addBubbleBtn?.click() ?: error("Add Bubble not found")
- }
+ val addBubbleBtn = waitAndGetAddBubbleBtn()
+ addBubbleBtn?.click() ?: error("Add Bubble not found")
}
transitions {
- wm.run { wm.getDefaultDisplay().getMetrics(displaySize) }
+ wm.run { wm.defaultDisplay.getMetrics(displaySize) }
val dist = Point((displaySize.widthPixels / 2), displaySize.heightPixels)
- val showBubble = device.wait(Until.findObject(
- By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT)
+ val showBubble =
+ device.wait(
+ Until.findObject(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
+ FIND_OBJECT_TIMEOUT
+ )
showBubble?.run { drag(dist, 1000) } ?: error("Show bubble not found")
}
}
@@ -69,8 +71,6 @@ open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScree
@Presubmit
@Test
open fun testAppIsAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(testApp.component)
- }
+ flicker.assertLayers { this.isVisible(testApp) }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
new file mode 100644
index 000000000000..62fa7b4516c7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.flicker.bubble
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class DragToDismissBubbleScreenTestCfArm(flicker: FlickerTest) :
+ DragToDismissBubbleScreenTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
deleted file mode 100644
index 684e5cad0e67..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.bubble
-
-import android.platform.test.annotations.Presubmit
-import android.view.WindowInsets
-import android.view.WindowManager
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import org.junit.Assume
-import org.junit.runner.RunWith
-import org.junit.Test
-import org.junit.runners.Parameterized
-
-/**
- * Test launching a new activity from bubble.
- *
- * To run this test: `atest WMShellFlickerTests:LaunchBubbleFromLockScreen`
- *
- * Actions:
- * Launch an bubble from notification on lock screen
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-class LaunchBubbleFromLockScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition {
- setup {
- eachRun {
- val addBubbleBtn = waitAndGetAddBubbleBtn()
- addBubbleBtn?.click() ?: error("Bubble widget not found")
- device.sleep()
- wmHelper.waitFor("noAppWindowsOnTop") {
- it.wmState.topVisibleAppWindow.isEmpty()
- }
- device.wakeUp()
- }
- }
- transitions {
- // Swipe & wait for the notification shade to expand so all can be seen
- val wm = context.getSystemService(WindowManager::class.java)
- val metricInsets = wm.getCurrentWindowMetrics().windowInsets
- val insets = metricInsets.getInsetsIgnoringVisibility(
- WindowInsets.Type.statusBars()
- or WindowInsets.Type.displayCutout())
- device.swipe(100, insets.top + 100, 100, device.getDisplayHeight() / 2, 4)
- device.waitForIdle(2000)
- instrumentation.uiAutomation.syncInputTransactions()
-
- val notification = device.wait(Until.findObject(
- By.text("BubbleChat")), FIND_OBJECT_TIMEOUT)
- notification?.click() ?: error("Notification not found")
- instrumentation.uiAutomation.syncInputTransactions()
- val showBubble = device.wait(Until.findObject(
- By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT)
- showBubble?.click() ?: error("Bubble notify not found")
- instrumentation.uiAutomation.syncInputTransactions()
- val cancelAllBtn = waitAndGetCancelAllBtn()
- cancelAllBtn?.click() ?: error("Cancel widget not found")
- }
- }
-
- @Presubmit
- @Test
- fun testAppIsVisibleAtEnd() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- testSpec.assertLayersEnd {
- this.isVisible(testApp.component)
- }
- }
-
- @FlakyTest
- @Test
- fun testAppIsVisibleAtEnd_ShellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- testSpec.assertLayersEnd {
- this.isVisible(testApp.component)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
new file mode 100644
index 000000000000..889e1771593d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.bubble
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.view.WindowInsets
+import android.view.WindowManager
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd
+import com.android.server.wm.flicker.navBarLayerPositionAtEnd
+import org.junit.Assume
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a new activity from bubble.
+ *
+ * To run this test: `atest WMShellFlickerTests:OpenActivityFromBubbleOnLocksreenTest`
+ *
+ * Actions:
+ * ```
+ * Launch an bubble from notification on lock screen
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class OpenActivityFromBubbleOnLocksreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = buildTransition {
+ setup {
+ val addBubbleBtn = waitAndGetAddBubbleBtn()
+ addBubbleBtn?.click() ?: error("Bubble widget not found")
+ device.sleep()
+ wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+ device.wakeUp()
+ }
+ transitions {
+ // Swipe & wait for the notification shade to expand so all can be seen
+ val wm =
+ context.getSystemService(WindowManager::class.java)
+ ?: error("Unable to obtain WM service")
+ val metricInsets = wm.currentWindowMetrics.windowInsets
+ val insets =
+ metricInsets.getInsetsIgnoringVisibility(
+ WindowInsets.Type.statusBars() or WindowInsets.Type.displayCutout()
+ )
+ device.swipe(100, insets.top + 100, 100, device.displayHeight / 2, 4)
+ device.waitForIdle(2000)
+ instrumentation.uiAutomation.syncInputTransactions()
+
+ val notification =
+ device.wait(Until.findObject(By.text("BubbleChat")), FIND_OBJECT_TIMEOUT)
+ notification?.click() ?: error("Notification not found")
+ instrumentation.uiAutomation.syncInputTransactions()
+ val showBubble =
+ device.wait(
+ Until.findObject(By.res("com.android.systemui", "bubble_view")),
+ FIND_OBJECT_TIMEOUT
+ )
+ showBubble?.click() ?: error("Bubble notify not found")
+ instrumentation.uiAutomation.syncInputTransactions()
+ val cancelAllBtn = waitAndGetCancelAllBtn()
+ cancelAllBtn?.click() ?: error("Cancel widget not found")
+ }
+ }
+
+ @FlakyTest(bugId = 242088970)
+ @Test
+ fun testAppIsVisibleAtEnd() {
+ flicker.assertLayersEnd { this.isVisible(testApp) }
+ }
+
+ @Postsubmit
+ @Test
+ fun navBarLayerIsVisibleAtEnd() {
+ Assume.assumeFalse(flicker.scenario.isTablet)
+ flicker.navBarLayerIsVisibleAtEnd()
+ }
+
+ @Postsubmit
+ @Test
+ fun navBarLayerPositionAtEnd() {
+ Assume.assumeFalse(flicker.scenario.isTablet)
+ flicker.navBarLayerPositionAtEnd()
+ }
+
+ /** {@inheritDoc} */
+ @FlakyTest
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() {
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ super.navBarLayerIsVisibleAtStartAndEnd()
+ }
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() {
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ super.navBarLayerPositionAtStartAndEnd()
+ }
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() {
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ super.navBarWindowIsAlwaysVisible()
+ }
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. Taskbar is not shown on lock screen")
+ override fun taskBarLayerIsVisibleAtStartAndEnd() {}
+
+ @Test
+ @Ignore("Not applicable to this CUJ. Taskbar is not shown on lock screen")
+ override fun taskBarWindowIsAlwaysVisible() {}
+
+ /** Checks that the [ComponentNameMatcher.TASK_BAR] is visible at the end of the transition */
+ @Postsubmit
+ @Test
+ fun taskBarLayerIsVisibleAtEnd() {
+ Assume.assumeTrue(flicker.scenario.isTablet)
+ flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
index f288b0a24d9d..07ba41333071 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
@@ -17,15 +17,14 @@
package com.android.wm.shell.flicker.bubble
import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
import androidx.test.filters.RequiresDevice
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import org.junit.runner.RunWith
import org.junit.Test
+import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
@@ -34,27 +33,30 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen`
*
* Actions:
+ * ```
* Launch an app and enable app's bubble notification
* Send a bubble notification
* The activity for the bubble is launched
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+open class OpenActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+ /** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = buildTransition {
setup {
- test {
- val addBubbleBtn = waitAndGetAddBubbleBtn()
- addBubbleBtn?.click() ?: error("Add Bubble not found")
- }
+ val addBubbleBtn = waitAndGetAddBubbleBtn()
+ addBubbleBtn?.click() ?: error("Add Bubble not found")
}
transitions {
- val showBubble = device.wait(Until.findObject(
- By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT)
+ val showBubble =
+ device.wait(
+ Until.findObject(By.res("com.android.systemui", "bubble_view")),
+ FIND_OBJECT_TIMEOUT
+ )
showBubble?.run { showBubble.click() } ?: error("Bubble notify not found")
}
}
@@ -62,8 +64,6 @@ open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen
@Presubmit
@Test
open fun testAppIsAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(testApp.component)
- }
+ flicker.assertLayers { this.isVisible(testApp) }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
index 471e010cf560..6c61710d6284 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker.helpers
+package com.android.wm.shell.flicker.bubble
-import android.app.Instrumentation
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.wm.shell.flicker.testapp.Components
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
-class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
- instrumentation,
- Components.FixedActivity.LABEL,
- Components.FixedActivity.COMPONENT.toFlickerComponent()
-) \ No newline at end of file
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class OpenActivityFromBubbleTestCfArm(flicker: FlickerTest) : OpenActivityFromBubbleTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
index 0bb4d398bff4..29f76d01af83 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
@@ -17,11 +17,12 @@
package com.android.wm.shell.flicker.bubble
import android.platform.test.annotations.Presubmit
-import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -32,28 +33,34 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen`
*
* Actions:
+ * ```
* Launch an app and enable app's bubble notification
* Send a bubble notification
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@Group4
-open class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+open class SendBubbleNotificationTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+ /** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = buildTransition {
transitions {
val addBubbleBtn = waitAndGetAddBubbleBtn()
addBubbleBtn?.click() ?: error("Bubble widget not found")
+
+ device.wait(
+ Until.findObjects(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
+ FIND_OBJECT_TIMEOUT
+ )
+ ?: error("No bubbles found")
}
}
@Presubmit
@Test
open fun testAppIsAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(testApp.component)
- }
+ flicker.assertLayers { this.isVisible(testApp) }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
new file mode 100644
index 000000000000..e323ebf3b5c8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.flicker.bubble
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+open class SendBubbleNotificationTestCfArm(flicker: FlickerTest) :
+ SendBubbleNotificationTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
deleted file mode 100644
index 41cd31aabf05..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.helpers
-
-import android.app.Instrumentation
-import com.android.server.wm.flicker.Flicker
-import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.server.wm.traces.common.region.Region
-
-class AppPairsHelper(
- instrumentation: Instrumentation,
- activityLabel: String,
- component: FlickerComponentName
-) : BaseAppHelper(instrumentation, activityLabel, component) {
- fun getPrimaryBounds(dividerBounds: Region): Region {
- val primaryAppBounds = Region.from(0, 0, dividerBounds.bounds.right,
- dividerBounds.bounds.bottom + WindowUtils.dockedStackDividerInset)
- return primaryAppBounds
- }
-
- fun getSecondaryBounds(dividerBounds: Region): Region {
- val displayBounds = WindowUtils.displayBounds
- val secondaryAppBounds = Region.from(0,
- dividerBounds.bounds.bottom - WindowUtils.dockedStackDividerInset,
- displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarFrameHeight)
- return secondaryAppBounds
- }
-
- companion object {
- const val TEST_REPETITIONS = 1
- const val TIMEOUT_MS = 3_000L
-
- fun Flicker.waitAppsShown(app1: SplitScreenHelper?, app2: SplitScreenHelper?) {
- wmHelper.waitFor("primaryAndSecondaryAppsVisible") { dump ->
- val primaryAppVisible = app1?.let {
- dump.wmState.isWindowSurfaceShown(app1.defaultWindowName)
- } ?: false
- val secondaryAppVisible = app2?.let {
- dump.wmState.isWindowSurfaceShown(app2.defaultWindowName)
- } ?: false
- primaryAppVisible && secondaryAppVisible
- }
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
deleted file mode 100644
index 3dd9e0572947..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.helpers
-
-import android.app.Instrumentation
-import android.content.pm.PackageManager.FEATURE_LEANBACK
-import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY
-import android.os.SystemProperties
-import android.support.test.launcherhelper.LauncherStrategyFactory
-import android.util.Log
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
-import com.android.compatibility.common.util.SystemUtil
-import com.android.server.wm.flicker.helpers.StandardAppHelper
-import com.android.server.wm.traces.common.FlickerComponentName
-import java.io.IOException
-
-abstract class BaseAppHelper(
- instrumentation: Instrumentation,
- launcherName: String,
- component: FlickerComponentName
-) : StandardAppHelper(
- instrumentation,
- launcherName,
- component,
- LauncherStrategyFactory.getInstance(instrumentation).launcherStrategy
-) {
- private val appSelector = By.pkg(component.packageName).depth(0)
-
- protected val isTelevision: Boolean
- get() = context.packageManager.run {
- hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY)
- }
-
- val defaultWindowName: String
- get() = component.toWindowName()
-
- val ui: UiObject2?
- get() = uiDevice.findObject(appSelector)
-
- fun waitUntilClosed(): Boolean {
- return uiDevice.wait(Until.gone(appSelector), APP_CLOSE_WAIT_TIME_MS)
- }
-
- companion object {
- private const val APP_CLOSE_WAIT_TIME_MS = 3_000L
-
- fun isShellTransitionsEnabled() =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false)
-
- fun executeShellCommand(instrumentation: Instrumentation, cmd: String) {
- try {
- SystemUtil.runShellCommand(instrumentation, cmd)
- } catch (e: IOException) {
- Log.e("BaseAppHelper", "executeShellCommand error! $e")
- }
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
deleted file mode 100644
index cc5b9f9eb26d..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.helpers
-
-import android.app.Instrumentation
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.helpers.FIND_TIMEOUT
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.testapp.Components
-
-open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
- instrumentation,
- Components.ImeActivity.LABEL,
- Components.ImeActivity.COMPONENT.toFlickerComponent()
-) {
- /**
- * Opens the IME and wait for it to be displayed
- *
- * @param wmHelper Helper used to wait for WindowManager states
- */
- @JvmOverloads
- open fun openIME(wmHelper: WindowManagerStateHelper? = null) {
- if (!isTelevision) {
- val editText = uiDevice.wait(
- Until.findObject(By.res(getPackage(), "plain_text_input")),
- FIND_TIMEOUT)
-
- require(editText != null) {
- "Text field not found, this usually happens when the device " +
- "was left in an unknown state (e.g. in split screen)"
- }
- editText.click()
- waitAndAssertIMEShown(uiDevice, wmHelper)
- } else {
- // If we do the same thing as above - editText.click() - on TV, that's going to force TV
- // into the touch mode. We really don't want that.
- launchViaIntent(action = Components.ImeActivity.ACTION_OPEN_IME)
- }
- }
-
- protected fun waitAndAssertIMEShown(
- device: UiDevice,
- wmHelper: WindowManagerStateHelper? = null
- ) {
- if (wmHelper == null) {
- device.waitForIdle()
- } else {
- wmHelper.waitImeShown()
- }
- }
-
- /**
- * Opens the IME and wait for it to be gone
- *
- * @param wmHelper Helper used to wait for WindowManager states
- */
- @JvmOverloads
- open fun closeIME(wmHelper: WindowManagerStateHelper? = null) {
- if (!isTelevision) {
- uiDevice.pressBack()
- // Using only the AccessibilityInfo it is not possible to identify if the IME is active
- if (wmHelper == null) {
- uiDevice.waitForIdle()
- } else {
- wmHelper.waitImeGone()
- }
- } else {
- // While pressing the back button should close the IME on TV as well, it may also lead
- // to the app closing. So let's instead just ask the app to close the IME.
- launchViaIntent(action = Components.ImeActivity.ACTION_CLOSE_IME)
- }
- }
-} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt
deleted file mode 100644
index 12ccbafce651..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.helpers
-
-import android.app.Instrumentation
-import android.content.Context
-import android.provider.Settings
-import com.android.server.wm.traces.common.FlickerComponentName
-
-class MultiWindowHelper(
- instrumentation: Instrumentation,
- activityLabel: String,
- componentsInfo: FlickerComponentName
-) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) {
-
- companion object {
- fun getDevEnableNonResizableMultiWindow(context: Context): Int =
- Settings.Global.getInt(context.contentResolver,
- Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW)
-
- fun setDevEnableNonResizableMultiWindow(context: Context, configValue: Int) =
- Settings.Global.putInt(context.contentResolver,
- Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW, configValue)
-
- fun setSupportsNonResizableMultiWindow(instrumentation: Instrumentation, configValue: Int) =
- executeShellCommand(
- instrumentation,
- createConfigSupportsNonResizableMultiWindowCommand(configValue))
-
- fun resetMultiWindowConfig(instrumentation: Instrumentation) =
- executeShellCommand(instrumentation, resetMultiWindowConfigCommand)
-
- private fun createConfigSupportsNonResizableMultiWindowCommand(configValue: Int): String =
- "wm set-multi-window-config --supportsNonResizable $configValue"
-
- private const val resetMultiWindowConfigCommand: String = "wm reset-multi-window-config"
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
deleted file mode 100644
index 8157a4e453af..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.helpers
-
-import android.app.Instrumentation
-import android.media.session.MediaController
-import android.media.session.MediaSessionManager
-import android.os.SystemClock
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.helpers.FIND_TIMEOUT
-import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE
-import com.android.server.wm.traces.common.Rect
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow
-import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild
-import com.android.wm.shell.flicker.testapp.Components
-
-class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
- instrumentation,
- Components.PipActivity.LABEL,
- Components.PipActivity.COMPONENT.toFlickerComponent()
-) {
- private val mediaSessionManager: MediaSessionManager
- get() = context.getSystemService(MediaSessionManager::class.java)
- ?: error("Could not get MediaSessionManager")
-
- private val mediaController: MediaController?
- get() = mediaSessionManager.getActiveSessions(null).firstOrNull {
- it.packageName == component.packageName
- }
-
- fun clickObject(resId: String) {
- val selector = By.res(component.packageName, resId)
- val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object")
-
- if (!isTelevision) {
- obj.click()
- } else {
- focusOnObject(selector) || error("Could not focus on `$resId` object")
- uiDevice.pressDPadCenter()
- }
- }
-
- /**
- * Launches the app through an intent instead of interacting with the launcher and waits
- * until the app window is in PIP mode
- */
- @JvmOverloads
- fun launchViaIntentAndWaitForPip(
- wmHelper: WindowManagerStateHelper,
- expectedWindowName: String = "",
- action: String? = null,
- stringExtras: Map<String, String>
- ) {
- launchViaIntentAndWaitShown(
- wmHelper, expectedWindowName, action, stringExtras,
- waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition)
- )
- }
-
- /**
- * Expand the PIP window back to full screen via intent and wait until the app is visible
- */
- fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) =
- launchViaIntentAndWaitShown(wmHelper)
-
- private fun focusOnObject(selector: BySelector): Boolean {
- // We expect all the focusable UI elements to be arranged in a way so that it is possible
- // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top"
- // from "the bottom".
- repeat(FOCUS_ATTEMPTS) {
- uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true }
- ?: error("The object we try to focus on is gone.")
-
- uiDevice.pressDPadDown()
- uiDevice.waitForIdle()
- }
- return false
- }
-
- @JvmOverloads
- fun clickEnterPipButton(wmHelper: WindowManagerStateHelper? = null) {
- clickObject(ENTER_PIP_BUTTON_ID)
-
- // Wait on WMHelper or simply wait for 3 seconds
- wmHelper?.waitPipShown() ?: SystemClock.sleep(3_000)
- // when entering pip, the dismiss button is visible at the start. to ensure the pip
- // animation is complete, wait until the pip dismiss button is no longer visible.
- // b/176822698: dismiss-only state will be removed in the future
- uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT)
- }
-
- fun enableEnterPipOnUserLeaveHint() {
- clickObject(ENTER_PIP_ON_USER_LEAVE_HINT)
- }
-
- fun enableAutoEnterForPipActivity() {
- clickObject(ENTER_PIP_AUTOENTER)
- }
-
- fun clickStartMediaSessionButton() {
- clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID)
- }
-
- fun checkWithCustomActionsCheckbox() = uiDevice
- .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID))
- ?.takeIf { it.isCheckable }
- ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) }
- ?: error("'With custom actions' checkbox not found")
-
- fun pauseMedia() = mediaController?.transportControls?.pause()
- ?: error("No active media session found")
-
- fun stopMedia() = mediaController?.transportControls?.stop()
- ?: error("No active media session found")
-
- @Deprecated(
- "Use PipAppHelper.closePipWindow(wmHelper) instead",
- ReplaceWith("closePipWindow(wmHelper)")
- )
- fun closePipWindow() {
- if (isTelevision) {
- uiDevice.closeTvPipWindow()
- } else {
- closePipWindow(WindowManagerStateHelper(mInstrumentation))
- }
- }
-
- private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect {
- val windowRegion = wmHelper.getWindowRegion(component)
- require(!windowRegion.isEmpty) {
- "Unable to find a PIP window in the current state"
- }
- return windowRegion.bounds
- }
-
- /**
- * Taps the pip window and dismisses it by clicking on the X button.
- */
- fun closePipWindow(wmHelper: WindowManagerStateHelper) {
- if (isTelevision) {
- uiDevice.closeTvPipWindow()
- } else {
- val windowRect = getWindowRect(wmHelper)
- uiDevice.click(windowRect.centerX(), windowRect.centerY())
- // search and interact with the dismiss button
- val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
- uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
- val dismissPipObject = uiDevice.findObject(dismissSelector)
- ?: error("PIP window dismiss button not found")
- val dismissButtonBounds = dismissPipObject.visibleBounds
- uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY())
- }
-
- // Wait for animation to complete.
- wmHelper.waitPipGone()
- wmHelper.waitForHomeActivityVisible()
- }
-
- /**
- * Close the pip window by pressing the expand button
- */
- fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) {
- val windowRect = getWindowRect(wmHelper)
- uiDevice.click(windowRect.centerX(), windowRect.centerY())
- // search and interact with the expand button
- val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button")
- uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT)
- val expandPipObject = uiDevice.findObject(expandSelector)
- ?: error("PIP window expand button not found")
- val expandButtonBounds = expandPipObject.visibleBounds
- uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY())
- wmHelper.waitPipGone()
- wmHelper.waitForAppTransitionIdle()
- }
-
- /**
- * Double click on the PIP window to expand it
- */
- fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) {
- val windowRect = getWindowRect(wmHelper)
- uiDevice.click(windowRect.centerX(), windowRect.centerY())
- uiDevice.click(windowRect.centerX(), windowRect.centerY())
- wmHelper.waitForAppTransitionIdle()
- }
-
- companion object {
- private const val FOCUS_ATTEMPTS = 20
- private const val ENTER_PIP_BUTTON_ID = "enter_pip"
- private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions"
- private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start"
- private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual"
- private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter"
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
deleted file mode 100644
index 49eca63a23ec..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.helpers
-
-import android.app.Instrumentation
-import android.graphics.Point
-import android.os.SystemClock
-import android.view.InputDevice
-import android.view.MotionEvent
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.Until
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.testapp.Components
-import org.junit.Assert
-
-class SplitScreenHelper(
- instrumentation: Instrumentation,
- activityLabel: String,
- componentsInfo: FlickerComponentName
-) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) {
-
- companion object {
- const val TEST_REPETITIONS = 1
- const val TIMEOUT_MS = 3_000L
- const val DRAG_DURATION_MS = 1_000L
- const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
- const val GESTURE_STEP_MS = 16L
-
- private val notificationScrollerSelector: BySelector
- get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
- private val notificationContentSelector: BySelector
- get() = By.text("Notification content")
-
- fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper =
- SplitScreenHelper(
- instrumentation,
- Components.SplitScreenActivity.LABEL,
- Components.SplitScreenActivity.COMPONENT.toFlickerComponent()
- )
-
- fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper =
- SplitScreenHelper(
- instrumentation,
- Components.SplitScreenSecondaryActivity.LABEL,
- Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()
- )
-
- fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper =
- SplitScreenHelper(
- instrumentation,
- Components.NonResizeableActivity.LABEL,
- Components.NonResizeableActivity.COMPONENT.toFlickerComponent()
- )
-
- fun getSendNotification(instrumentation: Instrumentation): SplitScreenHelper =
- SplitScreenHelper(
- instrumentation,
- Components.SendNotificationActivity.LABEL,
- Components.SendNotificationActivity.COMPONENT.toFlickerComponent()
- )
-
- fun dragFromNotificationToSplit(
- instrumentation: Instrumentation,
- device: UiDevice,
- wmHelper: WindowManagerStateHelper
- ) {
- val displayBounds = wmHelper.currentState.layerState
- .displays.firstOrNull { !it.isVirtual }
- ?.layerStackSpace
- ?: error("Display not found")
-
- // Pull down the notifications
- device.swipe(
- displayBounds.centerX(), 5,
- displayBounds.centerX(), displayBounds.bottom, 20 /* steps */
- )
- SystemClock.sleep(TIMEOUT_MS)
-
- // Find the target notification
- val notificationScroller = device.wait(
- Until.findObject(notificationScrollerSelector), TIMEOUT_MS
- )
- var notificationContent = notificationScroller.findObject(notificationContentSelector)
-
- while (notificationContent == null) {
- device.swipe(
- displayBounds.centerX(), displayBounds.centerY(),
- displayBounds.centerX(), displayBounds.centerY() - 150, 20 /* steps */
- )
- notificationContent = notificationScroller.findObject(notificationContentSelector)
- }
-
- // Drag to split
- var dragStart = notificationContent.visibleCenter
- var dragMiddle = Point(dragStart.x + 50, dragStart.y)
- var dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
- val downTime = SystemClock.uptimeMillis()
-
- touch(
- instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime,
- TIMEOUT_MS, dragStart
- )
- // It needs a horizontal movement to trigger the drag
- touchMove(
- instrumentation, downTime, SystemClock.uptimeMillis(),
- DRAG_DURATION_MS, dragStart, dragMiddle
- )
- touchMove(
- instrumentation, downTime, SystemClock.uptimeMillis(),
- DRAG_DURATION_MS, dragMiddle, dragEnd
- )
- // Wait for a while to start splitting
- SystemClock.sleep(TIMEOUT_MS)
- touch(
- instrumentation, MotionEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(),
- GESTURE_STEP_MS, dragEnd
- )
- SystemClock.sleep(TIMEOUT_MS)
- }
-
- fun touch(
- instrumentation: Instrumentation,
- action: Int,
- downTime: Long,
- eventTime: Long,
- duration: Long,
- point: Point
- ) {
- val motionEvent = MotionEvent.obtain(
- downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0
- )
- motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
- instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
- motionEvent.recycle()
- SystemClock.sleep(duration)
- }
-
- fun touchMove(
- instrumentation: Instrumentation,
- downTime: Long,
- eventTime: Long,
- duration: Long,
- from: Point,
- to: Point
- ) {
- val steps: Long = duration / GESTURE_STEP_MS
- var currentTime = eventTime
- var currentX = from.x.toFloat()
- var currentY = from.y.toFloat()
- val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
- val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
-
- for (i in 1..steps) {
- val motionMove = MotionEvent.obtain(
- downTime, currentTime, MotionEvent.ACTION_MOVE, currentX, currentY, 0
- )
- motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
- instrumentation.uiAutomation.injectInputEvent(motionMove, true)
- motionMove.recycle()
-
- currentTime += GESTURE_STEP_MS
- if (i == steps - 1) {
- currentX = to.x.toFloat()
- currentY = to.y.toFloat()
- } else {
- currentX += stepX
- currentY += stepY
- }
- SystemClock.sleep(GESTURE_STEP_MS)
- }
- }
-
- fun createShortcutOnHotseatIfNotExist(
- taplInstrumentation: LauncherInstrumentation,
- appName: String
- ) {
- taplInstrumentation.workspace
- .deleteAppIcon(taplInstrumentation.workspace.getHotseatAppIcon(0))
- val allApps = taplInstrumentation.workspace.switchToAllApps()
- allApps.freeze()
- try {
- val appIconSrc = allApps.getAppIcon(appName)
- Assert.assertNotNull("Unable to find app icon", appIconSrc)
- val appIconDest = appIconSrc.dragToHotseat(0)
- Assert.assertNotNull("Unable to drag app icon on hotseat", appIconDest)
- } finally {
- allApps.unfreeze()
- }
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
new file mode 100644
index 000000000000..f7ce87088040
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.app.Instrumentation
+import android.os.SystemClock
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.helpers.WindowUtils
+import android.tools.device.traces.parsers.toFlickerComponent
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test entering pip from an app via auto-enter property when navigating to home from split screen.
+ *
+ * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest`
+ *
+ * Actions:
+ * ```
+ * Launch an app in full screen
+ * Select "Auto-enter PiP" radio button
+ * Open all apps and drag another app icon to enter split screen
+ * Press Home button or swipe up to go Home and put [pipApp] in pip mode
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. All assertions are inherited from [EnterPipTest]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class AutoEnterPipFromSplitScreenOnGoToHomeTest(flicker: FlickerTest) :
+ AutoEnterPipOnGoToHomeTest(flicker) {
+ private val portraitDisplayBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
+ /** Second app used to enter split screen mode */
+ protected val secondAppForSplitScreen = getSplitScreenApp(instrumentation)
+ fun getSplitScreenApp(instrumentation: Instrumentation): StandardAppHelper =
+ SimpleAppHelper(
+ instrumentation,
+ ActivityOptions.SplitScreen.Primary.LABEL,
+ ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
+ )
+
+ /** Defines the transition used to run the test */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ secondAppForSplitScreen.launchViaIntent(wmHelper)
+ pipApp.launchViaIntent(wmHelper)
+ tapl.goHome()
+ enterSplitScreen()
+ // wait until split screen is established
+ wmHelper
+ .StateSyncBuilder()
+ .withWindowSurfaceAppeared(pipApp)
+ .withWindowSurfaceAppeared(secondAppForSplitScreen)
+ .withSplitDividerVisible()
+ .waitForAndVerify()
+ pipApp.enableAutoEnterForPipActivity()
+ }
+ teardown {
+ // close gracefully so that onActivityUnpinned() can be called before force exit
+ pipApp.closePipWindow(wmHelper)
+ pipApp.exit(wmHelper)
+ secondAppForSplitScreen.exit(wmHelper)
+ }
+ transitions { tapl.goHome() }
+ }
+
+ // TODO(b/285400227) merge the code in a common utility - this is copied from SplitScreenUtils
+ private val TIMEOUT_MS = 3_000L
+ private val overviewSnapshotSelector: BySelector
+ get() = By.res(LAUNCHER_UI_PACKAGE_NAME, "snapshot")
+ private fun enterSplitScreen() {
+ // Note: The initial split position in landscape is different between tablet and phone.
+ // In landscape, tablet will let the first app split to right side, and phone will
+ // split to left side.
+ if (tapl.isTablet) {
+ // TAPL's currentTask on tablet is sometimes not what we expected if the overview
+ // contains more than 3 task views. We need to use uiautomator directly to find the
+ // second task to split.
+ tapl.workspace.switchToOverview().overviewActions.clickSplit()
+ val snapshots = tapl.device.wait(Until.findObjects(overviewSnapshotSelector),
+ TIMEOUT_MS)
+ if (snapshots == null || snapshots.size < 1) {
+ error("Fail to find a overview snapshot to split.")
+ }
+
+ // Find the second task in the upper right corner in split select mode by sorting
+ // 'left' in descending order and 'top' in ascending order.
+ snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+ t2.getVisibleBounds().left - t1.getVisibleBounds().left
+ }
+ snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+ t1.getVisibleBounds().top - t2.getVisibleBounds().top
+ }
+ snapshots[0].click()
+ } else {
+ tapl.workspace
+ .switchToOverview()
+ .currentTask
+ .tapMenu()
+ .tapSplitMenuItem()
+ .currentTask
+ .open()
+ }
+ SystemClock.sleep(TIMEOUT_MS)
+ }
+
+ @Presubmit
+ @Test
+ override fun pipOverlayLayerAppearThenDisappear() {
+ // when entering from split screen we use alpha animation, without overlay
+ }
+
+ @Presubmit
+ @Test
+ override fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ // when entering from split screen we use alpha animation, without overlay
+ }
+
+ @Presubmit
+ @Test
+ override fun pipLayerReduces() {
+ // when entering from split screen we use alpha animation, so there is no size change
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipLayerReduces()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipAppLayerAlwaysVisible() {
+ // pip layer in gesture nav will disappear during transition with alpha animation
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipAppLayerAlwaysVisible()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipWindowRemainInsideVisibleBounds() {
+ if (tapl.isTablet) {
+ flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+ } else {
+ // on phones home does not rotate in landscape, PiP enters back to portrait
+ // orientation so use display bounds from that orientation for assertion
+ flicker.assertWmVisibleRegion(pipApp) { coversAtMost(portraitDisplayBounds) }
+ }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index ce624f2b5bbe..b95732e43357 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -16,13 +16,11 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
import androidx.test.filters.RequiresDevice
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
@@ -36,69 +34,64 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest`
*
* Actions:
+ * ```
* Launch an app in full screen
* Select "Auto-enter PiP" radio button
* Press Home button or swipe up to go Home and put [pipApp] in pip mode
+ * ```
*
* Notes:
+ * ```
* 1. All assertions are inherited from [EnterPipTest]
* 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
* including configuring navigation mode, initial orientation and ensuring no
* apps are running before setup
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@FlakyTest(bugId = 238367575)
-@Group3
-class AutoEnterPipOnGoToHomeTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) {
- protected val taplInstrumentation = LauncherInstrumentation()
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setupAndTeardown(this)
- setup {
- eachRun {
- pipApp.launchViaIntent(wmHelper)
- pipApp.enableAutoEnterForPipActivity()
- }
- }
- teardown {
- eachRun {
- // close gracefully so that onActivityUnpinned() can be called before force exit
- pipApp.closePipWindow(wmHelper)
- pipApp.exit(wmHelper)
- }
- }
- transitions {
- taplInstrumentation.goHome()
- }
+open class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { tapl.goHome() }
+ }
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ pipApp.launchViaIntent(wmHelper)
+ pipApp.enableAutoEnterForPipActivity()
}
+ }
+ override val defaultTeardown: FlickerBuilder.() -> Unit = {
+ teardown {
+ // close gracefully so that onActivityUnpinned() can be called before force exit
+ pipApp.closePipWindow(wmHelper)
+ pipApp.exit(wmHelper)
+ }
+ }
+
+ @Presubmit
+ @Test
override fun pipLayerReduces() {
- val layerName = pipApp.component.toLayerName()
- testSpec.assertLayers {
- val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
pipLayerList.zipWithNext { previous, current ->
current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
}
}
}
- /**
- * Checks that [pipApp] window is animated towards default position in right bottom corner
- */
+ /** Checks that [pipApp] window is animated towards default position in right bottom corner */
+ @Presubmit
@Test
fun pipLayerMovesTowardsRightBottomCorner() {
// in gestural nav the swipe makes PiP first go upwards
- Assume.assumeFalse(testSpec.isGesturalNavigation)
- val layerName = pipApp.component.toLayerName()
- testSpec.assertLayers {
- val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
// Pip animates towards the right bottom corner, but because it is being resized at the
// same time, it is possible it shrinks first quickly below the default position and get
// moved up after that in just few last frames
@@ -108,9 +101,11 @@ class AutoEnterPipOnGoToHomeTest(testSpec: FlickerTestParameter) : EnterPipTest(
}
}
+ @Presubmit
+ @Test
override fun focusChanges() {
// in gestural nav the focus goes to different activity on swipe up
- Assume.assumeFalse(testSpec.isGesturalNavigation)
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
super.focusChanges()
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
new file mode 100644
index 000000000000..afcc1729ed16
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test closing a pip window by swiping it to the bottom-center of the screen
+ *
+ * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest`
+ *
+ * Actions:
+ * ```
+ * Launch an app in pip mode [pipApp],
+ * Swipe the pip window to the bottom-center of the screen and wait it disappear
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class ClosePipBySwipingDownTest(flicker: FlickerTest) : ClosePipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions {
+ val pipRegion = wmHelper.getWindowRegion(pipApp).bounds
+ val pipCenterX = pipRegion.centerX()
+ val pipCenterY = pipRegion.centerY()
+ val displayCenterX = device.displayWidth / 2
+ val barComponent =
+ if (flicker.scenario.isTablet) {
+ ComponentNameMatcher.TASK_BAR
+ } else {
+ ComponentNameMatcher.NAV_BAR
+ }
+ val barLayerHeight =
+ wmHelper.currentState.layerState
+ .getLayerWithBuffer(barComponent)
+ ?.visibleRegion
+ ?.height
+ ?: error("Couldn't find Nav or Task bar layer")
+ // The dismiss button doesn't appear at the complete bottom of the screen,
+ // it appears above the hot seat but `hotseatBarSize` is not available outside
+ // the platform
+ val displayY = (device.displayHeight * 0.9).toInt() - barLayerHeight
+ device.swipe(pipCenterX, pipCenterY, displayCenterX, displayY, 50)
+ // Wait until the other app is no longer visible
+ wmHelper
+ .StateSyncBuilder()
+ .withPipGone()
+ .withWindowSurfaceDisappeared(pipApp)
+ .withAppTransitionIdle()
+ .waitForAndVerify()
+ }
+ }
+
+ /** Checks that the focus doesn't change between windows during the transition */
+ @Presubmit
+ @Test
+ fun focusDoesNotChange() {
+ flicker.assertEventLog { this.focusDoesNotChange() }
+ }
+
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+ // TODO(b/270678766): Enable the assertion after fixing the case:
+ // Assume the PiP task has shadow.
+ // 1. The PiP activity is visible -> Task is invisible because it is occluded by activity.
+ // 2. Activity becomes invisible -> Task is visible because it has shadow.
+ // 3. Task is moved outside screen -> Task becomes invisible.
+ // The assertion is triggered for 2 that the Task is only visible in one frame.
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt
new file mode 100644
index 000000000000..02f60100d069
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ClosePipBySwipingDownTestCfArm(flicker: FlickerTest) : ClosePipBySwipingDownTest(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt
new file mode 100644
index 000000000000..e52b71e602f9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher.Companion.LAUNCHER
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.Test
+import org.junit.runners.Parameterized
+
+/** Base class for exiting pip (closing pip window) without returning to the app */
+abstract class ClosePipTransition(flicker: FlickerTest) : PipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ setup { this.setRotation(flicker.scenario.startRotation) }
+ teardown { this.setRotation(Rotation.ROTATION_0) }
+ }
+
+ /**
+ * Checks that [pipApp] window is pinned and visible at the start and then becomes unpinned and
+ * invisible at the same moment, and remains unpinned and invisible until the end of the
+ * transition
+ */
+ @Presubmit
+ @Test
+ open fun pipWindowBecomesInvisible() {
+ // When Shell transition is enabled, we change the windowing mode at start, but
+ // update the visibility after the transition is finished, so we can't check isNotPinned
+ // and isAppWindowInvisible in the same assertion block.
+ flicker.assertWm {
+ this.invoke("hasPipWindow") {
+ it.isPinned(pipApp).isAppWindowVisible(pipApp).isAppWindowOnTop(pipApp)
+ }
+ .then()
+ .invoke("!hasPipWindow") { it.isNotPinned(pipApp).isAppWindowNotOnTop(pipApp) }
+ }
+ flicker.assertWmEnd { isAppWindowInvisible(pipApp) }
+ }
+
+ /**
+ * Checks that [pipApp] and [LAUNCHER] layers are visible at the start of the transition. Then
+ * [pipApp] layer becomes invisible, and remains invisible until the end of the transition
+ */
+ @Presubmit
+ @Test
+ open fun pipLayerBecomesInvisible() {
+ flicker.assertLayers {
+ this.isVisible(pipApp)
+ .isVisible(LAUNCHER)
+ .then()
+ .isInvisible(pipApp)
+ .isVisible(LAUNCHER)
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
index 437ad893f1d9..86fe583c94e6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
@@ -17,14 +17,10 @@
package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,37 +33,30 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTests:ExitPipWithDismissButtonTest`
*
* Actions:
+ * ```
* Launch an app in pip mode [pipApp],
* Click on the pip window
* Click on dismiss button and wait window disappear
+ * ```
*
* Notes:
+ * ```
* 1. Some default assertions (e.g., nav bar, status bar and screen covered)
* are inherited [PipTransition]
* 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
* including configuring navigation mode, initial orientation and ensuring no
* apps are running before setup
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) {
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- pipApp.closePipWindow(wmHelper)
- }
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
+open class ClosePipWithDismissButtonTest(flicker: FlickerTest) : ClosePipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { pipApp.closePipWindow(wmHelper) }
+ }
/**
* Checks that the focus changes between the pip menu window and the launcher when clicking the
@@ -75,25 +64,7 @@ class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTran
*/
@Presubmit
@Test
- fun focusDoesNotChange() {
- testSpec.assertEventLog {
- this.focusChanges("PipMenuView", "NexusLauncherActivity")
- }
- }
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3)
- }
+ fun focusChanges() {
+ flicker.assertEventLog { this.focusChanges("PipMenuView", "NexusLauncherActivity") }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt
new file mode 100644
index 000000000000..05262feceba5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class ClosePipWithDismissButtonTestCfArm(flicker: FlickerTest) :
+ ClosePipWithDismissButtonTest(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
index 953f59a1f70b..01d67cc35a14 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -16,14 +16,14 @@
package com.android.wm.shell.flicker.pip
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
import androidx.test.filters.RequiresDevice
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
import org.junit.Assume
import org.junit.FixMethodOrder
+import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
@@ -34,78 +34,125 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTests:EnterPipOnUserLeaveHintTest`
*
* Actions:
+ * ```
* Launch an app in full screen
* Select "Via code behind" radio button
* Press Home button or swipe up to go Home and put [pipApp] in pip mode
- *
- * Notes:
- * 1. All assertions are inherited from [EnterPipTest]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) {
- protected val taplInstrumentation = LauncherInstrumentation()
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setupAndTeardown(this)
- setup {
- eachRun {
- pipApp.launchViaIntent(wmHelper)
- pipApp.enableEnterPipOnUserLeaveHint()
- }
- }
- teardown {
- eachRun {
- pipApp.exit(wmHelper)
- }
- }
- transitions {
- taplInstrumentation.goHome()
- }
+open class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { tapl.goHome() }
+ }
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ pipApp.launchViaIntent(wmHelper)
+ pipApp.enableEnterPipOnUserLeaveHint()
}
+ }
+ override val defaultTeardown: FlickerBuilder.() -> Unit = {
+ teardown {
+ // close gracefully so that onActivityUnpinned() can be called before force exit
+ pipApp.closePipWindow(wmHelper)
+ pipApp.exit(wmHelper)
+ }
+ }
+
+ @Presubmit
+ @Test
+ override fun pipAppWindowAlwaysVisible() {
+ // In gestural nav the pip will first move behind home and then above home. The visual
+ // appearance visible->invisible->visible is asserted by pipAppLayerAlwaysVisible().
+ // But the internal states of activity don't need to follow that, such as a temporary
+ // visibility state can be changed quickly outside a transaction so the test doesn't
+ // detect that. Hence, skip the case to avoid restricting the internal implementation.
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipAppWindowAlwaysVisible()
+ }
+
+ @Presubmit
+ @Test
override fun pipAppLayerAlwaysVisible() {
- if (!testSpec.isGesturalNavigation) super.pipAppLayerAlwaysVisible() else {
- // pip layer in gesture nav will disappear during transition
- testSpec.assertLayers {
- this.isVisible(pipApp.component)
- .then().isInvisible(pipApp.component)
- .then().isVisible(pipApp.component)
- }
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipAppLayerAlwaysVisible()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipOverlayLayerAppearThenDisappear() {
+ // no overlay in gesture nav for non-auto enter PiP transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipOverlayLayerAppearThenDisappear()
+ }
+
+ @Presubmit
+ @Test
+ fun pipAppWindowVisibleChanges() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ flicker.assertWm {
+ this.isAppWindowVisible(pipApp)
+ .then()
+ .isAppWindowInvisible(pipApp, isOptional = true)
+ .then()
+ .isAppWindowVisible(pipApp, isOptional = true)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun pipAppLayerVisibleChanges() {
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ // pip layer in gesture nav will disappear during transition
+ flicker.assertLayers {
+ this.isVisible(pipApp).then().isInvisible(pipApp).then().isVisible(pipApp)
}
}
+ @Presubmit
+ @Test
override fun pipLayerReduces() {
// in gestural nav the pip enters through alpha animation
- Assume.assumeFalse(testSpec.isGesturalNavigation)
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
super.pipLayerReduces()
}
+ @Presubmit
+ @Test
override fun focusChanges() {
// in gestural nav the focus goes to different activity on swipe up
- Assume.assumeFalse(testSpec.isGesturalNavigation)
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
super.focusChanges()
}
- override fun pipLayerRemainInsideVisibleBounds() {
- if (!testSpec.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds() else {
- // pip layer in gesture nav will disappear during transition
- testSpec.assertLayersStart {
- this.visibleRegion(pipApp.component).coversAtMost(displayBounds)
- }
- testSpec.assertLayersEnd {
- this.visibleRegion(pipApp.component).coversAtMost(displayBounds)
- }
- }
+ @Presubmit
+ @Test
+ override fun entireScreenCovered() {
+ super.entireScreenCovered()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipLayerOrOverlayRemainInsideVisibleBounds()
+ }
+
+ @Presubmit
+ @Test
+ fun pipLayerRemainInsideVisibleBounds() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ // pip layer in gesture nav will disappear during transition
+ flicker.assertLayersStart { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
+ flicker.assertLayersEnd { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt
new file mode 100644
index 000000000000..90f99c0c4cae
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** This test will fail because of b/264261596 */
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class EnterPipOnUserLeaveHintTestCfArm(flicker: FlickerTest) : EnterPipOnUserLeaveHintTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
deleted file mode 100644
index 61ac49835185..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.LAUNCHER_COMPONENT
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test entering pip from an app by interacting with the app UI
- *
- * To run this test: `atest WMShellFlickerTests:EnterPipTest`
- *
- * Actions:
- * Launch an app in full screen
- * Press an "enter pip" button to put [pipApp] in pip mode
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited from [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
-
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setupAndTeardown(this)
- setup {
- eachRun {
- pipApp.launchViaIntent(wmHelper)
- }
- }
- teardown {
- eachRun {
- pipApp.exit(wmHelper)
- }
- }
- transitions {
- pipApp.clickEnterPipButton(wmHelper)
- }
- }
-
- /**
- * Checks [pipApp] window remains visible throughout the animation
- */
- @Presubmit
- @Test
- fun pipAppWindowAlwaysVisible() {
- testSpec.assertWm {
- this.isAppWindowVisible(pipApp.component)
- }
- }
-
- /**
- * Checks [pipApp] layer remains visible throughout the animation
- */
- @Presubmit
- @Test
- open fun pipAppLayerAlwaysVisible() {
- testSpec.assertLayers {
- this.isVisible(pipApp.component)
- }
- }
-
- /**
- * Checks that the pip app window remains inside the display bounds throughout the whole
- * animation
- */
- @Presubmit
- @Test
- fun pipWindowRemainInsideVisibleBounds() {
- testSpec.assertWmVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
- }
-
- /**
- * Checks that the pip app layer remains inside the display bounds throughout the whole
- * animation
- */
- @Presubmit
- @Test
- open fun pipLayerRemainInsideVisibleBounds() {
- testSpec.assertLayersVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
- }
-
- /**
- * Checks that the visible region of [pipApp] always reduces during the animation
- */
- @Presubmit
- @Test
- open fun pipLayerReduces() {
- val layerName = pipApp.component.toLayerName()
- testSpec.assertLayers {
- val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
- pipLayerList.zipWithNext { previous, current ->
- current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
- }
- }
- }
-
- /**
- * Checks that [pipApp] window becomes pinned
- */
- @Presubmit
- @Test
- fun pipWindowBecomesPinned() {
- testSpec.assertWm {
- invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp.component) }
- .then()
- .invoke("pipWindowIsPinned") { it.isPinned(pipApp.component) }
- }
- }
-
- /**
- * Checks [LAUNCHER_COMPONENT] layer remains visible throughout the animation
- */
- @Presubmit
- @Test
- fun launcherLayerBecomesVisible() {
- testSpec.assertLayers {
- isInvisible(LAUNCHER_COMPONENT)
- .then()
- .isVisible(LAUNCHER_COMPONENT)
- }
- }
-
- /**
- * Checks that the focus changes between the [pipApp] window and the launcher when
- * closing the pip window
- */
- @Presubmit
- @Test
- open fun focusChanges() {
- testSpec.assertEventLog {
- this.focusChanges(pipApp.`package`, "NexusLauncherActivity")
- }
- }
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(
- supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3
- )
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
new file mode 100644
index 000000000000..5480144ba1ce
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.app.Activity
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.helpers.WindowUtils
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.entireScreenCovered
+import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP
+import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
+import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
+import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test entering pip while changing orientation (from app in landscape to pip window in portrait)
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest`
+ *
+ * Actions:
+ * ```
+ * Launch [testApp] on a fixed portrait orientation
+ * Launch [pipApp] on a fixed landscape orientation
+ * Broadcast action [ACTION_ENTER_PIP] to enter pip mode
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterPipToOtherOrientation(flicker: FlickerTest) : PipTransition(flicker) {
+ private val testApp = FixedOrientationAppHelper(instrumentation)
+ private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90)
+ private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
+
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ teardown {
+ testApp.exit(wmHelper)
+ }
+ transitions {
+ // Enter PiP, and assert that the PiP is within bounds now that the device is back
+ // in portrait
+ broadcastActionTrigger.doAction(ACTION_ENTER_PIP)
+ // during rotation the status bar becomes invisible and reappears at the end
+ wmHelper
+ .StateSyncBuilder()
+ .withPipShown()
+ .withNavOrTaskBarVisible()
+ .withStatusBarVisible()
+ .waitForAndVerify()
+ }
+ }
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ // Launch a portrait only app on the fullscreen stack
+ testApp.launchViaIntent(
+ wmHelper,
+ stringExtras = mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())
+ )
+ // Launch the PiP activity fixed as landscape, but don't enter PiP
+ pipApp.launchViaIntent(
+ wmHelper,
+ stringExtras =
+ mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
+ )
+ }
+ }
+
+ /**
+ * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] to
+ * fix a orientation, Tablets instead keep the same orientation and add letterboxes
+ */
+ @Before
+ fun setup() {
+ Assume.assumeFalse(tapl.isTablet)
+ }
+
+ /**
+ * Checks that all parts of the screen are covered at the start and end of the transition
+ *
+ * TODO b/197726599 Prevents all states from being checked
+ */
+ @Presubmit
+ @Test
+ fun entireScreenCoveredAtStartAndEnd() = flicker.entireScreenCovered(allStates = false)
+
+ /** Checks [pipApp] window remains visible and on top throughout the transition */
+ @Presubmit
+ @Test
+ fun pipAppWindowIsAlwaysOnTop() {
+ flicker.assertWm { isAppWindowOnTop(pipApp) }
+ }
+
+ /** Checks that [testApp] window is not visible at the start */
+ @Presubmit
+ @Test
+ fun testAppWindowInvisibleOnStart() {
+ flicker.assertWmStart { isAppWindowInvisible(testApp) }
+ }
+
+ /** Checks that [testApp] window is visible at the end */
+ @Presubmit
+ @Test
+ fun testAppWindowVisibleOnEnd() {
+ flicker.assertWmEnd { isAppWindowVisible(testApp) }
+ }
+
+ /** Checks that [testApp] layer is not visible at the start */
+ @Presubmit
+ @Test
+ fun testAppLayerInvisibleOnStart() {
+ flicker.assertLayersStart { isInvisible(testApp) }
+ }
+
+ /** Checks that [testApp] layer is visible at the end */
+ @Presubmit
+ @Test
+ fun testAppLayerVisibleOnEnd() {
+ flicker.assertLayersEnd { isVisible(testApp) }
+ }
+
+ /**
+ * Checks that the visible region of [pipApp] covers the full display area at the start of the
+ * transition
+ */
+ @Presubmit
+ @Test
+ fun pipAppLayerCoversFullScreenOnStart() {
+ Assume.assumeFalse(tapl.isTablet)
+ flicker.assertLayersStart { visibleRegion(pipApp).coversExactly(startingBounds) }
+ }
+
+ /**
+ * Checks that the visible region of [pipApp] covers the full display area at the start of the
+ * transition
+ */
+ @Postsubmit
+ @Test
+ fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() {
+ Assume.assumeFalse(tapl.isTablet)
+ flicker.assertLayersStart {
+ visibleRegion(pipApp.or(ComponentNameMatcher.LETTERBOX)).coversExactly(startingBounds)
+ }
+ }
+
+ /**
+ * Checks that the visible region of [testApp] plus the visible region of [pipApp] cover the
+ * full display area at the end of the transition
+ */
+ @Presubmit
+ @Test
+ fun testAppPlusPipLayerCoversFullScreenOnEnd() {
+ flicker.assertLayersEnd {
+ val pipRegion = visibleRegion(pipApp).region
+ visibleRegion(testApp).plus(pipRegion).coversExactly(endingBounds)
+ }
+ }
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 267424412)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt
new file mode 100644
index 000000000000..58416660826f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** This test fails because of b/264261596 */
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterPipToOtherOrientationCfArm(flicker: FlickerTest) :
+ EnterPipToOtherOrientation(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
deleted file mode 100644
index 7680f4dfa1d5..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.entireScreenCovered
-import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
-import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
-import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT
-import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION
-import com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test entering pip while changing orientation (from app in landscape to pip window in portrait)
- *
- * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest`
- *
- * Actions:
- * Launch [testApp] on a fixed portrait orientation
- * Launch [pipApp] on a fixed landscape orientation
- * Broadcast action [ACTION_ENTER_PIP] to enter pip mode
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class EnterPipToOtherOrientationTest(
- testSpec: FlickerTestParameter
-) : PipTransition(testSpec) {
- private val testApp = FixedAppHelper(instrumentation)
- private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90)
- private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0)
-
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setupAndTeardown(this)
-
- setup {
- eachRun {
- // Launch a portrait only app on the fullscreen stack
- testApp.launchViaIntent(wmHelper, stringExtras = mapOf(
- EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString()))
- // Launch the PiP activity fixed as landscape
- pipApp.launchViaIntent(wmHelper, stringExtras = mapOf(
- EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()))
- }
- }
- teardown {
- eachRun {
- pipApp.exit(wmHelper)
- testApp.exit(wmHelper)
- }
- }
- transitions {
- // Enter PiP, and assert that the PiP is within bounds now that the device is back
- // in portrait
- broadcastActionTrigger.doAction(ACTION_ENTER_PIP)
- wmHelper.waitPipShown()
- wmHelper.waitForAppTransitionIdle()
- // during rotation the status bar becomes invisible and reappears at the end
- wmHelper.waitForNavBarStatusBarVisible()
- }
- }
-
- /**
- * Checks that the [FlickerComponentName.NAV_BAR] has the correct position at
- * the start and end of the transition
- */
- @FlakyTest
- @Test
- override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- /**
- * Checks that all parts of the screen are covered at the start and end of the transition
- *
- * TODO b/197726599 Prevents all states from being checked
- */
- @Presubmit
- @Test
- override fun entireScreenCovered() = testSpec.entireScreenCovered(allStates = false)
-
- /**
- * Checks [pipApp] window remains visible and on top throughout the transition
- */
- @Presubmit
- @Test
- fun pipAppWindowIsAlwaysOnTop() {
- testSpec.assertWm {
- isAppWindowOnTop(pipApp.component)
- }
- }
-
- /**
- * Checks that [testApp] window is not visible at the start
- */
- @Presubmit
- @Test
- fun testAppWindowInvisibleOnStart() {
- testSpec.assertWmStart {
- isAppWindowInvisible(testApp.component)
- }
- }
-
- /**
- * Checks that [testApp] window is visible at the end
- */
- @Presubmit
- @Test
- fun testAppWindowVisibleOnEnd() {
- testSpec.assertWmEnd {
- isAppWindowVisible(testApp.component)
- }
- }
-
- /**
- * Checks that [testApp] layer is not visible at the start
- */
- @Presubmit
- @Test
- fun testAppLayerInvisibleOnStart() {
- testSpec.assertLayersStart {
- isInvisible(testApp.component)
- }
- }
-
- /**
- * Checks that [testApp] layer is visible at the end
- */
- @Presubmit
- @Test
- fun testAppLayerVisibleOnEnd() {
- testSpec.assertLayersEnd {
- isVisible(testApp.component)
- }
- }
-
- /**
- * Checks that the visible region of [pipApp] covers the full display area at the start of
- * the transition
- */
- @Presubmit
- @Test
- fun pipAppLayerCoversFullScreenOnStart() {
- testSpec.assertLayersStart {
- visibleRegion(pipApp.component).coversExactly(startingBounds)
- }
- }
-
- /**
- * Checks that the visible region of [testApp] plus the visible region of [pipApp]
- * cover the full display area at the end of the transition
- */
- @Presubmit
- @Test
- fun testAppPlusPipLayerCoversFullScreenOnEnd() {
- testSpec.assertLayersEnd {
- val pipRegion = visibleRegion(pipApp.component).region
- visibleRegion(testApp.component)
- .plus(pipRegion)
- .coversExactly(endingBounds)
- }
- }
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt
new file mode 100644
index 000000000000..cdbdb85a9195
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.Test
+import org.junit.runners.Parameterized
+
+abstract class EnterPipTransition(flicker: FlickerTest) : PipTransition(flicker) {
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ pipApp.launchViaIntent(wmHelper)
+ }
+ }
+
+ /** Checks [pipApp] window remains visible throughout the animation */
+ @Presubmit
+ @Test
+ open fun pipAppWindowAlwaysVisible() {
+ flicker.assertWm { this.isAppWindowVisible(pipApp) }
+ }
+
+ /** Checks [pipApp] layer remains visible throughout the animation */
+ @Presubmit
+ @Test
+ open fun pipAppLayerAlwaysVisible() {
+ flicker.assertLayers { this.isVisible(pipApp) }
+ }
+
+ /** Checks the content overlay appears then disappears during the animation */
+ @Presubmit
+ @Test
+ open fun pipOverlayLayerAppearThenDisappear() {
+ val overlay = ComponentNameMatcher.PIP_CONTENT_OVERLAY
+ flicker.assertLayers {
+ this.notContains(overlay).then().contains(overlay).then().notContains(overlay)
+ }
+ }
+
+ /**
+ * Checks that the pip app window remains inside the display bounds throughout the whole
+ * animation
+ */
+ @Presubmit
+ @Test
+ open fun pipWindowRemainInsideVisibleBounds() {
+ flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+ }
+
+ /**
+ * Checks that the pip app layer remains inside the display bounds throughout the whole
+ * animation
+ */
+ @Presubmit
+ @Test
+ open fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ flicker.assertLayersVisibleRegion(pipApp.or(ComponentNameMatcher.PIP_CONTENT_OVERLAY)) {
+ coversAtMost(displayBounds)
+ }
+ }
+
+ /** Checks that the visible region of [pipApp] always reduces during the animation */
+ @Presubmit
+ @Test
+ open fun pipLayerReduces() {
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
+ }
+ }
+ }
+
+ /** Checks that [pipApp] window becomes pinned */
+ @Presubmit
+ @Test
+ fun pipWindowBecomesPinned() {
+ flicker.assertWm {
+ invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp) }
+ .then()
+ .invoke("pipWindowIsPinned") { it.isPinned(pipApp) }
+ }
+ }
+
+ /** Checks [ComponentNameMatcher.LAUNCHER] layer remains visible throughout the animation */
+ @Presubmit
+ @Test
+ fun launcherLayerBecomesVisible() {
+ flicker.assertLayers {
+ isInvisible(ComponentNameMatcher.LAUNCHER)
+ .then()
+ .isVisible(ComponentNameMatcher.LAUNCHER)
+ }
+ }
+
+ /**
+ * Checks that the focus changes between the [pipApp] window and the launcher when closing the
+ * pip window
+ */
+ @Presubmit
+ @Test
+ open fun focusChanges() {
+ flicker.assertEventLog { this.focusChanges(pipApp.`package`, "NexusLauncherActivity") }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
new file mode 100644
index 000000000000..95725b64a48a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test entering pip from an app by interacting with the app UI
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterPipTest`
+ *
+ * Actions:
+ * ```
+ * Launch an app in full screen
+ * Press an "enter pip" button to put [pipApp] in pip mode
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited from [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterPipViaAppUiButtonTest(flicker: FlickerTest) : EnterPipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { pipApp.clickEnterPipButton(wmHelper) }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt
new file mode 100644
index 000000000000..4390f0bb70b2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class EnterPipViaAppUiButtonTestCfArm(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
index 990872f58dc1..5ac9829b6c8f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
@@ -17,15 +17,17 @@
package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
import org.junit.Test
+import org.junit.runners.Parameterized
-/**
- * Base class for pip expand tests
- */
-abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
- protected val testApp = FixedAppHelper(instrumentation)
+/** Base class for pip expand tests */
+abstract class ExitPipToAppTransition(flicker: FlickerTest) : PipTransition(flicker) {
+ protected val testApp = SimpleAppHelper(instrumentation)
/**
* Checks that the pip app window remains inside the display bounds throughout the whole
@@ -34,7 +36,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans
@Presubmit
@Test
open fun pipAppWindowRemainInsideVisibleBounds() {
- testSpec.assertWmVisibleRegion(pipApp.component) {
+ flicker.assertWmVisibleRegion(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) {
coversAtMost(displayBounds)
}
}
@@ -46,7 +48,7 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans
@Presubmit
@Test
open fun pipAppLayerRemainInsideVisibleBounds() {
- testSpec.assertLayersVisibleRegion(pipApp.component) {
+ flicker.assertLayersVisibleRegion(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) {
coversAtMost(displayBounds)
}
}
@@ -58,15 +60,15 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans
@Presubmit
@Test
open fun showBothAppWindowsThenHidePip() {
- testSpec.assertWm {
+ flicker.assertWm {
// when the activity is STOPPING, sometimes it becomes invisible in an entry before
// the window, sometimes in the same entry. This occurs because we log 1x per frame
// thus we ignore activity here
- isAppWindowVisible(testApp.component)
- .isAppWindowOnTop(pipApp.component)
- .then()
- .isAppWindowInvisible(testApp.component)
- .isAppWindowVisible(pipApp.component)
+ isAppWindowVisible(testApp)
+ .isAppWindowOnTop(pipApp)
+ .then()
+ .isAppWindowInvisible(testApp)
+ .isAppWindowVisible(pipApp)
}
}
@@ -77,54 +79,66 @@ abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTrans
@Presubmit
@Test
open fun showBothAppLayersThenHidePip() {
- testSpec.assertLayers {
- isVisible(testApp.component)
- .isVisible(pipApp.component)
- .then()
- .isInvisible(testApp.component)
- .isVisible(pipApp.component)
+ flicker.assertLayers {
+ isVisible(testApp)
+ .isVisible(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT))
+ .then()
+ .isInvisible(testApp)
+ .isVisible(pipApp)
}
}
/**
- * Checks that the visible region of [testApp] plus the visible region of [pipApp]
- * cover the full display area at the start of the transition
+ * Checks that the visible region of [testApp] plus the visible region of [pipApp] cover the
+ * full display area at the start of the transition
*/
@Presubmit
@Test
open fun testPlusPipAppsCoverFullScreenAtStart() {
- testSpec.assertLayersStart {
- val pipRegion = visibleRegion(pipApp.component).region
- visibleRegion(testApp.component)
- .plus(pipRegion)
- .coversExactly(displayBounds)
+ flicker.assertLayersStart {
+ val pipRegion = visibleRegion(pipApp).region
+ visibleRegion(testApp).plus(pipRegion).coversExactly(displayBounds)
}
}
/**
- * Checks that the visible region oft [pipApp] covers the full display area at the end of
- * the transition
+ * Checks that the visible region oft [pipApp] covers the full display area at the end of the
+ * transition
*/
@Presubmit
@Test
open fun pipAppCoversFullScreenAtEnd() {
- testSpec.assertLayersEnd {
- visibleRegion(pipApp.component).coversExactly(displayBounds)
- }
+ flicker.assertLayersEnd { visibleRegion(pipApp).coversExactly(displayBounds) }
}
- /**
- * Checks that the visible region of [pipApp] always expands during the animation
- */
+ /** Checks that the visible region of [pipApp] always expands during the animation */
@Presubmit
@Test
open fun pipLayerExpands() {
- val layerName = pipApp.component.toLayerName()
- testSpec.assertLayers {
- val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
pipLayerList.zipWithNext { previous, current ->
current.visibleRegion.coversAtLeast(previous.visibleRegion.region)
}
}
}
+
+ /** {@inheritDoc} */
+ @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
new file mode 100644
index 000000000000..0b3d16a8087d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window back to full screen via the expand button
+ *
+ * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest`
+ *
+ * Actions:
+ * ```
+ * Launch an app in pip mode [pipApp],
+ * Launch another full screen mode [testApp]
+ * Expand [pipApp] app to full screen by clicking on the pip window and
+ * then on the expand button
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class ExitPipToAppViaExpandButtonTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ setup {
+ // launch an app behind the pip one
+ testApp.launchViaIntent(wmHelper)
+ }
+ transitions {
+ // This will bring PipApp to fullscreen
+ pipApp.expandPipWindowToApp(wmHelper)
+ // Wait until the other app is no longer visible
+ wmHelper.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt
new file mode 100644
index 000000000000..eccb85d98798
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExitPipToAppViaExpandButtonTestCfArm(flicker: FlickerTest) :
+ ExitPipToAppViaExpandButtonTest(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
new file mode 100644
index 000000000000..bb2d40becdc9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window back to full screen via an intent
+ *
+ * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest`
+ *
+ * Actions:
+ * ```
+ * Launch an app in pip mode [pipApp],
+ * Launch another full screen mode [testApp]
+ * Expand [pipApp] app to full screen via an intent
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited from [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class ExitPipToAppViaIntentTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ setup {
+ // launch an app behind the pip one
+ testApp.launchViaIntent(wmHelper)
+ }
+ transitions {
+ // This will bring PipApp to fullscreen
+ pipApp.exitPipToFullScreenViaIntent(wmHelper)
+ // Wait until the other app is no longer visible
+ wmHelper.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt
new file mode 100644
index 000000000000..6ab6a1f0bb73
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExitPipToAppViaIntentTestCfArm(flicker: FlickerTest) : ExitPipToAppViaIntentTest(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
deleted file mode 100644
index 0b4bc761838d..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.LAUNCHER_COMPONENT
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import com.android.server.wm.flicker.helpers.setRotation
-import org.junit.Test
-
-/**
- * Base class for exiting pip (closing pip window) without returning to the app
- */
-abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = true) {
- setup {
- eachRun {
- this.setRotation(testSpec.startRotation)
- }
- }
- teardown {
- eachRun {
- this.setRotation(Surface.ROTATION_0)
- }
- }
- }
-
- /**
- * Checks that [pipApp] window is pinned and visible at the start and then becomes
- * unpinned and invisible at the same moment, and remains unpinned and invisible
- * until the end of the transition
- */
- @Presubmit
- @Test
- open fun pipWindowBecomesInvisible() {
- if (isShellTransitionsEnabled) {
- // When Shell transition is enabled, we change the windowing mode at start, but
- // update the visibility after the transition is finished, so we can't check isNotPinned
- // and isAppWindowInvisible in the same assertion block.
- testSpec.assertWm {
- this.invoke("hasPipWindow") {
- it.isPinned(pipApp.component)
- .isAppWindowVisible(pipApp.component)
- .isAppWindowOnTop(pipApp.component)
- }.then().invoke("!hasPipWindow") {
- it.isNotPinned(pipApp.component)
- .isAppWindowNotOnTop(pipApp.component)
- }
- }
- testSpec.assertWmEnd { isAppWindowInvisible(pipApp.component) }
- } else {
- testSpec.assertWm {
- this.invoke("hasPipWindow") {
- it.isPinned(pipApp.component).isAppWindowVisible(pipApp.component)
- }.then().invoke("!hasPipWindow") {
- it.isNotPinned(pipApp.component).isAppWindowInvisible(pipApp.component)
- }
- }
- }
- }
-
- /**
- * Checks that [pipApp] and [LAUNCHER_COMPONENT] layers are visible at the start
- * of the transition. Then [pipApp] layer becomes invisible, and remains invisible
- * until the end of the transition
- */
- @Presubmit
- @Test
- open fun pipLayerBecomesInvisible() {
- testSpec.assertLayers {
- this.isVisible(pipApp.component)
- .isVisible(LAUNCHER_COMPONENT)
- .then()
- .isInvisible(pipApp.component)
- .isVisible(LAUNCHER_COMPONENT)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
deleted file mode 100644
index 0768e82e491c..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test expanding a pip window back to full screen via the expand button
- *
- * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest`
- *
- * Actions:
- * Launch an app in pip mode [pipApp],
- * Launch another full screen mode [testApp]
- * Expand [pipApp] app to full screen by clicking on the pip window and
- * then on the expand button
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-@FlakyTest(bugId = 219750830)
-class ExitPipViaExpandButtonClickTest(
- testSpec: FlickerTestParameter
-) : ExitPipToAppTransition(testSpec) {
-
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = true) {
- setup {
- eachRun {
- // launch an app behind the pip one
- testApp.launchViaIntent(wmHelper)
- }
- }
- transitions {
- // This will bring PipApp to fullscreen
- pipApp.expandPipWindowToApp(wmHelper)
- // Wait until the other app is no longer visible
- wmHelper.waitForWindowSurfaceDisappeared(testApp.component)
- }
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 197726610)
- @Test
- override fun pipLayerExpands() = super.pipLayerExpands()
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
deleted file mode 100644
index c6a705dacb8d..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import org.junit.Assume
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test expanding a pip window back to full screen via an intent
- *
- * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest`
- *
- * Actions:
- * Launch an app in pip mode [pipApp],
- * Launch another full screen mode [testApp]
- * Expand [pipApp] app to full screen via an intent
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited from [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransition(testSpec) {
-
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = true) {
- setup {
- eachRun {
- // launch an app behind the pip one
- testApp.launchViaIntent(wmHelper)
- }
- }
- transitions {
- // This will bring PipApp to fullscreen
- pipApp.exitPipToFullScreenViaIntent(wmHelper)
- // Wait until the other app is no longer visible
- wmHelper.waitForWindowSurfaceDisappeared(testApp.component)
- }
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- super.statusBarLayerRotatesScales()
- }
-
- @Presubmit
- @Test
- fun statusBarLayerRotatesScales_ShellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- super.statusBarLayerRotatesScales()
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 197726610)
- @Test
- override fun pipLayerExpands() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- super.pipLayerExpands()
- }
-
- @Presubmit
- @Test
- fun pipLayerExpands_ShellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- super.pipLayerExpands()
- }
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
deleted file mode 100644
index 128703ad332c..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test closing a pip window by swiping it to the bottom-center of the screen
- *
- * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest`
- *
- * Actions:
- * Launch an app in pip mode [pipApp],
- * Swipe the pip window to the bottom-center of the screen and wait it disappear
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- super.transition(this)
- transitions {
- val pipRegion = wmHelper.getWindowRegion(pipApp.component).bounds
- val pipCenterX = pipRegion.centerX()
- val pipCenterY = pipRegion.centerY()
- val displayCenterX = device.displayWidth / 2
- device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 10)
- wmHelper.waitPipGone()
- wmHelper.waitForWindowSurfaceDisappeared(pipApp.component)
- wmHelper.waitForAppTransitionIdle()
- }
- }
-
- @FlakyTest
- @Test
- override fun pipWindowBecomesInvisible() = super.pipWindowBecomesInvisible()
-
- @FlakyTest
- @Test
- override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible()
-
- /**
- * Checks that the focus doesn't change between windows during the transition
- */
- @Presubmit
- @Test
- fun focusDoesNotChange() {
- testSpec.assertEventLog {
- this.focusDoesNotChange()
- }
- }
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
index 28b7fc9bd29e..fd16b6ea6ada 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -16,16 +16,14 @@
package com.android.wm.shell.flicker.pip
-import androidx.test.filters.FlakyTest
import android.platform.test.annotations.Presubmit
-import android.view.Surface
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.LAUNCHER_COMPONENT
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -33,34 +31,34 @@ import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
/**
- * Test expanding a pip window by double clicking it
+ * Test expanding a pip window by double-clicking it
*
* To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest`
*
* Actions:
+ * ```
* Launch an app in pip mode [pipApp],
* Expand [pipApp] app to its maximum pip size by double clicking on it
+ * ```
*
* Notes:
+ * ```
* 1. Some default assertions (e.g., nav bar, status bar and screen covered)
* are inherited [PipTransition]
* 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
* including configuring navigation mode, initial orientation and ensuring no
* apps are running before setup
+ * ```
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = true) {
- transitions {
- pipApp.doubleClickPipWindow(wmHelper)
- }
- }
+open class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { pipApp.doubleClickPipWindow(wmHelper) }
+ }
/**
* Checks that the pip app window remains inside the display bounds throughout the whole
@@ -69,9 +67,7 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition
@Presubmit
@Test
fun pipWindowRemainInsideVisibleBounds() {
- testSpec.assertWmVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
+ flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
}
/**
@@ -81,42 +77,29 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition
@Presubmit
@Test
fun pipLayerRemainInsideVisibleBounds() {
- testSpec.assertLayersVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
+ flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
}
- /**
- * Checks [pipApp] window remains visible throughout the animation
- */
+ /** Checks [pipApp] window remains visible throughout the animation */
@Presubmit
@Test
fun pipWindowIsAlwaysVisible() {
- testSpec.assertWm {
- isAppWindowVisible(pipApp.component)
- }
+ flicker.assertWm { isAppWindowVisible(pipApp) }
}
- /**
- * Checks [pipApp] layer remains visible throughout the animation
- */
+ /** Checks [pipApp] layer remains visible throughout the animation */
@Presubmit
@Test
fun pipLayerIsAlwaysVisible() {
- testSpec.assertLayers {
- isVisible(pipApp.component)
- }
+ flicker.assertLayers { isVisible(pipApp) }
}
- /**
- * Checks that the visible region of [pipApp] always expands during the animation
- */
- @FlakyTest(bugId = 228012337)
+ /** Checks that the visible region of [pipApp] always expands during the animation */
+ @Presubmit
@Test
fun pipLayerExpands() {
- val layerName = pipApp.component.toLayerName()
- testSpec.assertLayers {
- val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
pipLayerList.zipWithNext { previous, current ->
current.visibleRegion.coversAtLeast(previous.visibleRegion.region)
}
@@ -126,65 +109,48 @@ class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition
@Presubmit
@Test
fun pipSameAspectRatio() {
- val layerName = pipApp.component.toLayerName()
- testSpec.assertLayers {
- val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
pipLayerList.zipWithNext { previous, current ->
current.visibleRegion.isSameAspectRatio(previous.visibleRegion)
}
}
}
- /**
- * Checks [pipApp] window remains pinned throughout the animation
- */
+ /** Checks [pipApp] window remains pinned throughout the animation */
@Presubmit
@Test
fun windowIsAlwaysPinned() {
- testSpec.assertWm {
- this.invoke("hasPipWindow") { it.isPinned(pipApp.component) }
- }
+ flicker.assertWm { this.invoke("hasPipWindow") { it.isPinned(pipApp) } }
}
- /**
- * Checks [pipApp] layer remains visible throughout the animation
- */
+ /** Checks [ComponentNameMatcher.LAUNCHER] layer remains visible throughout the animation */
@Presubmit
@Test
fun launcherIsAlwaysVisible() {
- testSpec.assertLayers {
- isVisible(LAUNCHER_COMPONENT)
- }
+ flicker.assertLayers { isVisible(ComponentNameMatcher.LAUNCHER) }
}
- /**
- * Checks that the focus doesn't change between windows during the transition
- */
- @FlakyTest(bugId = 216306753)
+ /** Checks that the focus doesn't change between windows during the transition */
+ @Presubmit
@Test
fun focusDoesNotChange() {
- testSpec.assertEventLog {
- this.focusDoesNotChange()
- }
+ flicker.assertEventLog { this.focusDoesNotChange() }
}
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
companion object {
/**
* Creates the test configurations.
*
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
*/
@Parameterized.Parameters(name = "{0}")
@JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3)
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
}
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt
new file mode 100644
index 000000000000..c09623490041
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExpandPipOnDoubleClickTestTestCfArm(flicker: FlickerTest) :
+ ExpandPipOnDoubleClickTest(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
new file mode 100644
index 000000000000..253aa4cae5c7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** Test expanding a pip window via pinch out gesture. */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class ExpandPipOnPinchOpenTest(flicker: FlickerTest) : PipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.25f, 30) }
+ }
+
+ /** Checks that the visible region area of [pipApp] always increases during the animation. */
+ @Presubmit
+ @Test
+ fun pipLayerAreaIncreases() {
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ previous.visibleRegion.notBiggerThan(current.visibleRegion.region)
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt
new file mode 100644
index 000000000000..e064bf2ee921
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExpandPipOnPinchOpenTestCfArm(flicker: FlickerTest) : ExpandPipOnPinchOpenTest(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
new file mode 100644
index 000000000000..094060f86691
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.Direction
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test Pip movement with Launcher shelf height change (increase).
+ *
+ * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest`
+ *
+ * Actions:
+ * ```
+ * Launch [pipApp] in pip mode
+ * Press home
+ * Launch [testApp]
+ * Check if pip window moves down (visually)
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class MovePipDownOnShelfHeightChange(flicker: FlickerTest) : MovePipShelfHeightTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ teardown { testApp.exit(wmHelper) }
+ transitions { testApp.launchViaIntent(wmHelper) }
+ }
+
+ /** Checks that the visible region of [pipApp] window always moves down during the animation. */
+ @Presubmit @Test fun pipWindowMovesDown() = pipWindowMoves(Direction.DOWN)
+
+ /** Checks that the visible region of [pipApp] layer always moves down during the animation. */
+ @Presubmit @Test fun pipLayerMovesDown() = pipLayerMoves(Direction.DOWN)
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt
deleted file mode 100644
index 8729bb6776f0..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.traces.region.RegionSubject
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip movement with Launcher shelf height change (decrease).
- *
- * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest`
- *
- * Actions:
- * Launch [pipApp] in pip mode
- * Launch [testApp]
- * Press home
- * Check if pip window moves down (visually)
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-open class MovePipDownShelfHeightChangeTest(
- testSpec: FlickerTestParameter
-) : MovePipShelfHeightTransition(testSpec) {
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = false) {
- teardown {
- eachRun {
- testApp.launchViaIntent(wmHelper)
- }
- test {
- testApp.exit(wmHelper)
- }
- }
- transitions {
- taplInstrumentation.pressHome()
- }
- }
-
- override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) {
- current.isHigherOrEqual(previous.region)
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
new file mode 100644
index 000000000000..ff51c27bf116
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.helpers.WindowUtils
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** Test Pip launch. To run this test: `atest WMShellFlickerTests:PipKeyboardTest` */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class MovePipOnImeVisibilityChangeTest(flicker: FlickerTest) : PipTransition(flicker) {
+ private val imeApp = ImeAppHelper(instrumentation)
+
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ setup {
+ imeApp.launchViaIntent(wmHelper)
+ setRotation(flicker.scenario.startRotation)
+ }
+ teardown { imeApp.exit(wmHelper) }
+ transitions {
+ // open the soft keyboard
+ imeApp.openIME(wmHelper)
+ createTag(TAG_IME_VISIBLE)
+
+ // then close it again
+ imeApp.closeIME(wmHelper)
+ }
+ }
+
+ /** Ensure the pip window remains visible throughout any keyboard interactions */
+ @Presubmit
+ @Test
+ open fun pipInVisibleBounds() {
+ flicker.assertWmVisibleRegion(pipApp) {
+ val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
+ coversAtMost(displayBounds)
+ }
+ }
+
+ /** Ensure that the pip window does not obscure the keyboard */
+ @Presubmit
+ @Test
+ open fun pipIsAboveAppWindow() {
+ flicker.assertWmTag(TAG_IME_VISIBLE) { isAboveWindow(ComponentNameMatcher.IME, pipApp) }
+ }
+
+ companion object {
+ private const val TAG_IME_VISIBLE = "imeIsVisible"
+
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTestCfArm.kt
index fe51228230cb..d3d77d20662e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTestCfArm.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -16,33 +16,29 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.Presubmit
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import org.junit.Assume
-import org.junit.Before
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
import org.junit.FixMethodOrder
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
-@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
-class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardTest(testSpec) {
+class MovePipOnImeVisibilityChangeTestCfArm(flicker: FlickerTest) :
+ MovePipOnImeVisibilityChangeTest(flicker) {
+ companion object {
+ private const val TAG_IME_VISIBLE = "imeIsVisible"
- @Before
- override fun before() {
- Assume.assumeTrue(isShellTransitionsEnabled)
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
}
-
- @Presubmit
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
index 0499e7de9a0a..109354ab5c79 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
@@ -17,46 +17,31 @@
package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.traces.region.RegionSubject
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
+import android.tools.common.Rotation
+import android.tools.common.flicker.subject.region.RegionSubject
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
+import com.android.wm.shell.flicker.Direction
import org.junit.Test
+import org.junit.runners.Parameterized
-/**
- * Base class for pip tests with Launcher shelf height change
- */
-abstract class MovePipShelfHeightTransition(
- testSpec: FlickerTestParameter
-) : PipTransition(testSpec) {
- protected val taplInstrumentation = LauncherInstrumentation()
- protected val testApp = FixedAppHelper(instrumentation)
-
- /**
- * Checks if the window movement direction is valid
- */
- protected abstract fun assertRegionMovement(previous: RegionSubject, current: RegionSubject)
+/** Base class for pip tests with Launcher shelf height change */
+abstract class MovePipShelfHeightTransition(flicker: FlickerTest) : PipTransition(flicker) {
+ protected val testApp = FixedOrientationAppHelper(instrumentation)
- /**
- * Checks [pipApp] window remains visible throughout the animation
- */
+ /** Checks [pipApp] window remains visible throughout the animation */
@Presubmit
@Test
open fun pipWindowIsAlwaysVisible() {
- testSpec.assertWm {
- isAppWindowVisible(pipApp.component)
- }
+ flicker.assertWm { isAppWindowVisible(pipApp) }
}
- /**
- * Checks [pipApp] layer remains visible throughout the animation
- */
+ /** Checks [pipApp] layer remains visible throughout the animation */
@Presubmit
@Test
open fun pipLayerIsAlwaysVisible() {
- testSpec.assertLayers {
- isVisible(pipApp.component)
- }
+ flicker.assertLayers { isVisible(pipApp) }
}
/**
@@ -66,9 +51,7 @@ abstract class MovePipShelfHeightTransition(
@Presubmit
@Test
open fun pipWindowRemainInsideVisibleBounds() {
- testSpec.assertWmVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
+ flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
}
/**
@@ -78,39 +61,65 @@ abstract class MovePipShelfHeightTransition(
@Presubmit
@Test
open fun pipLayerRemainInsideVisibleBounds() {
- testSpec.assertLayersVisibleRegion(pipApp.component) {
- coversAtMost(displayBounds)
- }
+ flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
}
/**
- * Checks that the visible region of [pipApp] always moves in the correct direction
+ * Checks that the visible region of [pipApp] window always moves in the specified direction
* during the animation.
*/
- @Presubmit
- @Test
- open fun pipWindowMoves() {
- val windowName = pipApp.component.toWindowName()
- testSpec.assertWm {
- val pipWindowList = this.windowStates { it.name.contains(windowName) && it.isVisible }
- pipWindowList.zipWithNext { previous, current ->
- assertRegionMovement(previous.frame, current.frame)
+ protected fun pipWindowMoves(direction: Direction) {
+ flicker.assertWm {
+ val pipWindowFrameList =
+ this.windowStates { pipApp.windowMatchesAnyOf(it) && it.isVisible }.map { it.frame }
+ when (direction) {
+ Direction.UP -> assertRegionMovementUp(pipWindowFrameList)
+ Direction.DOWN -> assertRegionMovementDown(pipWindowFrameList)
+ else -> error("Unhandled direction")
}
}
}
/**
- * Checks that the visible region of [pipApp] always moves up during the animation
+ * Checks that the visible region of [pipApp] layer always moves in the specified direction
+ * during the animation.
*/
- @Presubmit
- @Test
- open fun pipLayerMoves() {
- val layerName = pipApp.component.toLayerName()
- testSpec.assertLayers {
- val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible }
- pipLayerList.zipWithNext { previous, current ->
- assertRegionMovement(previous.visibleRegion, current.visibleRegion)
+ protected fun pipLayerMoves(direction: Direction) {
+ flicker.assertLayers {
+ val pipLayerRegionList =
+ this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ .map { it.visibleRegion }
+ when (direction) {
+ Direction.UP -> assertRegionMovementUp(pipLayerRegionList)
+ Direction.DOWN -> assertRegionMovementDown(pipLayerRegionList)
+ else -> error("Unhandled direction")
}
}
}
-} \ No newline at end of file
+
+ private fun assertRegionMovementDown(regions: List<RegionSubject>) {
+ regions.zipWithNext { previous, current -> current.isLowerOrEqual(previous) }
+ regions.last().isLower(regions.first())
+ }
+
+ private fun assertRegionMovementUp(regions: List<RegionSubject>) {
+ regions.zipWithNext { previous, current -> current.isHigherOrEqual(previous.region) }
+ regions.last().isHigher(regions.first())
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
new file mode 100644
index 000000000000..27b061b67a85
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.Direction
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test Pip movement with Launcher shelf height change (decrease).
+ *
+ * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest`
+ *
+ * Actions:
+ * ```
+ * Launch [pipApp] in pip mode
+ * Launch [testApp]
+ * Press home
+ * Check if pip window moves up (visually)
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class MovePipUpOnShelfHeightChangeTest(flicker: FlickerTest) :
+ MovePipShelfHeightTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit =
+ {
+ setup { testApp.launchViaIntent(wmHelper) }
+ transitions { tapl.pressHome() }
+ teardown { testApp.exit(wmHelper) }
+ }
+
+ /** Checks that the visible region of [pipApp] window always moves up during the animation. */
+ @Presubmit @Test fun pipWindowMovesUp() = pipWindowMoves(Direction.UP)
+
+ /** Checks that the visible region of [pipApp] layer always moves up during the animation. */
+ @Presubmit @Test fun pipLayerMovesUp() = pipLayerMoves(Direction.UP)
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt
deleted file mode 100644
index 388b5e0b5e47..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import androidx.test.filters.FlakyTest
-import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import com.android.server.wm.flicker.traces.region.RegionSubject
-import org.junit.Assume
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip movement with Launcher shelf height change (increase).
- *
- * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest`
- *
- * Actions:
- * Launch [pipApp] in pip mode
- * Press home
- * Launch [testApp]
- * Check if pip window moves up (visually)
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-class MovePipUpShelfHeightChangeTest(
- testSpec: FlickerTestParameter
-) : MovePipShelfHeightTransition(testSpec) {
- @Before
- fun before() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- }
-
- /**
- * Defines the transition used to run the test
- */
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = false) {
- teardown {
- eachRun {
- taplInstrumentation.pressHome()
- }
- test {
- testApp.exit(wmHelper)
- }
- }
- transitions {
- testApp.launchViaIntent(wmHelper)
- }
- }
-
- override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) {
- current.isLowerOrEqual(previous.region)
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- supportedRotations = listOf(Surface.ROTATION_0), repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt
new file mode 100644
index 000000000000..9f81ba8eee87
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.flicker.pip
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.RequiresDevice
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** Test the dragging of a PIP window. */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class PipDragTest(flicker: FlickerTest) : PipTransition(flicker) {
+ private var isDraggedLeft: Boolean = true
+
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { pipApp.dragPipWindowAwayFromEdgeWithoutRelease(wmHelper, 50) }
+ }
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ val stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true")
+ setup {
+ tapl.setEnableRotation(true)
+ pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras)
+
+ // determine the direction of dragging to test for
+ isDraggedLeft = pipApp.isCloserToRightEdge(wmHelper)
+ }
+ }
+
+ override val defaultTeardown: FlickerBuilder.() -> Unit = {
+ teardown {
+ // release the primary pointer after dragging without release
+ pipApp.releasePipAfterDragging()
+
+ pipApp.exit(wmHelper)
+ tapl.setEnableRotation(false)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun pipLayerMovesAwayFromEdge() {
+ flicker.assertLayers {
+ val pipLayerList = layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ if (isDraggedLeft) {
+ previous.visibleRegion.isToTheRight(current.visibleRegion.region)
+ } else {
+ current.visibleRegion.isToTheRight(previous.visibleRegion.region)
+ }
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt
new file mode 100644
index 000000000000..9fe9f52fd4af
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.flicker.pip
+
+import android.graphics.Rect
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** Test the snapping of a PIP window via dragging, releasing, and checking its final location. */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class PipDragThenSnapTest(flicker: FlickerTest) : PipTransition(flicker) {
+ // represents the direction in which the pip window should be snapping
+ private var willSnapRight: Boolean = true
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ val stringExtras: Map<String, String> =
+ mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true")
+
+ // cache the starting bounds here
+ val startBounds = Rect()
+
+ setup {
+ // Launch the PIP activity and wait for it to enter PiP mode
+ setRotation(Rotation.ROTATION_0)
+ RemoveAllTasksButHomeRule.removeAllTasksButHome()
+ pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras)
+
+ // get the initial region bounds and cache them
+ val initRegion = pipApp.getWindowRect(wmHelper)
+ startBounds.set(
+ initRegion.left,
+ initRegion.top,
+ initRegion.right,
+ initRegion.bottom
+ )
+
+ // drag the pip window away from the edge
+ pipApp.dragPipWindowAwayFromEdge(wmHelper, 50)
+
+ // determine the direction in which the snapping should occur
+ willSnapRight = pipApp.isCloserToRightEdge(wmHelper)
+ }
+ transitions {
+ // continue the transition until the PIP snaps
+ pipApp.waitForPipToSnapTo(wmHelper, startBounds)
+ }
+ }
+
+ /**
+ * Checks that the visible region area of [pipApp] moves to closest edge during the animation.
+ */
+ @Postsubmit
+ @Test
+ fun pipLayerMovesToClosestEdge() {
+ flicker.assertLayers {
+ val pipLayerList = layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ if (willSnapRight) {
+ current.visibleRegion.isToTheRight(previous.visibleRegion.region)
+ } else {
+ previous.visibleRegion.isToTheRight(current.visibleRegion.region)
+ }
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
deleted file mode 100644
index 1e30f6b83874..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.traces.common.FlickerComponentName
-import com.android.wm.shell.flicker.helpers.ImeAppHelper
-import org.junit.Assume.assumeFalse
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip launch.
- * To run this test: `atest WMShellFlickerTests:PipKeyboardTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
-@FlakyTest(bugId = 218604389)
-open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
- private val imeApp = ImeAppHelper(instrumentation)
-
- @Before
- open fun before() {
- assumeFalse(isShellTransitionsEnabled)
- }
-
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = false) {
- setup {
- test {
- imeApp.launchViaIntent(wmHelper)
- setRotation(testSpec.startRotation)
- }
- }
- teardown {
- test {
- imeApp.exit(wmHelper)
- setRotation(Surface.ROTATION_0)
- }
- }
- transitions {
- // open the soft keyboard
- imeApp.openIME(wmHelper)
- createTag(TAG_IME_VISIBLE)
-
- // then close it again
- imeApp.closeIME(wmHelper)
- }
- }
-
- /** {@inheritDoc} */
- @FlakyTest(bugId = 206753786)
- @Test
- override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
-
- /**
- * Ensure the pip window remains visible throughout any keyboard interactions
- */
- @Presubmit
- @Test
- open fun pipInVisibleBounds() {
- testSpec.assertWmVisibleRegion(pipApp.component) {
- val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
- coversAtMost(displayBounds)
- }
- }
-
- /**
- * Ensure that the pip window does not obscure the keyboard
- */
- @Presubmit
- @Test
- open fun pipIsAboveAppWindow() {
- testSpec.assertWmTag(TAG_IME_VISIBLE) {
- isAboveWindow(FlickerComponentName.IME, pipApp.component)
- }
- }
-
- companion object {
- private const val TAG_IME_VISIBLE = "imeIsVisible"
-
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
new file mode 100644
index 000000000000..60bf5ffdc7af
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.flicker.pip
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** Test minimizing a pip window via pinch in gesture. */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@FlakyTest(bugId = 270677470)
+class PipPinchInTest(flicker: FlickerTest) : PipTransition(flicker) {
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { pipApp.pinchInPipWindow(wmHelper, 0.4f, 30) }
+ }
+
+ /** Checks that the visible region area of [pipApp] always decreases during the animation. */
+ @Postsubmit
+ @Test
+ fun pipLayerAreaDecreases() {
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
deleted file mode 100644
index 9fad4997e63a..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.entireScreenCovered
-import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.wm.shell.flicker.helpers.FixedAppHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip Stack in bounds after rotations.
- *
- * To run this test: `atest WMShellFlickerTests:PipRotationTest`
- *
- * Actions:
- * Launch a [pipApp] in pip mode
- * Launch another app [fixedApp] (appears below pip)
- * Rotate the screen from [testSpec.startRotation] to [testSpec.endRotation]
- * (usually, 0->90 and 90->0)
- *
- * Notes:
- * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
- * are inherited from [PipTransition]
- * 2. Part of the test setup occurs automatically via
- * [com.android.server.wm.flicker.TransitionRunnerWithRules],
- * including configuring navigation mode, initial orientation and ensuring no
- * apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
-open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
- private val fixedApp = FixedAppHelper(instrumentation)
- private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.startRotation)
- private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.endRotation)
-
- override val transition: FlickerBuilder.() -> Unit
- get() = buildTransition(eachRun = false) {
- setup {
- test {
- fixedApp.launchViaIntent(wmHelper)
- }
- eachRun {
- setRotation(testSpec.startRotation)
- }
- }
- transitions {
- setRotation(testSpec.endRotation)
- }
- }
-
- /**
- * Checks that all parts of the screen are covered at the start and end of the transition
- */
- @Presubmit
- @Test
- override fun entireScreenCovered() = testSpec.entireScreenCovered()
-
- /**
- * Checks the position of the navigation bar at the start and end of the transition
- */
- @FlakyTest
- @Test
- override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
-
- /**
- * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition
- */
- @Presubmit
- @Test
- fun appLayerRotates_StartingBounds() {
- testSpec.assertLayersStart {
- visibleRegion(fixedApp.component).coversExactly(screenBoundsStart)
- }
- }
-
- /**
- * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition
- */
- @Presubmit
- @Test
- fun appLayerRotates_EndingBounds() {
- testSpec.assertLayersEnd {
- visibleRegion(fixedApp.component).coversExactly(screenBoundsEnd)
- }
- }
-
- /**
- * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition
- */
- private fun pipLayerRotates_StartingBounds_internal() {
- testSpec.assertLayersStart {
- visibleRegion(pipApp.component).coversAtMost(screenBoundsStart)
- }
- }
-
- /**
- * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition
- */
- @Presubmit
- @Test
- fun pipLayerRotates_StartingBounds() {
- pipLayerRotates_StartingBounds_internal()
- }
-
- /**
- * Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition
- */
- @Presubmit
- @Test
- fun pipLayerRotates_EndingBounds() {
- testSpec.assertLayersEnd {
- visibleRegion(pipApp.component).coversAtMost(screenBoundsEnd)
- }
- }
-
- /**
- * Ensure that the [pipApp] window does not obscure the [fixedApp] at the start of the
- * transition
- */
- @Presubmit
- @Test
- fun pipIsAboveFixedAppWindow_Start() {
- testSpec.assertWmStart {
- isAboveWindow(pipApp.component, fixedApp.component)
- }
- }
-
- /**
- * Ensure that the [pipApp] window does not obscure the [fixedApp] at the end of the
- * transition
- */
- @Presubmit
- @Test
- fun pipIsAboveFixedAppWindow_End() {
- testSpec.assertWmEnd {
- isAboveWindow(pipApp.component, fixedApp.component)
- }
- }
-
- companion object {
- /**
- * Creates the test configurations.
- *
- * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
- * repetitions, screen orientation and navigation modes.
- */
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigRotationTests(
- supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90),
- repetitions = 3)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt
deleted file mode 100644
index 7ba085d3cf1a..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTestBase.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import com.android.wm.shell.flicker.FlickerTestBase
-import com.android.wm.shell.flicker.helpers.PipAppHelper
-import org.junit.Before
-
-abstract class PipTestBase(
- rotationName: String,
- rotation: Int
-) : FlickerTestBase(rotationName, rotation) {
- protected val testApp = PipAppHelper(instrumentation)
-
- @Before
- override fun televisionSetUp() {
- /**
- * The super implementation assumes ([org.junit.Assume]) that not running on TV, thus
- * disabling the test on TV. This test, however, *should run on TV*, so we overriding this
- * method and simply leaving it blank.
- */
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
index 8d542c8ec9e6..17a178f78de3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
@@ -19,32 +19,24 @@ package com.android.wm.shell.flicker.pip
import android.app.Instrumentation
import android.content.Intent
import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.wm.flicker.FlickerBuilderProvider
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.entireScreenCovered
-import com.android.server.wm.flicker.helpers.WindowUtils
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
+import android.tools.device.helpers.WindowUtils
+import com.android.server.wm.flicker.helpers.PipAppHelper
import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.navBarLayerIsVisible
-import com.android.server.wm.flicker.navBarLayerRotatesAndScales
-import com.android.server.wm.flicker.navBarWindowIsVisible
-import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
-import com.android.server.wm.flicker.statusBarLayerIsVisible
-import com.android.server.wm.flicker.statusBarLayerRotatesScales
-import com.android.server.wm.flicker.statusBarWindowIsVisible
-import com.android.wm.shell.flicker.helpers.PipAppHelper
-import com.android.wm.shell.flicker.testapp.Components
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.BaseTest
+import com.google.common.truth.Truth
import org.junit.Test
-abstract class PipTransition(protected val testSpec: FlickerTestParameter) {
- protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) {
protected val pipApp = PipAppHelper(instrumentation)
- protected val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
+ protected val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation)
- protected abstract val transition: FlickerBuilder.() -> Unit
+
// Helper class to process test actions by broadcast.
protected class BroadcastActionTrigger(private val instrumentation: Instrumentation) {
private fun createIntentWithAction(broadcastAction: String): Intent {
@@ -52,123 +44,65 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) {
}
fun doAction(broadcastAction: String) {
- instrumentation.context
- .sendBroadcast(createIntentWithAction(broadcastAction))
+ instrumentation.context.sendBroadcast(createIntentWithAction(broadcastAction))
}
companion object {
// Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
- @JvmStatic
- val ORIENTATION_LANDSCAPE = 0
+ @JvmStatic val ORIENTATION_LANDSCAPE = 0
// Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- @JvmStatic
- val ORIENTATION_PORTRAIT = 1
+ @JvmStatic val ORIENTATION_PORTRAIT = 1
}
}
- @FlickerBuilderProvider
- fun buildFlicker(): FlickerBuilder {
- return FlickerBuilder(instrumentation).apply {
- transition(this)
- }
- }
+ /** Defines the transition used to run the test */
+ protected open val thisTransition: FlickerBuilder.() -> Unit = {}
- /**
- * Gets a configuration that handles basic setup and teardown of pip tests
- */
- protected val setupAndTeardown: FlickerBuilder.() -> Unit
+ override val transition: FlickerBuilder.() -> Unit
get() = {
- setup {
- test {
- removeAllTasksButHome()
- device.wakeUpAndGoToHomeScreen()
- }
- }
- teardown {
- eachRun {
- setRotation(Surface.ROTATION_0)
- }
- test {
- removeAllTasksButHome()
- pipApp.exit(wmHelper)
- }
- }
+ defaultSetup(this)
+ defaultEnterPip(this)
+ thisTransition(this)
+ defaultTeardown(this)
}
- /**
- * Gets a configuration that handles basic setup and teardown of pip tests and that
- * launches the Pip app for test
- *
- * @param eachRun If the pip app should be launched in each run (otherwise only 1x per test)
- * @param stringExtras Arguments to pass to the PIP launch intent
- * @param extraSpec Addicional segment of flicker specification
- */
- @JvmOverloads
- protected open fun buildTransition(
- eachRun: Boolean,
- stringExtras: Map<String, String> = mapOf(Components.PipActivity.EXTRA_ENTER_PIP to "true"),
- extraSpec: FlickerBuilder.() -> Unit = {}
- ): FlickerBuilder.() -> Unit {
- return {
- setupAndTeardown(this)
-
- setup {
- test {
- if (!eachRun) {
- pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras)
- wmHelper.waitPipShown()
- }
- }
- eachRun {
- if (eachRun) {
- pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras)
- wmHelper.waitPipShown()
- }
- }
- }
- teardown {
- eachRun {
- if (eachRun) {
- pipApp.exit(wmHelper)
- }
- }
- test {
- if (!eachRun) {
- pipApp.exit(wmHelper)
- }
- }
- }
-
- extraSpec(this)
+ /** Defines the default setup steps required by the test */
+ protected open val defaultSetup: FlickerBuilder.() -> Unit = {
+ setup {
+ setRotation(Rotation.ROTATION_0)
+ removeAllTasksButHome()
}
}
- @Presubmit
- @Test
- open fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible()
-
- @Presubmit
- @Test
- open fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible()
-
- @Presubmit
- @Test
- open fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible()
-
- @Presubmit
- @Test
- open fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible()
+ /** Defines the default method of entering PiP */
+ protected open val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ pipApp.launchViaIntentAndWaitForPip(wmHelper,
+ stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true"))
+ }
+ }
- @Presubmit
- @Test
- open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales()
+ /** Defines the default teardown required to clean up after the test */
+ protected open val defaultTeardown: FlickerBuilder.() -> Unit = {
+ teardown {
+ pipApp.exit(wmHelper)
+ }
+ }
@Presubmit
@Test
- open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
+ fun hasAtMostOnePipDismissOverlayWindow() {
+ val matcher = ComponentNameMatcher("", "pip-dismiss-overlay")
+ flicker.assertWm {
+ val overlaysPerState =
+ trace.entries.map { entry ->
+ entry.windowStates.count { window -> matcher.windowMatchesAnyOf(window) } <= 1
+ }
- @Presubmit
- @Test
- open fun entireScreenCovered() = testSpec.entireScreenCovered()
-} \ No newline at end of file
+ Truth.assertWithMessage("Number of dismiss overlays per state")
+ .that(overlaysPerState)
+ .doesNotContain(false)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
new file mode 100644
index 000000000000..c618e5a24fdf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.app.Activity
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.helpers.WindowUtils
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
+import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test exiting Pip with orientation changes. To run this test: `atest
+ * WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class SetRequestedOrientationWhilePinned(flicker: FlickerTest) : PipTransition(flicker) {
+ private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
+ private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90)
+
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions {
+ // Launch the activity back into fullscreen and ensure that it is now in landscape
+ pipApp.launchViaIntent(wmHelper)
+ // System bar may fade out during fixed rotation.
+ wmHelper
+ .StateSyncBuilder()
+ .withFullScreenApp(pipApp)
+ .withRotation(Rotation.ROTATION_90)
+ .withNavOrTaskBarVisible()
+ .withStatusBarVisible()
+ .waitForAndVerify()
+ }
+ }
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ // Launch the PiP activity fixed as landscape.
+ pipApp.launchViaIntent(
+ wmHelper,
+ stringExtras =
+ mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
+ )
+ // Enter PiP.
+ broadcastActionTrigger.doAction(ActivityOptions.Pip.ACTION_ENTER_PIP)
+ // System bar may fade out during fixed rotation.
+ wmHelper
+ .StateSyncBuilder()
+ .withPipShown()
+ .withRotation(Rotation.ROTATION_0)
+ .withNavOrTaskBarVisible()
+ .withStatusBarVisible()
+ .waitForAndVerify()
+ }
+ }
+
+ /**
+ * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] to
+ * fix a orientation, Tablets instead keep the same orientation and add letterboxes
+ */
+ @Before
+ fun setup() {
+ Assume.assumeFalse(tapl.isTablet)
+ }
+
+ @Presubmit
+ @Test
+ fun displayEndsAt90Degrees() {
+ flicker.assertWmEnd { hasRotation(Rotation.ROTATION_90) }
+ }
+
+ @Presubmit
+ @Test
+ fun pipWindowInsideDisplay() {
+ flicker.assertWmStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
+ }
+
+ @Presubmit
+ @Test
+ fun pipAppShowsOnTop() {
+ flicker.assertWmEnd { isAppWindowOnTop(pipApp) }
+ }
+
+ @Presubmit
+ @Test
+ fun pipLayerInsideDisplay() {
+ flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
+ }
+
+ @Presubmit
+ @Test
+ fun pipAlwaysVisible() {
+ flicker.assertWm { this.isAppWindowVisible(pipApp) }
+ }
+
+ @Presubmit
+ @Test
+ fun pipAppLayerCoversFullScreen() {
+ flicker.assertLayersEnd { visibleRegion(pipApp).coversExactly(endingBounds) }
+ }
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 264243884)
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
deleted file mode 100644
index 51339a1deb4b..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group4
-import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
-import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
-import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
-import com.android.wm.shell.flicker.testapp.Components
-import com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test exiting Pip with orientation changes.
- * To run this test: `atest WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
-open class SetRequestedOrientationWhilePinnedTest(
- testSpec: FlickerTestParameter
-) : PipTransition(testSpec) {
- private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0)
- private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90)
-
- override val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- test {
- removeAllTasksButHome()
- device.wakeUpAndGoToHomeScreen()
- }
- eachRun {
- // Launch the PiP activity fixed as landscape.
- pipApp.launchViaIntent(wmHelper, stringExtras = mapOf(
- EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()))
- // Enter PiP.
- broadcastActionTrigger.doAction(Components.PipActivity.ACTION_ENTER_PIP)
- wmHelper.waitPipShown()
- wmHelper.waitForRotation(Surface.ROTATION_0)
- wmHelper.waitForAppTransitionIdle()
- // System bar may fade out during fixed rotation.
- wmHelper.waitForNavBarStatusBarVisible()
- }
- }
- teardown {
- eachRun {
- pipApp.exit(wmHelper)
- setRotation(Surface.ROTATION_0)
- }
- test {
- removeAllTasksButHome()
- }
- }
- transitions {
- // Launch the activity back into fullscreen and ensure that it is now in landscape
- pipApp.launchViaIntent(wmHelper)
- wmHelper.waitForFullScreenApp(pipApp.component)
- wmHelper.waitForRotation(Surface.ROTATION_90)
- wmHelper.waitForAppTransitionIdle()
- // System bar may fade out during fixed rotation.
- wmHelper.waitForNavBarStatusBarVisible()
- }
- }
-
- @Presubmit
- @Test
- fun displayEndsAt90Degrees() {
- testSpec.assertWmEnd {
- hasRotation(Surface.ROTATION_90)
- }
- }
-
- @Presubmit
- @Test
- override fun navBarLayerIsVisible() = super.navBarLayerIsVisible()
-
- @Presubmit
- @Test
- override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible()
-
- @FlakyTest
- @Test
- override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales()
-
- @Presubmit
- @Test
- fun pipWindowInsideDisplay() {
- testSpec.assertWmStart {
- frameRegion(pipApp.component).coversAtMost(startingBounds)
- }
- }
-
- @Presubmit
- @Test
- fun pipAppShowsOnTop() {
- testSpec.assertWmEnd {
- isAppWindowOnTop(pipApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun pipLayerInsideDisplay() {
- testSpec.assertLayersStart {
- visibleRegion(pipApp.component).coversAtMost(startingBounds)
- }
- }
-
- @Presubmit
- @Test
- fun pipAlwaysVisible() {
- testSpec.assertWm {
- this.isAppWindowVisible(pipApp.component)
- }
- }
-
- @Presubmit
- @Test
- fun pipAppLayerCoversFullScreen() {
- testSpec.assertLayersEnd {
- visibleRegion(pipApp.component).coversExactly(endingBounds)
- }
- }
-
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun getParams(): Collection<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance()
- .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
- repetitions = 1)
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
new file mode 100644
index 000000000000..43d6c8f26126
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.helpers.WindowUtils
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test Pip Stack in bounds after rotations.
+ *
+ * To run this test: `atest WMShellFlickerTests:PipRotationTest`
+ *
+ * Actions:
+ * ```
+ * Launch a [pipApp] in pip mode
+ * Launch another app [fixedApp] (appears below pip)
+ * Rotate the screen from [flicker.scenario.startRotation] to [flicker.scenario.endRotation]
+ * (usually, 0->90 and 90->0)
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited from [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.device.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class ShowPipAndRotateDisplay(flicker: FlickerTest) : PipTransition(flicker) {
+ private val testApp = SimpleAppHelper(instrumentation)
+ private val screenBoundsStart = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
+ private val screenBoundsEnd = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
+
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ setup {
+ testApp.launchViaIntent(wmHelper)
+ setRotation(flicker.scenario.startRotation)
+ }
+ transitions { setRotation(flicker.scenario.endRotation) }
+ }
+
+ /** Checks that [testApp] layer is within [screenBoundsStart] at the start of the transition */
+ @Presubmit
+ @Test
+ fun fixedAppLayer_StartingBounds() {
+ flicker.assertLayersStart { visibleRegion(testApp).coversAtMost(screenBoundsStart) }
+ }
+
+ /** Checks that [testApp] layer is within [screenBoundsEnd] at the end of the transition */
+ @Presubmit
+ @Test
+ fun fixedAppLayer_EndingBounds() {
+ flicker.assertLayersEnd { visibleRegion(testApp).coversAtMost(screenBoundsEnd) }
+ }
+
+ /**
+ * Checks that [testApp] plus [pipApp] layers are within [screenBoundsEnd] at the start of the
+ * transition
+ */
+ @Presubmit
+ @Test
+ fun appLayers_StartingBounds() {
+ flicker.assertLayersStart {
+ visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsStart)
+ }
+ }
+
+ /**
+ * Checks that [testApp] plus [pipApp] layers are within [screenBoundsEnd] at the end of the
+ * transition
+ */
+ @Presubmit
+ @Test
+ fun appLayers_EndingBounds() {
+ flicker.assertLayersEnd { visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsEnd) }
+ }
+
+ /** Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition */
+ private fun pipLayerRotates_StartingBounds_internal() {
+ flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(screenBoundsStart) }
+ }
+
+ /** Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition */
+ @Presubmit
+ @Test
+ fun pipLayerRotates_StartingBounds() {
+ pipLayerRotates_StartingBounds_internal()
+ }
+
+ /** Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition */
+ @Presubmit
+ @Test
+ fun pipLayerRotates_EndingBounds() {
+ flicker.assertLayersEnd { visibleRegion(pipApp).coversAtMost(screenBoundsEnd) }
+ }
+
+ /**
+ * Ensure that the [pipApp] window does not obscure the [testApp] at the start of the transition
+ */
+ @Presubmit
+ @Test
+ fun pipIsAboveFixedAppWindow_Start() {
+ flicker.assertWmStart { isAboveWindow(pipApp, testApp) }
+ }
+
+ /**
+ * Ensure that the [pipApp] window does not obscure the [testApp] at the end of the transition
+ */
+ @Presubmit
+ @Test
+ fun pipIsAboveFixedAppWindow_End() {
+ flicker.assertWmEnd { isAboveWindow(pipApp, testApp) }
+ }
+
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() {
+ super.navBarLayerIsVisibleAtStartAndEnd()
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.rotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt
new file mode 100644
index 000000000000..b7a2c47e3b32
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.flicker.pip
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ShowPipAndRotateDisplayCfArm(flicker: FlickerTest) : ShowPipAndRotateDisplay(flicker) {
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+ * and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.rotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt
new file mode 100644
index 000000000000..000ae8f9458e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip.tv
+
+import android.app.Instrumentation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.PipAppHelper
+
+/** Helper class for PIP app on AndroidTV */
+open class PipAppHelperTv(instrumentation: Instrumentation) : PipAppHelper(instrumentation) {
+ private val appSelector = By.pkg(`package`).depth(0)
+
+ val ui: UiObject2?
+ get() = uiDevice.findObject(appSelector)
+
+ private fun focusOnObject(selector: BySelector): Boolean {
+ // We expect all the focusable UI elements to be arranged in a way so that it is possible
+ // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top"
+ // from "the bottom".
+ repeat(FOCUS_ATTEMPTS) {
+ uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true }
+ ?: error("The object we try to focus on is gone.")
+
+ uiDevice.pressDPadDown()
+ uiDevice.waitForIdle()
+ }
+ return false
+ }
+
+ override fun clickObject(resId: String) {
+ val selector = By.res(`package`, resId)
+ focusOnObject(selector) || error("Could not focus on `$resId` object")
+ uiDevice.pressDPadCenter()
+ }
+
+ @Deprecated(
+ "Use PipAppHelper.closePipWindow(wmHelper) instead",
+ ReplaceWith("closePipWindow(wmHelper)")
+ )
+ override fun closePipWindow() {
+ uiDevice.closeTvPipWindow()
+ }
+
+ /** Taps the pip window and dismisses it by clicking on the X button. */
+ override fun closePipWindow(wmHelper: WindowManagerStateHelper) {
+ uiDevice.closeTvPipWindow()
+
+ // Wait for animation to complete.
+ wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify()
+ }
+
+ fun waitUntilClosed(): Boolean {
+ val appSelector = By.pkg(`package`).depth(0)
+ return uiDevice.wait(Until.gone(appSelector), APP_CLOSE_WAIT_TIME_MS)
+ }
+
+ companion object {
+ private const val FOCUS_ATTEMPTS = 20
+ private const val APP_CLOSE_WAIT_TIME_MS = 3_000L
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt
index 9c50630095be..2cb18f948f0e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,47 +14,36 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.pip.tv
import android.app.Instrumentation
import android.content.pm.PackageManager
-import android.content.pm.PackageManager.FEATURE_LEANBACK
-import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY
import android.view.Surface
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
-import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.runners.Parameterized
-/**
- * Base class of all Flicker test that performs common functions for all flicker tests:
- *
- * - Caches transitions so that a transition is run once and the transition results are used by
- * tests multiple times. This is needed for parameterized tests which call the BeforeClass methods
- * multiple times.
- * - Keeps track of all test artifacts and deletes ones which do not need to be reviewed.
- * - Fails tests if results are not available for any test due to jank.
- */
-abstract class FlickerTestBase(
- protected val rotationName: String,
- protected val rotation: Int
-) {
+abstract class PipTestBase(protected val rotationName: String, protected val rotation: Int) {
val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
val uiDevice = UiDevice.getInstance(instrumentation)
val packageManager: PackageManager = instrumentation.context.packageManager
protected val isTelevision: Boolean by lazy {
packageManager.run {
- hasSystemFeature(FEATURE_LEANBACK) || hasSystemFeature(FEATURE_LEANBACK_ONLY)
+ hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
+ hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)
}
}
+ protected val testApp = PipAppHelperTv(instrumentation)
- /**
- * By default WmShellFlickerTests do not run on TV devices.
- * If the test should run on TV - it should override this method.
- */
@Before
- open fun televisionSetUp() = assumeFalse(isTelevision)
+ open fun televisionSetUp() {
+ /**
+ * The super implementation assumes ([org.junit.Assume]) that not running on TV, thus
+ * disabling the test on TV. This test, however, *should run on TV*, so we overriding this
+ * method and simply leaving it blank.
+ */
+ }
companion object {
@Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt
index 49094e609fbc..8a073abf032c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt
@@ -25,16 +25,11 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-/**
- * Test Pip Menu on TV.
- * To run this test: `atest WMShellFlickerTests:TvPipBasicTest`
- */
+/** Test Pip Menu on TV. To run this test: `atest WMShellFlickerTests:TvPipBasicTest` */
@RequiresDevice
@RunWith(Parameterized::class)
-class TvPipBasicTest(
- private val radioButtonId: String,
- private val pipWindowRatio: Rational?
-) : TvPipTestBase() {
+class TvPipBasicTest(private val radioButtonId: String, private val pipWindowRatio: Rational?) :
+ TvPipTestBase() {
@Test
fun enterPip_openMenu_pressBack_closePip() {
@@ -43,10 +38,10 @@ class TvPipBasicTest(
// Set up ratio and enter Pip
testApp.clickObject(radioButtonId)
- testApp.clickEnterPipButton()
+ testApp.clickEnterPipButton(wmHelper)
- val actualRatio: Float = testApp.ui?.visibleBounds?.ratio
- ?: fail("Application UI not found")
+ val actualRatio: Float =
+ testApp.ui?.visibleBounds?.ratio ?: fail("Application UI not found")
pipWindowRatio?.let { expectedRatio ->
assertEquals("Wrong Pip window ratio", expectedRatio.toFloat(), actualRatio)
}
@@ -62,7 +57,8 @@ class TvPipBasicTest(
// Make sure Pip Window ration remained the same after Pip menu was closed
testApp.ui?.visibleBounds?.let { newBounds ->
assertEquals("Pip window ratio has changed", actualRatio, newBounds.ratio)
- } ?: fail("Application UI not found")
+ }
+ ?: fail("Application UI not found")
// Close Pip
testApp.closePipWindow()
@@ -77,11 +73,11 @@ class TvPipBasicTest(
fun getParams(): Collection<Array<Any?>> {
infix fun Int.to(denominator: Int) = Rational(this, denominator)
return listOf(
- arrayOf("ratio_default", null),
- arrayOf("ratio_square", 1 to 1),
- arrayOf("ratio_wide", 2 to 1),
- arrayOf("ratio_tall", 1 to 2)
+ arrayOf("ratio_default", null),
+ arrayOf("ratio_square", 1 to 1),
+ arrayOf("ratio_wide", 2 to 1),
+ arrayOf("ratio_tall", 1 to 2)
)
}
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
index 061218a015e4..0432a8497fbe 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
@@ -19,35 +19,37 @@ package com.android.wm.shell.flicker.pip.tv
import android.graphics.Rect
import androidx.test.filters.RequiresDevice
import androidx.test.uiautomator.UiObject2
+import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.testapp.Components
import com.android.wm.shell.flicker.wait
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
-/**
- * Test Pip Menu on TV.
- * To run this test: `atest WMShellFlickerTests:TvPipMenuTests`
- */
+/** Test Pip Menu on TV. To run this test: `atest WMShellFlickerTests:TvPipMenuTests` */
@RequiresDevice
class TvPipMenuTests : TvPipTestBase() {
- private val systemUiResources =
- packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
- private val pipBoundsWhileInMenu: Rect = systemUiResources.run {
- val bounds = getString(getIdentifier("pip_menu_bounds", "string",
- SYSTEM_UI_PACKAGE_NAME))
- Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds")
+ private val systemUiResources by lazy {
+ packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
+ }
+ private val pipBoundsWhileInMenu: Rect by lazy {
+ systemUiResources.run {
+ val bounds =
+ getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME))
+ Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds")
+ }
}
- private val playButtonDescription = systemUiResources.run {
- getString(getIdentifier("pip_play", "string",
- SYSTEM_UI_PACKAGE_NAME))
+ private val playButtonDescription by lazy {
+ systemUiResources.run {
+ getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME))
+ }
}
- private val pauseButtonDescription = systemUiResources.run {
- getString(getIdentifier("pip_pause", "string",
- SYSTEM_UI_PACKAGE_NAME))
+ private val pauseButtonDescription by lazy {
+ systemUiResources.run {
+ getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME))
+ }
}
@Before
@@ -61,20 +63,29 @@ class TvPipMenuTests : TvPipTestBase() {
enterPip_openMenu_assertShown()
// Make sure the PiP task is positioned where it should be.
- val activityBounds: Rect = testApp.ui?.visibleBounds
- ?: error("Could not retrieve Pip Activity bounds")
- assertTrue("Pip Activity is positioned correctly while Pip menu is shown",
- pipBoundsWhileInMenu == activityBounds)
+ val activityBounds: Rect =
+ testApp.ui?.visibleBounds ?: error("Could not retrieve Pip Activity bounds")
+ assertTrue(
+ "Pip Activity is positioned correctly while Pip menu is shown",
+ pipBoundsWhileInMenu == activityBounds
+ )
// Make sure the Pip Menu Actions are positioned correctly.
uiDevice.findTvPipMenuControls()?.visibleBounds?.run {
- assertTrue("Pip Menu Actions should be positioned below the Activity in Pip",
- top >= activityBounds.bottom)
- assertTrue("Pip Menu Actions should be positioned central horizontally",
- centerX() == uiDevice.displayWidth / 2)
- assertTrue("Pip Menu Actions should be fully shown on the screen",
- left >= 0 && right <= uiDevice.displayWidth && bottom <= uiDevice.displayHeight)
- } ?: error("Could not retrieve Pip Menu Actions bounds")
+ assertTrue(
+ "Pip Menu Actions should be positioned below the Activity in Pip",
+ top >= activityBounds.bottom
+ )
+ assertTrue(
+ "Pip Menu Actions should be positioned central horizontally",
+ centerX() == uiDevice.displayWidth / 2
+ )
+ assertTrue(
+ "Pip Menu Actions should be fully shown on the screen",
+ left >= 0 && right <= uiDevice.displayWidth && bottom <= uiDevice.displayHeight
+ )
+ }
+ ?: error("Could not retrieve Pip Menu Actions bounds")
testApp.closePipWindow()
}
@@ -107,7 +118,7 @@ class TvPipMenuTests : TvPipTestBase() {
// PiP menu should contain the Close button
uiDevice.findTvPipMenuCloseButton()
- ?: fail("\"Close PIP\" button should be shown in Pip menu")
+ ?: fail("\"Close PIP\" button should be shown in Pip menu")
// Clicking on the Close button should close the app
uiDevice.clickTvPipMenuCloseButton()
@@ -120,13 +131,15 @@ class TvPipMenuTests : TvPipTestBase() {
// PiP menu should contain the Fullscreen button
uiDevice.findTvPipMenuFullscreenButton()
- ?: fail("\"Full screen\" button should be shown in Pip menu")
+ ?: fail("\"Full screen\" button should be shown in Pip menu")
// Clicking on the fullscreen button should return app to the fullscreen mode.
// Click, wait for the app to go fullscreen
uiDevice.clickTvPipMenuFullscreenButton()
- assertTrue("\"Full screen\" button should open the app fullscreen",
- wait { testApp.ui?.isFullscreen(uiDevice) ?: false })
+ assertTrue(
+ "\"Full screen\" button should open the app fullscreen",
+ wait { testApp.ui?.isFullscreen(uiDevice) ?: false }
+ )
// Close the app
uiDevice.pressBack()
@@ -143,8 +156,10 @@ class TvPipMenuTests : TvPipTestBase() {
// PiP menu should contain the Pause button
uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)
- ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " +
- "playing media session.")
+ ?: fail(
+ "\"Pause\" button should be shown in Pip menu if there is an active " +
+ "playing media session."
+ )
// When we pause media, the button should change from Pause to Play
uiDevice.clickTvPipMenuElementWithDescription(pauseButtonDescription)
@@ -152,8 +167,10 @@ class TvPipMenuTests : TvPipTestBase() {
assertFullscreenAndCloseButtonsAreShown()
// PiP menu should contain the Play button now
uiDevice.waitForTvPipMenuElementWithDescription(playButtonDescription)
- ?: fail("\"Play\" button should be shown in Pip menu if there is an active " +
- "paused media session.")
+ ?: fail(
+ "\"Play\" button should be shown in Pip menu if there is an active " +
+ "paused media session."
+ )
testApp.closePipWindow()
}
@@ -165,44 +182,47 @@ class TvPipMenuTests : TvPipTestBase() {
enterPip_openMenu_assertShown()
// PiP menu should contain "No-Op", "Off" and "Clear" buttons...
- uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP)
- ?: fail("\"No-Op\" button should be shown in Pip menu")
- uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF)
- ?: fail("\"Off\" button should be shown in Pip menu")
- uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR)
- ?: fail("\"Clear\" button should be shown in Pip menu")
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP)
+ ?: fail("\"No-Op\" button should be shown in Pip menu")
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF)
+ ?: fail("\"Off\" button should be shown in Pip menu")
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR)
+ ?: fail("\"Clear\" button should be shown in Pip menu")
// ... and should also contain the "Full screen" and "Close" buttons.
assertFullscreenAndCloseButtonsAreShown()
- uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF)
+ uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF)
// Invoking the "Off" action should replace it with the "On" action/button and should
// remove the "No-Op" action/button. "Clear" action/button should remain in the menu ...
- uiDevice.waitForTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_ON)
- ?: fail("\"On\" button should be shown in Pip for a corresponding custom action")
- assertNull("\"No-Op\" button should not be shown in Pip menu",
- uiDevice.findTvPipMenuElementWithDescription(
- Components.PipActivity.MENU_ACTION_NO_OP))
- uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR)
- ?: fail("\"Clear\" button should be shown in Pip menu")
+ uiDevice.waitForTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_ON)
+ ?: fail("\"On\" button should be shown in Pip for a corresponding custom action")
+ assertNull(
+ "\"No-Op\" button should not be shown in Pip menu",
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP)
+ )
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR)
+ ?: fail("\"Clear\" button should be shown in Pip menu")
// ... as well as the "Full screen" and "Close" buttons.
assertFullscreenAndCloseButtonsAreShown()
- uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR)
+ uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR)
// Invoking the "Clear" action should remove all the custom actions and their corresponding
// buttons, ...
- uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(
- Components.PipActivity.MENU_ACTION_ON)?.also {
- isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu")
- }
- assertNull("\"Off\" button should not be shown in Pip menu",
- uiDevice.findTvPipMenuElementWithDescription(
- Components.PipActivity.MENU_ACTION_OFF))
- assertNull("\"Clear\" button should not be shown in Pip menu",
- uiDevice.findTvPipMenuElementWithDescription(
- Components.PipActivity.MENU_ACTION_CLEAR))
- assertNull("\"No-Op\" button should not be shown in Pip menu",
- uiDevice.findTvPipMenuElementWithDescription(
- Components.PipActivity.MENU_ACTION_NO_OP))
+ uiDevice
+ .waitUntilTvPipMenuElementWithDescriptionIsGone(ActivityOptions.Pip.MENU_ACTION_ON)
+ ?.also { isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu") }
+ assertNull(
+ "\"Off\" button should not be shown in Pip menu",
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF)
+ )
+ assertNull(
+ "\"Clear\" button should not be shown in Pip menu",
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR)
+ )
+ assertNull(
+ "\"No-Op\" button should not be shown in Pip menu",
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP)
+ )
// ... but the menu should still contain the "Full screen" and "Close" buttons.
assertFullscreenAndCloseButtonsAreShown()
@@ -217,26 +237,32 @@ class TvPipMenuTests : TvPipTestBase() {
enterPip_openMenu_assertShown()
// PiP menu should contain "No-Op", "Off" and "Clear" buttons for the custom actions...
- uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_NO_OP)
- ?: fail("\"No-Op\" button should be shown in Pip menu")
- uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_OFF)
- ?: fail("\"Off\" button should be shown in Pip menu")
- uiDevice.findTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR)
- ?: fail("\"Clear\" button should be shown in Pip menu")
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_NO_OP)
+ ?: fail("\"No-Op\" button should be shown in Pip menu")
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_OFF)
+ ?: fail("\"Off\" button should be shown in Pip menu")
+ uiDevice.findTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR)
+ ?: fail("\"Clear\" button should be shown in Pip menu")
// ... should also contain the "Full screen" and "Close" buttons, ...
assertFullscreenAndCloseButtonsAreShown()
// ... but should not contain media buttons.
- assertNull("\"Play\" button should not be shown in menu when there are custom actions",
- uiDevice.findTvPipMenuElementWithDescription(playButtonDescription))
- assertNull("\"Pause\" button should not be shown in menu when there are custom actions",
- uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription))
-
- uiDevice.clickTvPipMenuElementWithDescription(Components.PipActivity.MENU_ACTION_CLEAR)
+ assertNull(
+ "\"Play\" button should not be shown in menu when there are custom actions",
+ uiDevice.findTvPipMenuElementWithDescription(playButtonDescription)
+ )
+ assertNull(
+ "\"Pause\" button should not be shown in menu when there are custom actions",
+ uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)
+ )
+
+ uiDevice.clickTvPipMenuElementWithDescription(ActivityOptions.Pip.MENU_ACTION_CLEAR)
// Invoking the "Clear" action should remove all the custom actions, which should bring up
// media buttons...
uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription)
- ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " +
- "playing media session.")
+ ?: fail(
+ "\"Pause\" button should be shown in Pip menu if there is an active " +
+ "playing media session."
+ )
// ... while the "Full screen" and "Close" buttons should remain in the menu.
assertFullscreenAndCloseButtonsAreShown()
@@ -244,7 +270,7 @@ class TvPipMenuTests : TvPipTestBase() {
}
private fun enterPip_openMenu_assertShown(): UiObject2 {
- testApp.clickEnterPipButton()
+ testApp.clickEnterPipButton(wmHelper)
// Pressing the Window key should bring up Pip menu
uiDevice.pressWindowKey()
return uiDevice.waitForTvPipMenu() ?: fail("Pip menu should have been shown")
@@ -252,8 +278,8 @@ class TvPipMenuTests : TvPipTestBase() {
private fun assertFullscreenAndCloseButtonsAreShown() {
uiDevice.findTvPipMenuCloseButton()
- ?: fail("\"Close PIP\" button should be shown in Pip menu")
+ ?: fail("\"Close PIP\" button should be shown in Pip menu")
uiDevice.findTvPipMenuFullscreenButton()
- ?: fail("\"Full screen\" button should be shown in Pip menu")
+ ?: fail("\"Full screen\" button should be shown in Pip menu")
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
index bcf38d340867..90406c510bad 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
@@ -34,8 +34,8 @@ import org.junit.Before
import org.junit.Test
/**
- * Test Pip Notifications on TV.
- * To run this test: `atest WMShellFlickerTests:TvPipNotificationTests`
+ * Test Pip Notifications on TV. To run this test: `atest
+ * WMShellFlickerTests:TvPipNotificationTests`
*/
@RequiresDevice
class TvPipNotificationTests : TvPipTestBase() {
@@ -56,49 +56,58 @@ class TvPipNotificationTests : TvPipTestBase() {
@Test
fun pipNotification_postedAndDismissed() {
testApp.launchViaIntent()
- testApp.clickEnterPipButton()
+ testApp.clickEnterPipButton(wmHelper)
- assertNotNull("Pip notification should have been posted",
- waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) })
+ assertNotNull(
+ "Pip notification should have been posted",
+ waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) }
+ )
testApp.closePipWindow()
- assertTrue("Pip notification should have been dismissed",
- waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) })
+ assertTrue(
+ "Pip notification should have been dismissed",
+ waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) }
+ )
}
@Test
fun pipNotification_closeIntent() {
testApp.launchViaIntent()
- testApp.clickEnterPipButton()
-
- val notification: StatusBarNotification = waitForNotificationToAppear {
- it.isPipNotificationWithTitle(testApp.appName)
- } ?: fail("Pip notification should have been posted")
-
- notification.deleteIntent?.send()
- ?: fail("Pip notification should contain `delete_intent`")
-
- assertTrue("Pip should have closed by sending the `delete_intent`",
- testApp.waitUntilClosed())
- assertTrue("Pip notification should have been dismissed",
- waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) })
+ testApp.clickEnterPipButton(wmHelper)
+
+ val notification: StatusBarNotification =
+ waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) }
+ ?: fail("Pip notification should have been posted")
+
+ notification.deleteIntent?.send() ?: fail("Pip notification should contain `delete_intent`")
+
+ assertTrue(
+ "Pip should have closed by sending the `delete_intent`",
+ testApp.waitUntilClosed()
+ )
+ assertTrue(
+ "Pip notification should have been dismissed",
+ waitForNotificationToDisappear { it.isPipNotificationWithTitle(testApp.appName) }
+ )
}
@Test
fun pipNotification_menuIntent() {
- testApp.launchViaIntent()
- testApp.clickEnterPipButton()
+ testApp.launchViaIntent(wmHelper)
+ testApp.clickEnterPipButton(wmHelper)
- val notification: StatusBarNotification = waitForNotificationToAppear {
- it.isPipNotificationWithTitle(testApp.appName)
- } ?: fail("Pip notification should have been posted")
+ val notification: StatusBarNotification =
+ waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) }
+ ?: fail("Pip notification should have been posted")
notification.contentIntent?.send()
?: fail("Pip notification should contain `content_intent`")
- assertNotNull("Pip menu should have been shown after sending `content_intent`",
- uiDevice.waitForTvPipMenu())
+ assertNotNull(
+ "Pip menu should have been shown after sending `content_intent`",
+ uiDevice.waitForTvPipMenu()
+ )
uiDevice.pressBack()
testApp.closePipWindow()
@@ -106,41 +115,44 @@ class TvPipNotificationTests : TvPipTestBase() {
@Test
fun pipNotification_mediaSessionTitle_isDisplayed() {
- testApp.launchViaIntent()
+ testApp.launchViaIntent(wmHelper)
// Start media session and to PiP
testApp.clickStartMediaSessionButton()
- testApp.clickEnterPipButton()
+ testApp.clickEnterPipButton(wmHelper)
// Wait for the correct notification to show up...
- waitForNotificationToAppear {
- it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING)
- } ?: fail("Pip notification with media session title should have been posted")
+ waitForNotificationToAppear { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) }
+ ?: fail("Pip notification with media session title should have been posted")
// ... and make sure "regular" PiP notification is now shown
- assertNull("Regular notification should not have been posted",
- findNotification { it.isPipNotificationWithTitle(testApp.appName) })
+ assertNull(
+ "Regular notification should not have been posted",
+ findNotification { it.isPipNotificationWithTitle(testApp.appName) }
+ )
// Pause the media session. When paused the application updates the title for the media
// session. This change should be reflected in the notification.
testApp.pauseMedia()
// Wait for the "paused" notification to show up...
- waitForNotificationToAppear {
- it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED)
- } ?: fail("Pip notification with media session title should have been posted")
+ waitForNotificationToAppear { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) }
+ ?: fail("Pip notification with media session title should have been posted")
// ... and make sure "playing" PiP notification is gone
- assertNull("Regular notification should not have been posted",
- findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) })
+ assertNull(
+ "Regular notification should not have been posted",
+ findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PLAYING) }
+ )
// Now stop the media session, which should revert the title to the "default" one.
testApp.stopMedia()
// Wait for the "regular" notification to show up...
- waitForNotificationToAppear {
- it.isPipNotificationWithTitle(testApp.appName)
- } ?: fail("Pip notification with media session title should have been posted")
+ waitForNotificationToAppear { it.isPipNotificationWithTitle(testApp.appName) }
+ ?: fail("Pip notification with media session title should have been posted")
// ... and make sure previous ("paused") notification is gone
- assertNull("Regular notification should not have been posted",
- findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) })
+ assertNull(
+ "Regular notification should not have been posted",
+ findNotification { it.isPipNotificationWithTitle(TITLE_MEDIA_SESSION_PAUSED) }
+ )
testApp.closePipWindow()
}
@@ -170,4 +182,4 @@ private val StatusBarNotification.deleteIntent: PendingIntent?
get() = tvExtensions?.getParcelable("delete_intent")
private fun StatusBarNotification.isPipNotificationWithTitle(expectedTitle: String): Boolean =
- tag == "TvPip" && title == expectedTitle \ No newline at end of file
+ tag == "TvPip" && title == expectedTitle
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
index 9c3b0fa183b6..6104b7bdacba 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
@@ -20,11 +20,11 @@ import android.app.ActivityManager
import android.app.IActivityManager
import android.app.IProcessObserver
import android.os.SystemClock
+import android.tools.device.helpers.wakeUpAndGoToHomeScreen
+import android.tools.device.traces.parsers.WindowManagerStateHelper
import android.view.Surface.ROTATION_0
import android.view.Surface.rotationToString
-import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.pip.PipTestBase
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assume.assumeTrue
@@ -33,6 +33,7 @@ import org.junit.Before
abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATION_0) {
private val systemUiProcessObserver = SystemUiProcessObserver()
+ protected val wmHelper = WindowManagerStateHelper()
@Before
final override fun televisionSetUp() {
@@ -67,7 +68,8 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO
fun start() {
hasDied = false
uiAutomation.adoptShellPermissionIdentity(
- android.Manifest.permission.SET_ACTIVITY_WATCHER)
+ android.Manifest.permission.SET_ACTIVITY_WATCHER
+ )
activityManager.registerProcessObserver(this)
}
@@ -88,4 +90,4 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO
companion object {
private const val AFTER_TEXT_PROCESS_CHECK_DELAY = 1_000L // 1 sec
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
index 1c663409b913..b0adbe1d07ce 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
@@ -33,32 +33,31 @@ private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "tv_pip_menu_fullscreen_but
private const val FOCUS_ATTEMPTS = 10
private const val WAIT_TIME_MS = 3_000L
-private val TV_PIP_MENU_SELECTOR =
- By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID)
+private val TV_PIP_MENU_SELECTOR = By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID)
private val TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR =
- By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_BUTTONS_CONTAINER_ID)
+ By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_BUTTONS_CONTAINER_ID)
private val TV_PIP_MENU_CLOSE_BUTTON_SELECTOR =
- By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID)
+ By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID)
private val TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR =
- By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID)
+ By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID)
fun UiDevice.waitForTvPipMenu(): UiObject2? =
- wait(Until.findObject(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS)
+ wait(Until.findObject(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS)
fun UiDevice.waitForTvPipMenuToClose(): Boolean =
- wait(Until.gone(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS)
+ wait(Until.gone(TV_PIP_MENU_SELECTOR), WAIT_TIME_MS)
fun UiDevice.findTvPipMenuControls(): UiObject2? =
- findTvPipMenuElement(TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR)
+ findTvPipMenuElement(TV_PIP_MENU_BUTTONS_CONTAINER_SELECTOR)
fun UiDevice.findTvPipMenuCloseButton(): UiObject2? =
- findTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR)
+ findTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR)
fun UiDevice.findTvPipMenuFullscreenButton(): UiObject2? =
- findTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR)
+ findTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR)
fun UiDevice.findTvPipMenuElementWithDescription(desc: String): UiObject2? =
- findTvPipMenuElement(By.desc(desc))
+ findTvPipMenuElement(By.desc(desc))
private fun UiDevice.findTvPipMenuElement(selector: BySelector): UiObject2? =
findObject(TV_PIP_MENU_SELECTOR)?.findObject(selector)
@@ -70,11 +69,10 @@ fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? {
// descendant and then retrieve the element from the menu and return to the caller of this
// method.
val elementSelector = By.desc(desc)
- val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR)
- .hasDescendant(elementSelector)
+ val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector)
return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS)
- ?.findObject(elementSelector)
+ ?.findObject(elementSelector)
}
fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boolean? {
@@ -86,18 +84,17 @@ fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boole
fun UiDevice.clickTvPipMenuCloseButton() {
focusOnAndClickTvPipMenuElement(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) ||
- error("Could not focus on the Close button")
+ error("Could not focus on the Close button")
}
fun UiDevice.clickTvPipMenuFullscreenButton() {
focusOnAndClickTvPipMenuElement(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) ||
- error("Could not focus on the Fullscreen button")
+ error("Could not focus on the Fullscreen button")
}
fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) {
- focusOnAndClickTvPipMenuElement(By.desc(desc)
- .pkg(SYSTEM_UI_PACKAGE_NAME)) ||
- error("Could not focus on the Pip menu object with \"$desc\" description")
+ focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) ||
+ error("Could not focus on the Pip menu object with \"$desc\" description")
// So apparently Accessibility framework on TV is not very reliable and sometimes the state of
// the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of
// the "real" state of the "UI tree". It seems, however, that moving focus around the tree
@@ -110,7 +107,8 @@ fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) {
private fun UiDevice.focusOnAndClickTvPipMenuElement(selector: BySelector): Boolean {
repeat(FOCUS_ATTEMPTS) {
- val element = findTvPipMenuElement(selector)
+ val element =
+ findTvPipMenuElement(selector)
?: error("The Pip Menu element we try to focus on is gone.")
if (element.isFocusedOrHasFocusedChild) {
@@ -119,10 +117,11 @@ private fun UiDevice.focusOnAndClickTvPipMenuElement(selector: BySelector): Bool
}
findTvPipMenuElement(By.focused(true))?.let { focused ->
- if (element.visibleCenter.x < focused.visibleCenter.x)
- pressDPadLeft() else pressDPadRight()
+ if (element.visibleCenter.x < focused.visibleCenter.x) pressDPadLeft()
+ else pressDPadRight()
waitForIdle()
- } ?: error("Pip menu does not contain a focused element")
+ }
+ ?: error("Pip menu does not contain a focused element")
}
return false
@@ -155,9 +154,8 @@ private fun UiDevice.moveFocus() {
fun UiDevice.pressWindowKey() = pressKeyCode(KeyEvent.KEYCODE_WINDOW)
-fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run {
- height() == uiDevice.displayHeight && width() == uiDevice.displayWidth
-}
+fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean =
+ visibleBounds.run { height() == uiDevice.displayHeight && width() == uiDevice.displayWidth }
val UiObject2.isFocusedOrHasFocusedChild: Boolean
get() = isFocused || findObject(By.focused(true)) != null
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
new file mode 100644
index 000000000000..d1f0980786bf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.EdgeExtensionComponentMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.appWindowKeepVisible
+import com.android.wm.shell.flicker.layerKeepVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
+import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test copy content from the left to the right side of the split-screen.
+ *
+ * To run this test: `atest WMShellFlickerTests:CopyContentInSplit`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class CopyContentInSplit(override val flicker: FlickerTest) :
+ CopyContentInSplitBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ override fun cujCompleted() {
+ flicker.appWindowIsVisibleAtStart(primaryApp)
+ flicker.appWindowIsVisibleAtStart(textEditApp)
+ flicker.splitScreenDividerIsVisibleAtStart()
+
+ flicker.appWindowIsVisibleAtEnd(primaryApp)
+ flicker.appWindowIsVisibleAtEnd(textEditApp)
+ flicker.splitScreenDividerIsVisibleAtEnd()
+
+ // The validation of copied text is already done in SplitScreenUtils.copyContentInSplit()
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+
+ @Presubmit @Test fun primaryAppLayerKeepVisible() = flicker.layerKeepVisible(primaryApp)
+
+ @Presubmit @Test fun textEditAppLayerKeepVisible() = flicker.layerKeepVisible(textEditApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsKeepVisible() =
+ flicker.splitAppLayerBoundsKeepVisible(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun textEditAppBoundsKeepVisible() =
+ flicker.splitAppLayerBoundsKeepVisible(
+ textEditApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+
+ @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp)
+
+ @Presubmit @Test fun textEditAppWindowKeepVisible() = flicker.appWindowKeepVisible(textEditApp)
+
+ /** {@inheritDoc} */
+ @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+ flicker.assertLayers {
+ this.visibleLayersShownMoreThanOneConsecutiveEntry(
+ ignoreLayers =
+ listOf(
+ ComponentNameMatcher.SPLASH_SCREEN,
+ ComponentNameMatcher.SNAPSHOT,
+ ComponentNameMatcher.IME_SNAPSHOT,
+ EdgeExtensionComponentMatcher(),
+ magnifierLayer,
+ popupWindowLayer
+ )
+ )
+ }
+ }
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 264241018)
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
new file mode 100644
index 000000000000..4505b9978b76
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.helpers.WindowUtils
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.layerBecomesInvisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
+import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
+import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByDividerBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test dismiss split screen by dragging the divider bar.
+ *
+ * To run this test: `atest WMShellFlickerTests:DismissSplitScreenByDivider`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class DismissSplitScreenByDivider(override val flicker: FlickerTest) :
+ DismissSplitScreenByDividerBenchmark(flicker), ICommonAssertions {
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesInvisible() = flicker.splitScreenDividerBecomesInvisible()
+
+ @Presubmit
+ @Test
+ fun primaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsBecomesInvisible() =
+ flicker.splitAppLayerBoundsBecomesInvisible(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsIsFullscreenAtEnd() {
+ flicker.assertLayersEnd {
+ val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
+ visibleRegion(secondaryApp).coversExactly(displayBounds)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
+
+ /** {@inheritDoc} */
+ @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 206753786)
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
new file mode 100644
index 000000000000..e05b22141e4d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.layerBecomesInvisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
+import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
+import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByGoHomeBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test dismiss split screen by go home.
+ *
+ * To run this test: `atest WMShellFlickerTests:DismissSplitScreenByGoHome`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class DismissSplitScreenByGoHome(override val flicker: FlickerTest) :
+ DismissSplitScreenByGoHomeBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesInvisible() = flicker.splitScreenDividerBecomesInvisible()
+
+ @FlakyTest(bugId = 241525302)
+ @Test
+ fun primaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(primaryApp)
+
+ // TODO(b/245472831): Move back to presubmit after shell transitions landing.
+ @FlakyTest(bugId = 245472831)
+ @Test
+ fun secondaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(secondaryApp)
+
+ // TODO(b/245472831): Move back to presubmit after shell transitions landing.
+ @FlakyTest(bugId = 245472831)
+ @Test
+ fun primaryAppBoundsBecomesInvisible() =
+ flicker.splitAppLayerBoundsBecomesInvisible(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @FlakyTest(bugId = 250530241)
+ @Test
+ fun secondaryAppBoundsBecomesInvisible() =
+ flicker.splitAppLayerBoundsBecomesInvisible(
+ secondaryApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 251268711)
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 206753786)
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @FlakyTest
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
new file mode 100644
index 000000000000..63c5d14d85e1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.appWindowKeepVisible
+import com.android.wm.shell.flicker.layerKeepVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsChanges
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
+import com.android.wm.shell.flicker.splitscreen.benchmark.DragDividerToResizeBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test resize split by dragging the divider bar.
+ *
+ * To run this test: `atest WMShellFlickerTests:DragDividerToResize`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class DragDividerToResize(override val flicker: FlickerTest) :
+ DragDividerToResizeBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ override fun cujCompleted() {
+ flicker.appWindowIsVisibleAtStart(primaryApp)
+ flicker.appWindowIsVisibleAtStart(secondaryApp)
+ flicker.splitScreenDividerIsVisibleAtStart()
+
+ flicker.appWindowIsVisibleAtEnd(primaryApp)
+ flicker.appWindowIsVisibleAtEnd(secondaryApp)
+ flicker.splitScreenDividerIsVisibleAtEnd()
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+
+ @Presubmit
+ @Test
+ fun primaryAppLayerVisibilityChanges() {
+ flicker.assertLayers {
+ this.isVisible(secondaryApp)
+ .then()
+ .isInvisible(secondaryApp)
+ .then()
+ .isVisible(secondaryApp)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerVisibilityChanges() {
+ flicker.assertLayers {
+ this.isVisible(secondaryApp)
+ .then()
+ .isInvisible(secondaryApp)
+ .then()
+ .isVisible(secondaryApp)
+ }
+ }
+
+ @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(secondaryApp)
+
+ @FlakyTest(bugId = 245472831)
+ @Test
+ fun primaryAppBoundsChanges() {
+ flicker.splitAppLayerBoundsChanges(
+ primaryApp,
+ landscapePosLeft = true,
+ portraitPosTop = false
+ )
+ }
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsChanges() =
+ flicker.splitAppLayerBoundsChanges(
+ secondaryApp,
+ landscapePosLeft = false,
+ portraitPosTop = true
+ )
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
index 702710caded7..e55868675da7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
@@ -16,24 +16,25 @@
package com.android.wm.shell.flicker.splitscreen
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
import com.android.wm.shell.flicker.appWindowBecomesVisible
import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
import com.android.wm.shell.flicker.layerBecomesVisible
import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
-import org.junit.Assume
-import org.junit.Before
+import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromAllAppsBenchmark
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,8 +42,8 @@ import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
/**
- * Test enter split screen by dragging app icon from all apps.
- * This test is only for large screen devices.
+ * Test enter split screen by dragging app icon from all apps. This test is only for large screen
+ * devices.
*
* To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromAllApps`
*/
@@ -50,74 +51,120 @@ import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class EnterSplitScreenByDragFromAllApps(
- testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
-
- @Before
- open fun before() {
- Assume.assumeTrue(taplInstrumentation.isTablet)
- }
-
+class EnterSplitScreenByDragFromAllApps(override val flicker: FlickerTest) :
+ EnterSplitScreenByDragFromAllAppsBenchmark(flicker), ICommonAssertions {
override val transition: FlickerBuilder.() -> Unit
get() = {
- super.transition(this)
- setup {
- eachRun {
- taplInstrumentation.goHome()
- primaryApp.launchViaIntent(wmHelper)
- }
- }
- transitions {
- taplInstrumentation.launchedAppState.taskbar
- .openAllApps()
- .getAppIcon(secondaryApp.appName)
- .dragToSplitscreen(secondaryApp.component.packageName,
- primaryApp.component.packageName)
- }
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
}
- @Presubmit
+ @FlakyTest(bugId = 245472831)
@Test
- fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+ fun splitScreenDividerBecomesVisible() {
+ flicker.splitScreenDividerBecomesVisible()
+ }
+ // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready.
@Presubmit
@Test
- fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+ fun splitScreenDividerIsVisibleAtEnd() {
+ flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+ }
+
+ @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
@Presubmit
@Test
- fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component)
+ fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
@Presubmit
@Test
- fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- testSpec.endRotation, primaryApp.component, false /* splitLeftTop */)
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = false,
+ portraitPosTop = false
+ )
@Presubmit
@Test
- fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
- testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */)
+ fun secondaryAppBoundsBecomesVisible() =
+ flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp)
@Presubmit
@Test
- fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+ fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
@Presubmit
@Test
- fun secondaryAppWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(secondaryApp.component)
+ fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
// TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
- supportedNavigationModes =
- listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY))
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
}
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
index 7323d992ecd4..ab8ecc54e71c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -16,25 +16,24 @@
package com.android.wm.shell.flicker.splitscreen
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
import androidx.test.filters.RequiresDevice
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
import com.android.wm.shell.flicker.layerBecomesVisible
import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
-import org.junit.Assume
-import org.junit.Before
+import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromNotificationBenchmark
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,8 +41,8 @@ import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
/**
- * Test enter split screen by dragging app icon from notification.
- * This test is only for large screen devices.
+ * Test enter split screen by dragging app icon from notification. This test is only for large
+ * screen devices.
*
* To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromNotification`
*/
@@ -51,88 +50,122 @@ import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class EnterSplitScreenByDragFromNotification(
- testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
-
- private val sendNotificationApp = SplitScreenHelper.getSendNotification(instrumentation)
-
- @Before
- fun before() {
- Assume.assumeTrue(taplInstrumentation.isTablet)
- }
-
+class EnterSplitScreenByDragFromNotification(override val flicker: FlickerTest) :
+ EnterSplitScreenByDragFromNotificationBenchmark(flicker), ICommonAssertions {
+ /** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = {
- super.transition(this)
- setup {
- eachRun {
- // Send a notification
- sendNotificationApp.launchViaIntent(wmHelper)
- val sendNotification = device.wait(
- Until.findObject(By.text("Send Notification")),
- SplitScreenHelper.TIMEOUT_MS
- )
- sendNotification?.click() ?: error("Send notification button not found")
-
- taplInstrumentation.goHome()
- primaryApp.launchViaIntent(wmHelper)
- }
- }
- transitions {
- SplitScreenHelper.dragFromNotificationToSplit(instrumentation, device, wmHelper)
- }
- teardown {
- eachRun {
- sendNotificationApp.exit(wmHelper)
- }
- }
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
}
- @Presubmit
+ @FlakyTest(bugId = 245472831)
@Test
- fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+ fun splitScreenDividerBecomesVisible() {
+ flicker.splitScreenDividerBecomesVisible()
+ }
+ // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready.
@Presubmit
@Test
- fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+ fun splitScreenDividerIsVisibleAtEnd() {
+ flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+ }
+
+ @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
@Presubmit
@Test
- fun secondaryAppLayerBecomesVisible() =
- testSpec.layerBecomesVisible(sendNotificationApp.component)
+ fun secondaryAppLayerBecomesVisible() {
+ flicker.layerBecomesVisible(sendNotificationApp)
+ }
@Presubmit
@Test
- fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- testSpec.endRotation, primaryApp.component, false /* splitLeftTop */
- )
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = false,
+ portraitPosTop = false
+ )
@Presubmit
@Test
- fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
- testSpec.endRotation, sendNotificationApp.component, true /* splitLeftTop */
- )
+ fun secondaryAppBoundsBecomesVisible() =
+ flicker.splitAppLayerBoundsBecomesVisibleByDrag(sendNotificationApp)
@Presubmit
@Test
- fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+ fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
@Presubmit
@Test
- fun secondaryAppWindowIsVisibleAtEnd() =
- testSpec.appWindowIsVisibleAtEnd(sendNotificationApp.component)
+ fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(sendNotificationApp)
+
+ /** {@inheritDoc} */
+ @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
// TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
- supportedNavigationModes =
- listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
new file mode 100644
index 000000000000..516ca97bc531
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromShortcutBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen by dragging a shortcut. This test is only for large screen devices.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromShortcut`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class EnterSplitScreenByDragFromShortcut(override val flicker: FlickerTest) :
+ EnterSplitScreenByDragFromShortcutBenchmark(flicker), ICommonAssertions {
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+ @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = false,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsBecomesVisible() =
+ flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() {
+ flicker.assertWm {
+ this.notContains(secondaryApp)
+ .then()
+ .isAppWindowInvisible(secondaryApp, isOptional = true)
+ .then()
+ .isAppWindowVisible(secondaryApp)
+ }
+ }
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 241523824)
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
index 05c6e24ee89d..4af7e248b660 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -16,24 +16,25 @@
package com.android.wm.shell.flicker.splitscreen
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
import com.android.wm.shell.flicker.appWindowBecomesVisible
import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
import com.android.wm.shell.flicker.layerBecomesVisible
import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
-import org.junit.Assume
-import org.junit.Before
+import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromTaskbarBenchmark
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,8 +42,8 @@ import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
/**
- * Test enter split screen by dragging app icon from taskbar.
- * This test is only for large screen devices.
+ * Test enter split screen by dragging app icon from taskbar. This test is only for large screen
+ * devices.
*
* To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromTaskbar`
*/
@@ -50,79 +51,121 @@ import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class EnterSplitScreenByDragFromTaskbar(
- testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
-
- @Before
- fun before() {
- Assume.assumeTrue(taplInstrumentation.isTablet)
- }
-
+class EnterSplitScreenByDragFromTaskbar(override val flicker: FlickerTest) :
+ EnterSplitScreenByDragFromTaskbarBenchmark(flicker), ICommonAssertions {
+ /** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = {
- super.transition(this)
- setup {
- eachRun {
- taplInstrumentation.goHome()
- SplitScreenHelper.createShortcutOnHotseatIfNotExist(
- taplInstrumentation, secondaryApp.appName
- )
- primaryApp.launchViaIntent(wmHelper)
- }
- }
- transitions {
- taplInstrumentation.launchedAppState.taskbar
- .getAppIcon(secondaryApp.appName)
- .dragToSplitscreen(
- secondaryApp.component.packageName,
- primaryApp.component.packageName
- )
- }
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
}
- @Presubmit
+ @FlakyTest(bugId = 245472831)
@Test
- fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+ fun splitScreenDividerBecomesVisible() {
+ flicker.splitScreenDividerBecomesVisible()
+ }
+ // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready.
@Presubmit
@Test
- fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+ fun splitScreenDividerIsVisibleAtEnd() {
+ flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+ }
+
+ @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
@Presubmit
@Test
- fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp.component)
+ fun secondaryAppLayerBecomesVisible() {
+ flicker.layerBecomesVisible(secondaryApp)
+ }
@Presubmit
@Test
- fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- testSpec.endRotation, primaryApp.component, false /* splitLeftTop */
- )
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = false,
+ portraitPosTop = false
+ )
@Presubmit
@Test
- fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
- testSpec.endRotation, secondaryApp.component, true /* splitLeftTop */
- )
+ fun secondaryAppBoundsBecomesVisible() =
+ flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp)
@Presubmit
@Test
- fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+ fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
@Presubmit
@Test
- fun secondaryAppWindowBecomesVisible() =
- testSpec.appWindowBecomesVisible(secondaryApp.component)
+ fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
- fun getParams(): List<FlickerTestParameter> {
- return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
- repetitions = SplitScreenHelper.TEST_REPETITIONS,
- supportedNavigationModes =
- listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
new file mode 100644
index 000000000000..faad9e82ffef
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 The Android Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenFromOverviewBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen from Overview.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenFromOverview`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class EnterSplitScreenFromOverview(override val flicker: FlickerTest) :
+ EnterSplitScreenFromOverviewBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+ @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsBecomesVisible() {
+ flicker.splitAppLayerBoundsBecomesVisible(
+ secondaryApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+ }
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 251269324)
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 252736515)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
index 52c2daf96a3c..195b73a14a72 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
@@ -16,44 +16,37 @@
package com.android.wm.shell.flicker.splitscreen
-import android.app.Instrumentation
import android.content.Context
-import androidx.test.platform.app.InstrumentationRegistry
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.FlickerBuilderProvider
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
import com.android.server.wm.flicker.helpers.setRotation
-import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import com.android.wm.shell.flicker.BaseBenchmarkTest
-abstract class SplitScreenBase(protected val testSpec: FlickerTestParameter) {
- protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
- protected val taplInstrumentation = LauncherInstrumentation()
+abstract class SplitScreenBase(flicker: FlickerTest) : BaseBenchmarkTest(flicker) {
protected val context: Context = instrumentation.context
- protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation)
- protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation)
+ protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+ protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
- @FlickerBuilderProvider
- fun buildFlicker(): FlickerBuilder {
- return FlickerBuilder(instrumentation).apply {
- transition(this)
+ protected open val defaultSetup: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.setEnableRotation(true)
+ setRotation(flicker.scenario.startRotation)
+ tapl.setExpectedRotation(flicker.scenario.startRotation.value)
+ tapl.workspace.switchToOverview().dismissAllTasks()
}
}
- protected open val transition: FlickerBuilder.() -> Unit
- get() = {
- setup {
- test {
- taplInstrumentation.setEnableRotation(true)
- setRotation(testSpec.startRotation)
- taplInstrumentation.setExpectedRotation(testSpec.startRotation)
- }
- }
- teardown {
- eachRun {
- primaryApp.exit(wmHelper)
- secondaryApp.exit(wmHelper)
- }
- }
+ protected open val defaultTeardown: FlickerBuilder.() -> Unit = {
+ teardown {
+ primaryApp.exit(wmHelper)
+ secondaryApp.exit(wmHelper)
}
+ }
+
+ protected open val withoutTracing: FlickerBuilder.() -> Unit = {
+ withoutLayerTracing()
+ withoutWindowManagerTracing()
+ withoutTransitionTracing()
+ withoutTransactionsTracing()
+ }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
new file mode 100644
index 000000000000..1063dfd8d737
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
@@ -0,0 +1,376 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.app.Instrumentation
+import android.graphics.Point
+import android.os.SystemClock
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.IComponentMatcher
+import android.tools.common.traces.component.IComponentNameMatcher
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import android.tools.device.traces.parsers.toFlickerComponent
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.NotificationAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import org.junit.Assert.assertNotNull
+
+internal object SplitScreenUtils {
+ private const val TIMEOUT_MS = 3_000L
+ private const val DRAG_DURATION_MS = 1_000L
+ private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
+ private const val DIVIDER_BAR = "docked_divider_handle"
+ private const val OVERVIEW_SNAPSHOT = "snapshot"
+ private const val GESTURE_STEP_MS = 16L
+ private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
+ private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
+
+ private val notificationScrollerSelector: BySelector
+ get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
+ private val notificationContentSelector: BySelector
+ get() = By.text("Flicker Test Notification")
+ private val dividerBarSelector: BySelector
+ get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
+ private val overviewSnapshotSelector: BySelector
+ get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
+
+ fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
+ SimpleAppHelper(
+ instrumentation,
+ ActivityOptions.SplitScreen.Primary.LABEL,
+ ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
+ )
+
+ fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
+ SimpleAppHelper(
+ instrumentation,
+ ActivityOptions.SplitScreen.Secondary.LABEL,
+ ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
+ )
+
+ fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
+ NonResizeableAppHelper(instrumentation)
+
+ fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
+ NotificationAppHelper(instrumentation)
+
+ fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
+
+ fun waitForSplitComplete(
+ wmHelper: WindowManagerStateHelper,
+ primaryApp: IComponentMatcher,
+ secondaryApp: IComponentMatcher,
+ ) {
+ wmHelper
+ .StateSyncBuilder()
+ .withWindowSurfaceAppeared(primaryApp)
+ .withWindowSurfaceAppeared(secondaryApp)
+ .withSplitDividerVisible()
+ .waitForAndVerify()
+ }
+
+ fun enterSplit(
+ wmHelper: WindowManagerStateHelper,
+ tapl: LauncherInstrumentation,
+ device: UiDevice,
+ primaryApp: StandardAppHelper,
+ secondaryApp: StandardAppHelper
+ ) {
+ primaryApp.launchViaIntent(wmHelper)
+ secondaryApp.launchViaIntent(wmHelper)
+ tapl.goHome()
+ wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+ splitFromOverview(tapl, device)
+ waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+
+ fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
+ // Note: The initial split position in landscape is different between tablet and phone.
+ // In landscape, tablet will let the first app split to right side, and phone will
+ // split to left side.
+ if (tapl.isTablet) {
+ // TAPL's currentTask on tablet is sometimes not what we expected if the overview
+ // contains more than 3 task views. We need to use uiautomator directly to find the
+ // second task to split.
+ tapl.workspace.switchToOverview().overviewActions.clickSplit()
+ val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
+ if (snapshots == null || snapshots.size < 1) {
+ error("Fail to find a overview snapshot to split.")
+ }
+
+ // Find the second task in the upper right corner in split select mode by sorting
+ // 'left' in descending order and 'top' in ascending order.
+ snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+ t2.getVisibleBounds().left - t1.getVisibleBounds().left
+ }
+ snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+ t1.getVisibleBounds().top - t2.getVisibleBounds().top
+ }
+ snapshots[0].click()
+ } else {
+ tapl.workspace
+ .switchToOverview()
+ .currentTask
+ .tapMenu()
+ .tapSplitMenuItem()
+ .currentTask
+ .open()
+ }
+ SystemClock.sleep(TIMEOUT_MS)
+ }
+
+ fun dragFromNotificationToSplit(
+ instrumentation: Instrumentation,
+ device: UiDevice,
+ wmHelper: WindowManagerStateHelper
+ ) {
+ val displayBounds =
+ wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+ ?: error("Display not found")
+
+ // Pull down the notifications
+ device.swipe(
+ displayBounds.centerX(),
+ 5,
+ displayBounds.centerX(),
+ displayBounds.bottom,
+ 50 /* steps */
+ )
+ SystemClock.sleep(TIMEOUT_MS)
+
+ // Find the target notification
+ val notificationScroller =
+ device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
+ ?: error("Unable to find view $notificationScrollerSelector")
+ var notificationContent = notificationScroller.findObject(notificationContentSelector)
+
+ while (notificationContent == null) {
+ device.swipe(
+ displayBounds.centerX(),
+ displayBounds.centerY(),
+ displayBounds.centerX(),
+ displayBounds.centerY() - 150,
+ 20 /* steps */
+ )
+ notificationContent = notificationScroller.findObject(notificationContentSelector)
+ }
+
+ // Drag to split
+ val dragStart = notificationContent.visibleCenter
+ val dragMiddle = Point(dragStart.x + 50, dragStart.y)
+ val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
+ val downTime = SystemClock.uptimeMillis()
+
+ touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
+ // It needs a horizontal movement to trigger the drag
+ touchMove(
+ instrumentation,
+ downTime,
+ SystemClock.uptimeMillis(),
+ DRAG_DURATION_MS,
+ dragStart,
+ dragMiddle
+ )
+ touchMove(
+ instrumentation,
+ downTime,
+ SystemClock.uptimeMillis(),
+ DRAG_DURATION_MS,
+ dragMiddle,
+ dragEnd
+ )
+ // Wait for a while to start splitting
+ SystemClock.sleep(TIMEOUT_MS)
+ touch(
+ instrumentation,
+ MotionEvent.ACTION_UP,
+ downTime,
+ SystemClock.uptimeMillis(),
+ GESTURE_STEP_MS,
+ dragEnd
+ )
+ SystemClock.sleep(TIMEOUT_MS)
+ }
+
+ fun touch(
+ instrumentation: Instrumentation,
+ action: Int,
+ downTime: Long,
+ eventTime: Long,
+ duration: Long,
+ point: Point
+ ) {
+ val motionEvent =
+ MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
+ motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
+ instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
+ motionEvent.recycle()
+ SystemClock.sleep(duration)
+ }
+
+ fun touchMove(
+ instrumentation: Instrumentation,
+ downTime: Long,
+ eventTime: Long,
+ duration: Long,
+ from: Point,
+ to: Point
+ ) {
+ val steps: Long = duration / GESTURE_STEP_MS
+ var currentTime = eventTime
+ var currentX = from.x.toFloat()
+ var currentY = from.y.toFloat()
+ val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
+ val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
+
+ for (i in 1..steps) {
+ val motionMove =
+ MotionEvent.obtain(
+ downTime,
+ currentTime,
+ MotionEvent.ACTION_MOVE,
+ currentX,
+ currentY,
+ 0
+ )
+ motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
+ instrumentation.uiAutomation.injectInputEvent(motionMove, true)
+ motionMove.recycle()
+
+ currentTime += GESTURE_STEP_MS
+ if (i == steps - 1) {
+ currentX = to.x.toFloat()
+ currentY = to.y.toFloat()
+ } else {
+ currentX += stepX
+ currentY += stepY
+ }
+ SystemClock.sleep(GESTURE_STEP_MS)
+ }
+ }
+
+ fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
+ tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
+ val allApps = tapl.workspace.switchToAllApps()
+ allApps.freeze()
+ try {
+ allApps.getAppIcon(appName).dragToHotseat(0)
+ } finally {
+ allApps.unfreeze()
+ }
+ }
+
+ fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
+ val displayBounds =
+ wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+ ?: error("Display not found")
+ val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+ dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
+
+ wmHelper
+ .StateSyncBuilder()
+ .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
+ .waitForAndVerify()
+ }
+
+ fun dragDividerToDismissSplit(
+ device: UiDevice,
+ wmHelper: WindowManagerStateHelper,
+ dragToRight: Boolean,
+ dragToBottom: Boolean
+ ) {
+ val displayBounds =
+ wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+ ?: error("Display not found")
+ val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+ dividerBar.drag(
+ Point(
+ if (dragToRight) {
+ displayBounds.width * 4 / 5
+ } else {
+ displayBounds.width * 1 / 5
+ },
+ if (dragToBottom) {
+ displayBounds.height * 4 / 5
+ } else {
+ displayBounds.height * 1 / 5
+ }
+ )
+ )
+ }
+
+ fun doubleTapDividerToSwitch(device: UiDevice) {
+ val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+ val interval =
+ (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
+ dividerBar.click()
+ SystemClock.sleep(interval.toLong())
+ dividerBar.click()
+ }
+
+ fun copyContentInSplit(
+ instrumentation: Instrumentation,
+ device: UiDevice,
+ sourceApp: IComponentNameMatcher,
+ destinationApp: IComponentNameMatcher,
+ ) {
+ // Copy text from sourceApp
+ val textView =
+ device.wait(
+ Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
+ TIMEOUT_MS
+ )
+ assertNotNull("Unable to find the TextView", textView)
+ textView.click(LONG_PRESS_TIME_MS)
+
+ val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
+ assertNotNull("Unable to find the copy button", copyBtn)
+ copyBtn.click()
+
+ // Paste text to destinationApp
+ val editText =
+ device.wait(
+ Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
+ TIMEOUT_MS
+ )
+ assertNotNull("Unable to find the EditText", editText)
+ editText.click(LONG_PRESS_TIME_MS)
+
+ val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
+ assertNotNull("Unable to find the paste button", pasteBtn)
+ pasteBtn.click()
+
+ // Verify text
+ if (!textView.text.contentEquals(editText.text)) {
+ error("Fail to copy content in split")
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
new file mode 100644
index 000000000000..03b8a75a1f32
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.layerKeepVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
+import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchAppByDoubleTapDividerBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test double tap the divider bar to switch the two apps.
+ *
+ * To run this test: `atest WMShellFlickerTests:SwitchAppByDoubleTapDivider`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SwitchAppByDoubleTapDivider(override val flicker: FlickerTest) :
+ SwitchAppByDoubleTapDividerBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ override fun cujCompleted() {
+ flicker.appWindowIsVisibleAtStart(primaryApp)
+ flicker.appWindowIsVisibleAtStart(secondaryApp)
+ flicker.splitScreenDividerIsVisibleAtStart()
+
+ flicker.appWindowIsVisibleAtEnd(primaryApp)
+ flicker.appWindowIsVisibleAtEnd(secondaryApp)
+ flicker.splitScreenDividerIsVisibleAtEnd()
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+
+ @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ secondaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
+
+ /** {@inheritDoc} */
+ @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
new file mode 100644
index 000000000000..078d95de1dd0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromAnotherAppBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switch to split pair from another app.
+ *
+ * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromAnotherApp`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SwitchBackToSplitFromAnotherApp(override val flicker: FlickerTest) :
+ SwitchBackToSplitFromAnotherAppBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+ @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ secondaryApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
new file mode 100644
index 000000000000..7c84243e00d7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromHomeBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switch to split pair from home.
+ *
+ * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromHome`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SwitchBackToSplitFromHome(override val flicker: FlickerTest) :
+ SwitchBackToSplitFromHomeBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+ @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ secondaryApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 252736515)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
new file mode 100644
index 000000000000..7c46d3e099a2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromRecentBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test switch back to split pair from recent.
+ *
+ * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromRecent`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SwitchBackToSplitFromRecent(override val flicker: FlickerTest) :
+ SwitchBackToSplitFromRecentBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+ @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ secondaryApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @FlakyTest
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
new file mode 100644
index 000000000000..674ba40f6a1f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.appWindowIsInvisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.layerBecomesInvisible
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
+import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBetweenSplitPairsBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switch between two split pairs.
+ *
+ * To run this test: `atest WMShellFlickerTests:SwitchBetweenSplitPairs`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SwitchBetweenSplitPairs(override val flicker: FlickerTest) :
+ SwitchBetweenSplitPairsBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ override fun cujCompleted() {
+ flicker.appWindowIsVisibleAtStart(thirdApp)
+ flicker.appWindowIsVisibleAtStart(fourthApp)
+ flicker.splitScreenDividerIsVisibleAtStart()
+
+ flicker.appWindowIsVisibleAtEnd(primaryApp)
+ flicker.appWindowIsVisibleAtEnd(secondaryApp)
+ flicker.appWindowIsInvisibleAtEnd(thirdApp)
+ flicker.appWindowIsInvisibleAtEnd(fourthApp)
+ flicker.splitScreenDividerIsVisibleAtEnd()
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerInvisibleAtMiddle() =
+ flicker.assertLayers {
+ this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+ .then()
+ .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+ .then()
+ .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+ }
+
+ @FlakyTest(bugId = 247095572)
+ @Test
+ fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
+
+ @FlakyTest(bugId = 247095572)
+ @Test
+ fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
+
+ @FlakyTest(bugId = 247095572)
+ @Test
+ fun thirdAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(thirdApp)
+
+ @FlakyTest(bugId = 247095572)
+ @Test
+ fun fourthAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(fourthApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false
+ )
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ secondaryApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true
+ )
+
+ @Presubmit
+ @Test
+ fun thirdAppBoundsIsVisibleAtBegin() =
+ flicker.assertLayersStart {
+ this.splitAppLayerBoundsSnapToDivider(
+ thirdApp,
+ landscapePosLeft = tapl.isTablet,
+ portraitPosTop = false,
+ flicker.scenario.startRotation
+ )
+ }
+
+ @Presubmit
+ @Test
+ fun fourthAppBoundsIsVisibleAtBegin() =
+ flicker.assertLayersStart {
+ this.splitAppLayerBoundsSnapToDivider(
+ fourthApp,
+ landscapePosLeft = !tapl.isTablet,
+ portraitPosTop = true,
+ flicker.scenario.startRotation
+ )
+ }
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun thirdAppWindowBecomesVisible() = flicker.appWindowBecomesInvisible(thirdApp)
+
+ @Presubmit
+ @Test
+ fun fourthAppWindowBecomesVisible() = flicker.appWindowBecomesInvisible(fourthApp)
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 251268711)
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 206753786)
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @FlakyTest
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
new file mode 100644
index 000000000000..676c150815ad
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.flicker.splitscreen
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.NavBar
+import android.tools.common.flicker.subject.region.RegionSubject
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.ICommonAssertions
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitscreen.benchmark.UnlockKeyguardToSplitScreenBenchmark
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test unlocking insecure keyguard to back to split screen tasks and verify the transition behavior.
+ *
+ * To run this test: `atest WMShellFlickerTests:UnlockKeyguardToSplitScreen`
+ */
+@RequiresDevice
+@Postsubmit
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class UnlockKeyguardToSplitScreen(override val flicker: FlickerTest) :
+ UnlockKeyguardToSplitScreenBenchmark(flicker), ICommonAssertions {
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Test
+ fun splitScreenDividerIsVisibleAtEnd() {
+ flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
+ }
+
+ @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
+
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp,
+ landscapePosLeft = false,
+ portraitPosTop = false
+ )
+
+ @Test
+ fun secondaryAppBoundsIsVisibleAtEnd() =
+ flicker.splitAppLayerBoundsIsVisibleAtEnd(
+ secondaryApp,
+ landscapePosLeft = true,
+ portraitPosTop = true
+ )
+
+ @Test
+ fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
+
+ @Test
+ fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
+
+ @Test
+ fun notOverlapsForPrimaryAndSecondaryAppLayers() {
+ flicker.assertLayers {
+ this.invoke("notOverlapsForPrimaryAndSecondaryLayers") {
+ val primaryAppRegions = it.subjects.filter { subject ->
+ subject.name.contains(primaryApp.toLayerName()) && subject.isVisible
+ }.mapNotNull { primaryApp -> primaryApp.layer.visibleRegion }.toTypedArray()
+
+ val primaryAppRegionArea = RegionSubject(primaryAppRegions, it.timestamp)
+ it.visibleRegion(secondaryApp).notOverlaps(primaryAppRegionArea.region)
+ }
+ }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
new file mode 100644
index 000000000000..c3c5f88eaa29
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class CopyContentInSplitBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val textEditApp = SplitScreenUtils.getIme(instrumentation)
+ protected val magnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#")
+ protected val popupWindowLayer = ComponentNameMatcher("", "PopupWindow:")
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, textEditApp) }
+ transitions {
+ SplitScreenUtils.copyContentInSplit(
+ instrumentation,
+ device,
+ primaryApp,
+ textEditApp
+ )
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ open fun cujCompleted() {
+ // The validation of copied text is already done in SplitScreenUtils.copyContentInSplit()
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
new file mode 100644
index 000000000000..37cd18fad521
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenDismissed
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class DismissSplitScreenByDividerBenchmark(flicker: FlickerTest) : SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
+ transitions {
+ if (tapl.isTablet) {
+ SplitScreenUtils.dragDividerToDismissSplit(
+ device,
+ wmHelper,
+ dragToRight = false,
+ dragToBottom = true
+ )
+ } else {
+ SplitScreenUtils.dragDividerToDismissSplit(
+ device,
+ wmHelper,
+ dragToRight = true,
+ dragToBottom = true
+ )
+ }
+ wmHelper.StateSyncBuilder().withFullScreenApp(secondaryApp).waitForAndVerify()
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = false)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
new file mode 100644
index 000000000000..0ec6dc96bcd9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenDismissed
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class DismissSplitScreenByGoHomeBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
+ transitions {
+ tapl.goHome()
+ wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = true)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
new file mode 100644
index 000000000000..190e2e765bcc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class DragDividerToResizeBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
+ transitions { SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Before
+ fun before() {
+ Assume.assumeTrue(tapl.isTablet || !flicker.scenario.isLandscapeOrSeascapeAtStart)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ open fun cujCompleted() {
+ // TODO(b/246490534): Add validation for resized app after withAppTransitionIdle is
+ // robust enough to get the correct end state.
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
new file mode 100644
index 000000000000..3a1d1a4415c3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ tapl.goHome()
+ primaryApp.launchViaIntent(wmHelper)
+ }
+ transitions {
+ tapl.launchedAppState.taskbar
+ .openAllApps()
+ .getAppIcon(secondaryApp.appName)
+ .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Before
+ fun before() {
+ Assume.assumeTrue(tapl.isTablet)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() =
+ flicker.splitScreenEntered(
+ primaryApp,
+ secondaryApp,
+ fromOtherApp = false,
+ appExistAtStart = false
+ )
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
new file mode 100644
index 000000000000..2033b7d64416
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterSplitScreenByDragFromNotificationBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val sendNotificationApp = SplitScreenUtils.getSendNotification(instrumentation)
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ // Send a notification
+ sendNotificationApp.launchViaIntent(wmHelper)
+ sendNotificationApp.postNotification(wmHelper)
+ tapl.goHome()
+ primaryApp.launchViaIntent(wmHelper)
+ }
+ transitions {
+ SplitScreenUtils.dragFromNotificationToSplit(instrumentation, device, wmHelper)
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp)
+ }
+ teardown { sendNotificationApp.exit(wmHelper) }
+ }
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() =
+ flicker.splitScreenEntered(primaryApp, sendNotificationApp, fromOtherApp = false)
+
+ @Before
+ fun before() {
+ Assume.assumeTrue(tapl.isTablet)
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
new file mode 100644
index 000000000000..b7a7110afe25
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterSplitScreenByDragFromShortcutBenchmark(flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ @Before
+ fun before() {
+ Assume.assumeTrue(tapl.isTablet)
+ }
+
+ protected val thisTransition: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.goHome()
+ SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
+ primaryApp.launchViaIntent(wmHelper)
+ }
+ transitions {
+ tapl.launchedAppState.taskbar
+ .getAppIcon(secondaryApp.appName)
+ .openDeepShortcutMenu()
+ .getMenuItem("Split Screen Secondary Activity")
+ .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() =
+ flicker.splitScreenEntered(
+ primaryApp,
+ secondaryApp,
+ fromOtherApp = false,
+ appExistAtStart = false
+ )
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
new file mode 100644
index 000000000000..b1ce62f99e6c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ tapl.goHome()
+ SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
+ primaryApp.launchViaIntent(wmHelper)
+ }
+ transitions {
+ tapl.launchedAppState.taskbar
+ .getAppIcon(secondaryApp.appName)
+ .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() =
+ flicker.splitScreenEntered(
+ primaryApp,
+ secondaryApp,
+ fromOtherApp = false,
+ appExistAtStart = false
+ )
+
+ @Before
+ fun before() {
+ Assume.assumeTrue(tapl.isTablet)
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
new file mode 100644
index 000000000000..14f07453b7d1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class EnterSplitScreenFromOverviewBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ primaryApp.launchViaIntent(wmHelper)
+ secondaryApp.launchViaIntent(wmHelper)
+ tapl.goHome()
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withHomeActivityVisible()
+ .waitForAndVerify()
+ }
+ transitions {
+ SplitScreenUtils.splitFromOverview(tapl, device)
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
new file mode 100644
index 000000000000..65fb1358a9b0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.helpers.WindowUtils
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class SwitchAppByDoubleTapDividerBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
+ transitions {
+ SplitScreenUtils.doubleTapDividerToSwitch(device)
+ wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+
+ waitForLayersToSwitch(wmHelper)
+ waitForWindowsToSwitch(wmHelper)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ private fun waitForWindowsToSwitch(wmHelper: WindowManagerStateHelper) {
+ wmHelper
+ .StateSyncBuilder()
+ .add("appWindowsSwitched") {
+ val primaryAppWindow =
+ it.wmState.visibleWindows.firstOrNull { window ->
+ primaryApp.windowMatchesAnyOf(window)
+ }
+ ?: return@add false
+ val secondaryAppWindow =
+ it.wmState.visibleWindows.firstOrNull { window ->
+ secondaryApp.windowMatchesAnyOf(window)
+ }
+ ?: return@add false
+
+ if (isLandscape(flicker.scenario.endRotation)) {
+ return@add if (flicker.scenario.isTablet) {
+ secondaryAppWindow.frame.right <= primaryAppWindow.frame.left
+ } else {
+ primaryAppWindow.frame.right <= secondaryAppWindow.frame.left
+ }
+ } else {
+ return@add if (flicker.scenario.isTablet) {
+ primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+ } else {
+ primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+ }
+ }
+ }
+ .waitForAndVerify()
+ }
+
+ private fun waitForLayersToSwitch(wmHelper: WindowManagerStateHelper) {
+ wmHelper
+ .StateSyncBuilder()
+ .add("appLayersSwitched") {
+ val primaryAppLayer =
+ it.layerState.visibleLayers.firstOrNull { window ->
+ primaryApp.layerMatchesAnyOf(window)
+ }
+ ?: return@add false
+ val secondaryAppLayer =
+ it.layerState.visibleLayers.firstOrNull { window ->
+ secondaryApp.layerMatchesAnyOf(window)
+ }
+ ?: return@add false
+
+ val primaryVisibleRegion = primaryAppLayer.visibleRegion?.bounds ?: return@add false
+ val secondaryVisibleRegion =
+ secondaryAppLayer.visibleRegion?.bounds ?: return@add false
+
+ if (isLandscape(flicker.scenario.endRotation)) {
+ return@add if (flicker.scenario.isTablet) {
+ secondaryVisibleRegion.right <= primaryVisibleRegion.left
+ } else {
+ primaryVisibleRegion.right <= secondaryVisibleRegion.left
+ }
+ } else {
+ return@add if (flicker.scenario.isTablet) {
+ primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+ } else {
+ primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+ }
+ }
+ }
+ .waitForAndVerify()
+ }
+
+ private fun isLandscape(rotation: Rotation): Boolean {
+ val displayBounds = WindowUtils.getDisplayBounds(rotation)
+ return displayBounds.width > displayBounds.height
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ open fun cujCompleted() {
+ // TODO(b/246490534): Add validation for switched app after withAppTransitionIdle is
+ // robust enough to get the correct end state.
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
new file mode 100644
index 000000000000..b333aba447a2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class SwitchBackToSplitFromAnotherAppBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ private val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation)
+
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+
+ thirdApp.launchViaIntent(wmHelper)
+ wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(thirdApp).waitForAndVerify()
+ }
+ transitions {
+ tapl.launchedAppState.quickSwitchToPreviousApp()
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
new file mode 100644
index 000000000000..a27540efdad7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class SwitchBackToSplitFromHomeBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+
+ tapl.goHome()
+ wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+ }
+ transitions {
+ tapl.workspace.quickSwitchToPreviousApp()
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
new file mode 100644
index 000000000000..18bf4ff054e0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class SwitchBackToSplitFromRecentBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+
+ tapl.goHome()
+ wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+ }
+ transitions {
+ tapl.workspace.switchToOverview().currentTask.open()
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ withoutTracing(this)
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit
+ @Test
+ fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
new file mode 100644
index 000000000000..c5fe61e26733
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.flicker.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class SwitchBetweenSplitPairsBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thirdApp = SplitScreenUtils.getIme(instrumentation)
+ protected val fourthApp = SplitScreenUtils.getSendNotification(instrumentation)
+
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+ SplitScreenUtils.enterSplit(wmHelper, tapl, device, thirdApp, fourthApp)
+ SplitScreenUtils.waitForSplitComplete(wmHelper, thirdApp, fourthApp)
+ }
+ transitions {
+ tapl.launchedAppState.quickSwitchToPreviousApp()
+ SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ teardown {
+ thirdApp.exit(wmHelper)
+ fourthApp.exit(wmHelper)
+ }
+ }
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @PlatinumTest(focusArea = "sysui")
+ @Presubmit @Test open fun cujCompleted() {}
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt
new file mode 100644
index 000000000000..5f16e5b4d65e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.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.flicker.splitscreen.benchmark
+
+import android.tools.common.NavBar
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class UnlockKeyguardToSplitScreenBenchmark(override val flicker: FlickerTest) :
+ SplitScreenBase(flicker) {
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
+ transitions {
+ device.sleep()
+ wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+ device.wakeUp()
+ device.pressMenu()
+ wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+ }
+ }
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests(
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp
deleted file mode 100644
index ea606df1536d..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/Android.bp
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_test {
- name: "WMShellFlickerTestApp",
- srcs: ["**/*.java"],
- sdk_version: "current",
- test_suites: ["device-tests"],
-}
-
-java_library {
- name: "wmshell-flicker-test-components",
- srcs: ["src/**/Components.java"],
- sdk_version: "test_current",
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
deleted file mode 100644
index bc0b0b6292b4..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
+++ /dev/null
@@ -1,147 +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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.wm.shell.flicker.testapp">
-
- <uses-sdk android:minSdkVersion="29"
- android:targetSdkVersion="29"/>
- <application android:allowBackup="false"
- android:supportsRtl="true">
- <activity android:name=".FixedActivity"
- android:resizeableActivity="true"
- android:supportsPictureInPicture="true"
- android:launchMode="singleTop"
- android:theme="@style/CutoutShortEdges"
- android:label="FixedApp"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
- <activity android:name=".PipActivity"
- android:resizeableActivity="true"
- android:supportsPictureInPicture="true"
- android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
- android:taskAffinity="com.android.wm.shell.flicker.testapp.PipActivity"
- android:theme="@style/CutoutShortEdges"
- android:launchMode="singleTop"
- android:label="PipApp"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
- </intent-filter>
- </activity>
-
- <activity android:name=".ImeActivity"
- android:taskAffinity="com.android.wm.shell.flicker.testapp.ImeActivity"
- android:theme="@style/CutoutShortEdges"
- android:label="ImeApp"
- android:launchMode="singleTop"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
- </intent-filter>
- </activity>
-
- <activity android:name=".SplitScreenActivity"
- android:resizeableActivity="true"
- android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenActivity"
- android:theme="@style/CutoutShortEdges"
- android:label="SplitScreenPrimaryApp"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
-
- <activity android:name=".SplitScreenSecondaryActivity"
- android:resizeableActivity="true"
- android:taskAffinity="com.android.wm.shell.flicker.testapp.SplitScreenSecondaryActivity"
- android:theme="@style/CutoutShortEdges"
- android:label="SplitScreenSecondaryApp"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
-
- <activity android:name=".SendNotificationActivity"
- android:taskAffinity="com.android.wm.shell.flicker.testapp.SendNotificationActivity"
- android:theme="@style/CutoutShortEdges"
- android:label="SendNotificationApp"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
-
- <activity android:name=".NonResizeableActivity"
- android:resizeableActivity="false"
- android:taskAffinity="com.android.wm.shell.flicker.testapp.NonResizeableActivity"
- android:theme="@style/CutoutShortEdges"
- android:label="NonResizeableApp"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
-
- <activity android:name=".SimpleActivity"
- android:taskAffinity="com.android.wm.shell.flicker.testapp.SimpleActivity"
- android:theme="@style/CutoutShortEdges"
- android:label="SimpleApp"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
- <activity
- android:name=".LaunchBubbleActivity"
- android:label="LaunchBubbleApp"
- android:exported="true"
- android:theme="@style/CutoutShortEdges"
- android:launchMode="singleTop">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <action android:name="android.intent.action.VIEW" />
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
- <activity
- android:name=".BubbleActivity"
- android:label="BubbleApp"
- android:exported="false"
- android:theme="@style/CutoutShortEdges"
- android:resizeableActivity="true" />
- </application>
-</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png
deleted file mode 100644
index d424a17b4157..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png
+++ /dev/null
Binary files differ
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml
deleted file mode 100644
index b43f31da748d..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
- <path
- android:fillColor="#FF000000"
- android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
- <path
- android:fillColor="#FF000000"
- android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/>
-</vector>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml
deleted file mode 100644
index 0e8c7a0fe64a..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:pathData="M12,4c-4.97,0 -9,3.58 -9,8c0,1.53 0.49,2.97 1.33,4.18c0.12,0.18 0.2,0.46 0.1,0.66c-0.33,0.68 -0.79,1.52 -1.38,2.39c-0.12,0.17 0.01,0.41 0.21,0.39c0.63,-0.05 1.86,-0.26 3.38,-0.91c0.17,-0.07 0.36,-0.06 0.52,0.03C8.55,19.54 10.21,20 12,20c4.97,0 9,-3.58 9,-8S16.97,4 12,4zM16.94,11.63l-3.29,3.29c-0.13,0.13 -0.34,0.04 -0.34,-0.14v-1.57c0,-0.11 -0.1,-0.21 -0.21,-0.2c-2.19,0.06 -3.65,0.65 -5.14,1.95c-0.15,0.13 -0.38,0 -0.33,-0.19c0.7,-2.57 2.9,-4.57 5.5,-4.75c0.1,-0.01 0.18,-0.09 0.18,-0.19V8.2c0,-0.18 0.22,-0.27 0.34,-0.14l3.29,3.29C17.02,11.43 17.02,11.55 16.94,11.63z"
- android:fillColor="#000000"
- android:fillType="evenOdd"/>
-</vector>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml
deleted file mode 100644
index f8b0ca3da26e..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <Button
- android:id="@+id/button_finish"
- android:layout_width="wrap_content"
- android:layout_height="48dp"
- android:layout_marginStart="8dp"
- android:text="Finish" />
- <Button
- android:id="@+id/button_new_task"
- android:layout_width="wrap_content"
- android:layout_height="46dp"
- android:layout_marginStart="8dp"
- android:text="New Task" />
- <Button
- android:id="@+id/button_new_bubble"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:layout_marginEnd="8dp"
- android:text="New Bubble" />
-
- <Button
- android:id="@+id/button_activity_for_result"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
- android:layout_marginStart="8dp"
- android:text="Activity For Result" />
-</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml
deleted file mode 100644
index 4708cfd48381..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_ime.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:focusableInTouchMode="true"
- android:background="@android:color/holo_green_light">
- <EditText android:id="@+id/plain_text_input"
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:inputType="text"/>
-</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml
deleted file mode 100644
index f23c46455c63..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@android:color/black">
-
- <Button
- android:id="@+id/button_create"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- android:text="Add Bubble" />
-
- <Button
- android:id="@+id/button_cancel"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/button_create"
- android:layout_centerHorizontal="true"
- android:layout_marginTop="20dp"
- android:text="Cancel Bubble" />
-
- <Button
- android:id="@+id/button_cancel_all"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/button_cancel"
- android:layout_centerHorizontal="true"
- android:layout_marginTop="20dp"
- android:text="Cancel All Bubble" />
-</RelativeLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml
deleted file mode 100644
index 45d5917f86d6..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_non_resizeable.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@android:color/holo_orange_light">
-
- <TextView
- android:id="@+id/NonResizeableTest"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:gravity="center_vertical|center_horizontal"
- android:text="NonResizeableActivity"
- android:textAppearance="?android:attr/textAppearanceLarge"/>
-
-</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml
deleted file mode 100644
index 8d59b567e59b..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@android:color/black">
-
- <Button
- android:id="@+id/button_send_notification"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- android:text="Send Notification" />
-</RelativeLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml
deleted file mode 100644
index 229098313afa..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml
+++ /dev/null
@@ -1,141 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@android:color/holo_blue_bright">
-
- <!-- All the buttons (and other clickable elements) should be arranged in a way so that it is
- possible to "cycle" over all them by clicking on the D-Pad DOWN button. The way we do it
- here is by arranging them this vertical LL and by relying on the nextFocusDown attribute
- where things are arranged differently and to circle back up to the top once we reach the
- bottom. -->
-
- <Button
- android:id="@+id/enter_pip"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Enter PIP"
- android:onClick="enterPip"/>
-
- <CheckBox
- android:id="@+id/with_custom_actions"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="With custom actions"/>
-
- <RadioGroup
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:checkedButton="@id/enter_pip_on_leave_disabled">
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Enter PiP on home press"/>
-
- <RadioButton
- android:id="@+id/enter_pip_on_leave_disabled"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Disabled"
- android:onClick="onAutoPipSelected"/>
-
- <RadioButton
- android:id="@+id/enter_pip_on_leave_manual"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Via code behind"
- android:onClick="onAutoPipSelected"/>
-
- <RadioButton
- android:id="@+id/enter_pip_on_leave_autoenter"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Auto-enter PiP"
- android:onClick="onAutoPipSelected"/>
- </RadioGroup>
-
- <RadioGroup
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:checkedButton="@id/ratio_default">
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Ratio"/>
-
- <RadioButton
- android:id="@+id/ratio_default"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Default"
- android:onClick="onRatioSelected"/>
-
- <RadioButton
- android:id="@+id/ratio_square"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Square [1:1]"
- android:onClick="onRatioSelected"/>
-
- <RadioButton
- android:id="@+id/ratio_wide"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Wide [2:1]"
- android:onClick="onRatioSelected"/>
-
- <RadioButton
- android:id="@+id/ratio_tall"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Tall [1:2]"
- android:onClick="onRatioSelected"/>
- </RadioGroup>
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Media Session"/>
-
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
-
- <Button
- android:id="@+id/media_session_start"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:nextFocusDown="@id/media_session_stop"
- android:text="Start"/>
-
- <Button
- android:id="@+id/media_session_stop"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:nextFocusDown="@id/enter_pip"
- android:text="Stop"/>
-
- </LinearLayout>
-
-</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml
deleted file mode 100644
index 5d94e5177dcc..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_simple.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@android:color/holo_orange_light">
-
-</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml
deleted file mode 100644
index 84789f5a6c02..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@android:color/holo_green_light">
-
- <TextView
- android:id="@+id/SplitScreenTest"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:gravity="center_vertical|center_horizontal"
- android:text="PrimaryActivity"
- android:textAppearance="?android:attr/textAppearanceLarge"/>
-
-</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml
deleted file mode 100644
index 674bb70ad01e..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@android:color/holo_blue_light">
-
- <TextView
- android:id="@+id/SplitScreenTest"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:gravity="center_vertical|center_horizontal"
- android:text="SecondaryActivity"
- android:textAppearance="?android:attr/textAppearanceLarge"/>
-
-</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml
deleted file mode 100644
index 23b51cc06f04..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/values/styles.xml
+++ /dev/null
@@ -1,34 +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.
- -->
-
-<resources>
- <style name="DefaultTheme" parent="@android:style/Theme.DeviceDefault">
- <item name="android:windowBackground">@android:color/darker_gray</item>
- </style>
-
- <style name="CutoutDefault" parent="@style/DefaultTheme">
- <item name="android:windowLayoutInDisplayCutoutMode">default</item>
- </style>
-
- <style name="CutoutShortEdges" parent="@style/DefaultTheme">
- <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
- </style>
-
- <style name="CutoutNever" parent="@style/DefaultTheme">
- <item name="android:windowLayoutInDisplayCutoutMode">never</item>
- </style>
-</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java
deleted file mode 100644
index bc3bc75ab903..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.widget.Toast;
-
-public class BubbleActivity extends Activity {
- private int mNotifId = 0;
-
- public BubbleActivity() {
- super();
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Intent intent = getIntent();
- if (intent != null) {
- mNotifId = intent.getIntExtra(BubbleHelper.EXTRA_BUBBLE_NOTIF_ID, -1);
- } else {
- mNotifId = -1;
- }
-
- setContentView(R.layout.activity_bubble);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- String result = resultCode == Activity.RESULT_OK ? "OK" : "CANCELLED";
- Toast.makeText(this, "Activity result: " + result, Toast.LENGTH_SHORT).show();
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
deleted file mode 100644
index 6cd93eff2803..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Point;
-import android.graphics.drawable.Icon;
-import android.os.SystemClock;
-import android.service.notification.StatusBarNotification;
-import android.view.WindowManager;
-
-import java.util.HashMap;
-
-public class BubbleHelper {
-
- static final String EXTRA_BUBBLE_NOTIF_ID = "EXTRA_BUBBLE_NOTIF_ID";
- static final String CHANNEL_ID = "bubbles";
- static final String CHANNEL_NAME = "Bubbles";
- static final int DEFAULT_HEIGHT_DP = 300;
-
- private static BubbleHelper sInstance;
-
- private final Context mContext;
- private NotificationManager mNotificationManager;
- private float mDisplayHeight;
-
- private HashMap<Integer, BubbleInfo> mBubbleMap = new HashMap<>();
-
- private int mNextNotifyId = 0;
- private int mColourIndex = 0;
-
- public static class BubbleInfo {
- public int id;
- public int height;
- public Icon icon;
-
- public BubbleInfo(int id, int height, Icon icon) {
- this.id = id;
- this.height = height;
- this.icon = icon;
- }
- }
-
- public static BubbleHelper getInstance(Context context) {
- if (sInstance == null) {
- sInstance = new BubbleHelper(context);
- }
- return sInstance;
- }
-
- private BubbleHelper(Context context) {
- mContext = context;
- mNotificationManager = context.getSystemService(NotificationManager.class);
-
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME,
- NotificationManager.IMPORTANCE_DEFAULT);
- channel.setDescription("Channel that posts bubbles");
- channel.setAllowBubbles(true);
- mNotificationManager.createNotificationChannel(channel);
-
- Point p = new Point();
- WindowManager wm = context.getSystemService(WindowManager.class);
- wm.getDefaultDisplay().getRealSize(p);
- mDisplayHeight = p.y;
-
- }
-
- private int getNextNotifyId() {
- int id = mNextNotifyId;
- mNextNotifyId++;
- return id;
- }
-
- private Icon getIcon() {
- return Icon.createWithResource(mContext, R.drawable.bg);
- }
-
- public int addNewBubble(boolean autoExpand, boolean suppressNotif) {
- int id = getNextNotifyId();
- BubbleInfo info = new BubbleInfo(id, DEFAULT_HEIGHT_DP, getIcon());
- mBubbleMap.put(info.id, info);
-
- Notification.BubbleMetadata data = getBubbleBuilder(info)
- .setSuppressNotification(suppressNotif)
- .setAutoExpandBubble(false)
- .build();
- Notification notification = getNotificationBuilder(info.id)
- .setBubbleMetadata(data).build();
-
- mNotificationManager.notify(info.id, notification);
- return info.id;
- }
-
- private Notification.Builder getNotificationBuilder(int id) {
- Person chatBot = new Person.Builder()
- .setBot(true)
- .setName("BubbleChat")
- .setImportant(true)
- .build();
- String shortcutId = "BubbleChat";
- return new Notification.Builder(mContext, CHANNEL_ID)
- .setChannelId(CHANNEL_ID)
- .setShortcutId(shortcutId)
- .setContentTitle("BubbleChat")
- .setContentIntent(PendingIntent.getActivity(mContext, 0,
- new Intent(mContext, LaunchBubbleActivity.class),
- PendingIntent.FLAG_UPDATE_CURRENT))
- .setStyle(new Notification.MessagingStyle(chatBot)
- .setConversationTitle("BubbleChat")
- .addMessage("BubbleChat",
- SystemClock.currentThreadTimeMillis() - 300000, chatBot)
- .addMessage("Is it me, " + id + ", you're looking for?",
- SystemClock.currentThreadTimeMillis(), chatBot)
- )
- .setSmallIcon(R.drawable.ic_bubble);
- }
-
- private Notification.BubbleMetadata.Builder getBubbleBuilder(BubbleInfo info) {
- Intent target = new Intent(mContext, BubbleActivity.class);
- target.putExtra(EXTRA_BUBBLE_NOTIF_ID, info.id);
- PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, info.id, target,
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- return new Notification.BubbleMetadata.Builder()
- .setIntent(bubbleIntent)
- .setIcon(info.icon)
- .setDesiredHeight(info.height);
- }
-
- public void cancel(int id) {
- mNotificationManager.cancel(id);
- }
-
- public void cancelAll() {
- mNotificationManager.cancelAll();
- }
-
- public void cancelLast() {
- StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications();
- if (activeNotifications.length > 0) {
- mNotificationManager.cancel(
- activeNotifications[activeNotifications.length - 1].getId());
- }
- }
-
- public void cancelFirst() {
- StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications();
- if (activeNotifications.length > 0) {
- mNotificationManager.cancel(activeNotifications[0].getId());
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
deleted file mode 100644
index a2b580da5898..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-import android.content.ComponentName;
-
-public class Components {
- public static final String PACKAGE_NAME = "com.android.wm.shell.flicker.testapp";
-
- public static class SimpleActivity {
- public static final String LABEL = "SimpleApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".SimpleActivity");
- }
-
- public static class FixedActivity {
- public static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation";
- public static final String LABEL = "FixedApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".FixedActivity");
- }
-
- public static class NonResizeableActivity {
- public static final String LABEL = "NonResizeableApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".NonResizeableActivity");
- }
-
- public static class PipActivity {
- // Test App > Pip Activity
- public static final String LABEL = "PipApp";
- public static final String MENU_ACTION_NO_OP = "No-Op";
- public static final String MENU_ACTION_ON = "On";
- public static final String MENU_ACTION_OFF = "Off";
- public static final String MENU_ACTION_CLEAR = "Clear";
-
- // Intent action that this activity dynamically registers to enter picture-in-picture
- public static final String ACTION_ENTER_PIP = PACKAGE_NAME + ".PipActivity.ENTER_PIP";
- // Intent action that this activity dynamically registers to set requested orientation.
- // Will apply the oriention to the value set in the EXTRA_FIXED_ORIENTATION extra.
- public static final String ACTION_SET_REQUESTED_ORIENTATION =
- PACKAGE_NAME + ".PipActivity.SET_REQUESTED_ORIENTATION";
-
- // Calls enterPictureInPicture() on creation
- public static final String EXTRA_ENTER_PIP = "enter_pip";
- // Sets the fixed orientation (can be one of {@link ActivityInfo.ScreenOrientation}
- public static final String EXTRA_PIP_ORIENTATION = "fixed_orientation";
- // Adds a click listener to finish this activity when it is clicked
- public static final String EXTRA_TAP_TO_FINISH = "tap_to_finish";
-
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".PipActivity");
- }
-
- public static class ImeActivity {
- public static final String LABEL = "ImeApp";
- public static final String ACTION_CLOSE_IME =
- PACKAGE_NAME + ".action.CLOSE_IME";
- public static final String ACTION_OPEN_IME =
- PACKAGE_NAME + ".action.OPEN_IME";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".ImeActivity");
- }
-
- public static class SplitScreenActivity {
- public static final String LABEL = "SplitScreenPrimaryApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".SplitScreenActivity");
- }
-
- public static class SplitScreenSecondaryActivity {
- public static final String LABEL = "SplitScreenSecondaryApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".SplitScreenSecondaryActivity");
- }
-
- public static class SendNotificationActivity {
- public static final String LABEL = "SendNotificationApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".SendNotificationActivity");
- }
-
- public static class LaunchBubbleActivity {
- public static final String LABEL = "LaunchBubbleApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".LaunchBubbleActivity");
- }
-
- public static class BubbleActivity {
- public static final String LABEL = "BubbleApp";
- public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
- PACKAGE_NAME + ".BubbleActivity");
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java
deleted file mode 100644
index d4ae6c1313bf..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/FixedActivity.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-import static com.android.wm.shell.flicker.testapp.Components.FixedActivity.EXTRA_FIXED_ORIENTATION;
-
-import android.os.Bundle;
-
-public class FixedActivity extends SimpleActivity {
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- // Set the fixed orientation if requested
- if (getIntent().hasExtra(EXTRA_FIXED_ORIENTATION)) {
- final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_FIXED_ORIENTATION));
- setRequestedOrientation(ori);
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java
deleted file mode 100644
index 59c64a1345ab..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/ImeActivity.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.inputmethod.InputMethodManager;
-
-public class ImeActivity extends Activity {
- private static final String ACTION_OPEN_IME =
- "com.android.wm.shell.flicker.testapp.action.OPEN_IME";
- private static final String ACTION_CLOSE_IME =
- "com.android.wm.shell.flicker.testapp.action.CLOSE_IME";
-
- private InputMethodManager mImm;
- private View mEditText;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- WindowManager.LayoutParams p = getWindow().getAttributes();
- p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
- .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
- getWindow().setAttributes(p);
- setContentView(R.layout.activity_ime);
-
- mEditText = findViewById(R.id.plain_text_input);
- mImm = getSystemService(InputMethodManager.class);
-
- handleIntent(getIntent());
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- handleIntent(intent);
- }
-
- private void handleIntent(Intent intent) {
- final String action = intent.getAction();
- if (ACTION_OPEN_IME.equals(action)) {
- mEditText.requestFocus();
- mImm.showSoftInput(mEditText, InputMethodManager.SHOW_FORCED);
- } else if (ACTION_CLOSE_IME.equals(action)) {
- mImm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
- mEditText.clearFocus();
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java
deleted file mode 100644
index 71fa66d8a61c..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-
-import android.app.Activity;
-import android.app.Person;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
-import android.graphics.drawable.Icon;
-import android.os.Bundle;
-import android.view.View;
-
-import java.util.Arrays;
-
-public class LaunchBubbleActivity extends Activity {
-
- private BubbleHelper mBubbleHelper;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- addInboxShortcut(getApplicationContext());
- mBubbleHelper = BubbleHelper.getInstance(this);
- setContentView(R.layout.activity_main);
- findViewById(R.id.button_create).setOnClickListener(this::add);
- findViewById(R.id.button_cancel).setOnClickListener(this::cancel);
- findViewById(R.id.button_cancel_all).setOnClickListener(this::cancelAll);
- }
-
- private void add(View v) {
- mBubbleHelper.addNewBubble(false /* autoExpand */, false /* suppressNotif */);
- }
-
- private void cancel(View v) {
- mBubbleHelper.cancelLast();
- }
-
- private void cancelAll(View v) {
- mBubbleHelper.cancelAll();
- }
-
- private void addInboxShortcut(Context context) {
- Icon icon = Icon.createWithResource(this, R.drawable.bg);
- Person[] persons = new Person[4];
- for (int i = 0; i < persons.length; i++) {
- persons[i] = new Person.Builder()
- .setBot(false)
- .setIcon(icon)
- .setName("google" + i)
- .setImportant(true)
- .build();
- }
-
- ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "BubbleChat")
- .setShortLabel("BubbleChat")
- .setLongLived(true)
- .setIntent(new Intent(Intent.ACTION_VIEW))
- .setIcon(Icon.createWithResource(context, R.drawable.ic_message))
- .setPersons(persons)
- .build();
- ShortcutManager scmanager = context.getSystemService(ShortcutManager.class);
- scmanager.addDynamicShortcuts(Arrays.asList(shortcut));
- }
-
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java
deleted file mode 100644
index 615b1730579c..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/PipActivity.java
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-import static android.media.MediaMetadata.METADATA_KEY_TITLE;
-import static android.media.session.PlaybackState.ACTION_PAUSE;
-import static android.media.session.PlaybackState.ACTION_PLAY;
-import static android.media.session.PlaybackState.ACTION_STOP;
-import static android.media.session.PlaybackState.STATE_PAUSED;
-import static android.media.session.PlaybackState.STATE_PLAYING;
-import static android.media.session.PlaybackState.STATE_STOPPED;
-
-import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP;
-import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION;
-import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP;
-import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.app.PictureInPictureParams;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Icon;
-import android.media.MediaMetadata;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.util.Log;
-import android.util.Rational;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.CheckBox;
-import android.widget.RadioButton;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-public class PipActivity extends Activity {
- private static final String TAG = PipActivity.class.getSimpleName();
- /**
- * A media session title for when the session is in {@link STATE_PLAYING}.
- * TvPipNotificationTests check whether the actual notification title matches this string.
- */
- private static final String TITLE_STATE_PLAYING = "TestApp media is playing";
- /**
- * A media session title for when the session is in {@link STATE_PAUSED}.
- * TvPipNotificationTests check whether the actual notification title matches this string.
- */
- private static final String TITLE_STATE_PAUSED = "TestApp media is paused";
-
- private static final Rational RATIO_DEFAULT = null;
- private static final Rational RATIO_SQUARE = new Rational(1, 1);
- private static final Rational RATIO_WIDE = new Rational(2, 1);
- private static final Rational RATIO_TALL = new Rational(1, 2);
-
- private static final String PIP_ACTION_NO_OP = "No-Op";
- private static final String PIP_ACTION_OFF = "Off";
- private static final String PIP_ACTION_ON = "On";
- private static final String PIP_ACTION_CLEAR = "Clear";
- private static final String ACTION_NO_OP = "com.android.wm.shell.flicker.testapp.NO_OP";
- private static final String ACTION_SWITCH_OFF =
- "com.android.wm.shell.flicker.testapp.SWITCH_OFF";
- private static final String ACTION_SWITCH_ON = "com.android.wm.shell.flicker.testapp.SWITCH_ON";
- private static final String ACTION_CLEAR = "com.android.wm.shell.flicker.testapp.CLEAR";
-
- private final PictureInPictureParams.Builder mPipParamsBuilder =
- new PictureInPictureParams.Builder()
- .setAspectRatio(RATIO_DEFAULT);
- private MediaSession mMediaSession;
- private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder()
- .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP)
- .setState(STATE_STOPPED, 0, 1f);
- private PlaybackState mPlaybackState = mPlaybackStateBuilder.build();
- private final MediaMetadata.Builder mMediaMetadataBuilder = new MediaMetadata.Builder();
-
- private final List<RemoteAction> mSwitchOffActions = new ArrayList<>();
- private final List<RemoteAction> mSwitchOnActions = new ArrayList<>();
- private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (isInPictureInPictureMode()) {
- switch (intent.getAction()) {
- case ACTION_SWITCH_ON:
- mPipParamsBuilder.setActions(mSwitchOnActions);
- break;
- case ACTION_SWITCH_OFF:
- mPipParamsBuilder.setActions(mSwitchOffActions);
- break;
- case ACTION_CLEAR:
- mPipParamsBuilder.setActions(Collections.emptyList());
- break;
- case ACTION_NO_OP:
- return;
- default:
- Log.w(TAG, "Unhandled action=" + intent.getAction());
- return;
- }
- setPictureInPictureParams(mPipParamsBuilder.build());
- } else {
- switch (intent.getAction()) {
- case ACTION_ENTER_PIP:
- enterPip(null);
- break;
- case ACTION_SET_REQUESTED_ORIENTATION:
- setRequestedOrientation(Integer.parseInt(intent.getStringExtra(
- EXTRA_PIP_ORIENTATION)));
- break;
- default:
- Log.w(TAG, "Unhandled action=" + intent.getAction());
- return;
- }
- }
- }
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final Window window = getWindow();
- final WindowManager.LayoutParams layoutParams = window.getAttributes();
- layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams
- .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
- window.setAttributes(layoutParams);
-
- setContentView(R.layout.activity_pip);
-
- findViewById(R.id.media_session_start)
- .setOnClickListener(v -> updateMediaSessionState(STATE_PLAYING));
- findViewById(R.id.media_session_stop)
- .setOnClickListener(v -> updateMediaSessionState(STATE_STOPPED));
-
- mMediaSession = new MediaSession(this, "WMShell_TestApp");
- mMediaSession.setPlaybackState(mPlaybackStateBuilder.build());
- mMediaSession.setCallback(new MediaSession.Callback() {
- @Override
- public void onPlay() {
- updateMediaSessionState(STATE_PLAYING);
- }
-
- @Override
- public void onPause() {
- updateMediaSessionState(STATE_PAUSED);
- }
-
- @Override
- public void onStop() {
- updateMediaSessionState(STATE_STOPPED);
- }
- });
-
- // Build two sets of the custom actions. We'll replace one with the other when 'On'/'Off'
- // action is invoked.
- // The first set consists of 3 actions: 1) Off; 2) No-Op; 3) Clear.
- // The second set consists of 2 actions: 1) On; 2) Clear.
- // Upon invocation 'Clear' action clear-off all the custom actions, including itself.
- final Icon icon = Icon.createWithResource(this, android.R.drawable.ic_menu_help);
- final RemoteAction noOpAction = buildRemoteAction(icon, PIP_ACTION_NO_OP, ACTION_NO_OP);
- final RemoteAction switchOnAction =
- buildRemoteAction(icon, PIP_ACTION_ON, ACTION_SWITCH_ON);
- final RemoteAction switchOffAction =
- buildRemoteAction(icon, PIP_ACTION_OFF, ACTION_SWITCH_OFF);
- final RemoteAction clearAllAction = buildRemoteAction(icon, PIP_ACTION_CLEAR, ACTION_CLEAR);
- mSwitchOffActions.addAll(Arrays.asList(switchOnAction, clearAllAction));
- mSwitchOnActions.addAll(Arrays.asList(noOpAction, switchOffAction, clearAllAction));
-
- final IntentFilter filter = new IntentFilter();
- filter.addAction(ACTION_NO_OP);
- filter.addAction(ACTION_SWITCH_ON);
- filter.addAction(ACTION_SWITCH_OFF);
- filter.addAction(ACTION_CLEAR);
- filter.addAction(ACTION_SET_REQUESTED_ORIENTATION);
- filter.addAction(ACTION_ENTER_PIP);
- registerReceiver(mBroadcastReceiver, filter);
-
- handleIntentExtra(getIntent());
- }
-
- @Override
- protected void onDestroy() {
- unregisterReceiver(mBroadcastReceiver);
- super.onDestroy();
- }
-
- @Override
- protected void onUserLeaveHint() {
- // Only used when auto PiP is disabled. This is to simulate the behavior that an app
- // supports regular PiP but not auto PiP.
- final boolean manuallyEnterPip =
- ((RadioButton) findViewById(R.id.enter_pip_on_leave_manual)).isChecked();
- if (manuallyEnterPip) {
- enterPictureInPictureMode();
- }
- }
-
- private RemoteAction buildRemoteAction(Icon icon, String label, String action) {
- final Intent intent = new Intent(action);
- final PendingIntent pendingIntent =
- PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- return new RemoteAction(icon, label, label, pendingIntent);
- }
-
- public void enterPip(View v) {
- final boolean withCustomActions =
- ((CheckBox) findViewById(R.id.with_custom_actions)).isChecked();
- mPipParamsBuilder.setActions(
- withCustomActions ? mSwitchOnActions : Collections.emptyList());
- enterPictureInPictureMode(mPipParamsBuilder.build());
- }
-
- public void onAutoPipSelected(View v) {
- switch (v.getId()) {
- case R.id.enter_pip_on_leave_manual:
- // disable auto enter PiP
- case R.id.enter_pip_on_leave_disabled:
- mPipParamsBuilder.setAutoEnterEnabled(false);
- setPictureInPictureParams(mPipParamsBuilder.build());
- break;
- case R.id.enter_pip_on_leave_autoenter:
- mPipParamsBuilder.setAutoEnterEnabled(true);
- setPictureInPictureParams(mPipParamsBuilder.build());
- break;
- }
- }
-
- public void onRatioSelected(View v) {
- switch (v.getId()) {
- case R.id.ratio_default:
- mPipParamsBuilder.setAspectRatio(RATIO_DEFAULT);
- break;
-
- case R.id.ratio_square:
- mPipParamsBuilder.setAspectRatio(RATIO_SQUARE);
- break;
-
- case R.id.ratio_wide:
- mPipParamsBuilder.setAspectRatio(RATIO_WIDE);
- break;
-
- case R.id.ratio_tall:
- mPipParamsBuilder.setAspectRatio(RATIO_TALL);
- break;
- }
- }
-
- private void updateMediaSessionState(int newState) {
- if (mPlaybackState.getState() == newState) {
- return;
- }
- final String title;
- switch (newState) {
- case STATE_PLAYING:
- title = TITLE_STATE_PLAYING;
- break;
- case STATE_PAUSED:
- title = TITLE_STATE_PAUSED;
- break;
- case STATE_STOPPED:
- title = "";
- break;
-
- default:
- throw new IllegalArgumentException("Unknown state " + newState);
- }
-
- mPlaybackStateBuilder.setState(newState, 0, 1f);
- mPlaybackState = mPlaybackStateBuilder.build();
-
- mMediaMetadataBuilder.putText(METADATA_KEY_TITLE, title);
-
- mMediaSession.setPlaybackState(mPlaybackState);
- mMediaSession.setMetadata(mMediaMetadataBuilder.build());
- mMediaSession.setActive(newState != STATE_STOPPED);
- }
-
- private void handleIntentExtra(Intent intent) {
- // Set the fixed orientation if requested
- if (intent.hasExtra(EXTRA_PIP_ORIENTATION)) {
- final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION));
- setRequestedOrientation(ori);
- }
- // Enter picture in picture with the given aspect ratio if provided
- if (intent.hasExtra(EXTRA_ENTER_PIP)) {
- mPipParamsBuilder.setActions(mSwitchOnActions);
- enterPip(null);
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java
deleted file mode 100644
index 8020ef2270a0..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-import android.app.Activity;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-
-public class SendNotificationActivity extends Activity {
- private NotificationManager mNotificationManager;
- private String mChannelId = "Channel id";
- private String mChannelName = "Channel name";
- private NotificationChannel mChannel;
- private int mNotifyId = 0;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_notification);
- findViewById(R.id.button_send_notification).setOnClickListener(this::sendNotification);
-
- mChannel = new NotificationChannel(mChannelId, mChannelName,
- NotificationManager.IMPORTANCE_DEFAULT);
- mNotificationManager = getSystemService(NotificationManager.class);
- mNotificationManager.createNotificationChannel(mChannel);
- }
-
- private void sendNotification(View v) {
- PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
- new Intent(this, SendNotificationActivity.class),
- PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
- Notification notification = new Notification.Builder(this, mChannelId)
- .setContentTitle("Notification App")
- .setContentText("Notification content")
- .setWhen(System.currentTimeMillis())
- .setSmallIcon(R.drawable.ic_message)
- .setContentIntent(pendingIntent)
- .build();
-
- mNotificationManager.notify(mNotifyId, notification);
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java
deleted file mode 100644
index 5343c1893d4e..000000000000
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SimpleActivity.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.testapp;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.WindowManager;
-
-public class SimpleActivity extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- WindowManager.LayoutParams p = getWindow().getAttributes();
- p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
- .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
- getWindow().setAttributes(p);
- setContentView(R.layout.activity_simple);
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 64bb481d4370..38e9f390835c 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -47,7 +47,7 @@ android_test {
"truth-prebuilt",
"testables",
"platform-test-annotations",
- "frameworks-base-testutils",
+ "servicestests-utils",
],
libs: [
@@ -69,6 +69,8 @@ android_test {
enabled: false,
},
+ test_suites: ["device-tests"],
+
platform_apis: true,
certificate: "platform",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
index b5ee037892ba..51a20ee9d090 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
@@ -17,8 +17,10 @@
package com.android.wm.shell;
import static android.view.Display.DEFAULT_DISPLAY;
+import static org.junit.Assume.assumeTrue;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.hardware.display.DisplayManager;
import android.testing.TestableContext;
@@ -36,6 +38,7 @@ import org.mockito.MockitoAnnotations;
public abstract class ShellTestCase {
protected TestableContext mContext;
+ private PackageManager mPm;
@Before
public void shellSetup() {
@@ -46,6 +49,7 @@ public abstract class ShellTestCase {
final Context context =
InstrumentationRegistry.getInstrumentation().getTargetContext();
final DisplayManager dm = context.getSystemService(DisplayManager.class);
+ mPm = context.getPackageManager();
mContext = new TestableContext(
context.createDisplayContext(dm.getDisplay(DEFAULT_DISPLAY)));
@@ -66,4 +70,20 @@ public abstract class ShellTestCase {
protected Context getContext() {
return mContext;
}
+
+ /**
+ * Makes an assumption that the test device is a TV device, used to guard tests that should
+ * only be run on TVs.
+ */
+ protected void assumeTelevision() {
+ assumeTrue(isTelevision());
+ }
+
+ /**
+ * Returns whether this test device is a TV device.
+ */
+ protected boolean isTelevision() {
+ return mPm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+ || mPm.hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY);
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
index fe8b305093d7..499870220190 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
@@ -48,10 +48,8 @@ public class TestShellExecutor implements ShellExecutor {
}
public void flushAll() {
- final ArrayList<Runnable> tmpRunnable = new ArrayList<>(mRunnables);
- mRunnables.clear();
- for (Runnable r : tmpRunnable) {
- r.run();
+ while (!mRunnables.isEmpty()) {
+ mRunnables.remove(0).run();
}
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
new file mode 100644
index 000000000000..a658375ca38a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TransitionInfoBuilder.java
@@ -0,0 +1,87 @@
+/*
+ * 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import static org.mockito.Mockito.mock;
+
+import android.app.ActivityManager;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+
+/**
+ * Utility for creating/editing synthetic TransitionInfos for tests.
+ */
+public class TransitionInfoBuilder {
+ final TransitionInfo mInfo;
+ static final int DISPLAY_ID = 0;
+
+ public TransitionInfoBuilder(@WindowManager.TransitionType int type) {
+ this(type, 0 /* flags */);
+ }
+
+ public TransitionInfoBuilder(@WindowManager.TransitionType int type,
+ @WindowManager.TransitionFlags int flags) {
+ this(type, flags, false /* asNoOp */);
+ }
+
+ public TransitionInfoBuilder(@WindowManager.TransitionType int type,
+ @WindowManager.TransitionFlags int flags, boolean asNoOp) {
+ mInfo = new TransitionInfo(type, flags);
+ if (!asNoOp) {
+ mInfo.addRootLeash(DISPLAY_ID, createMockSurface(true /* valid */), 0, 0);
+ }
+ }
+
+ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
+ @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) {
+ final TransitionInfo.Change change = new TransitionInfo.Change(
+ taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */));
+ change.setMode(mode);
+ change.setFlags(flags);
+ change.setTaskInfo(taskInfo);
+ return addChange(change);
+ }
+
+ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
+ ActivityManager.RunningTaskInfo taskInfo) {
+ return addChange(mode, TransitionInfo.FLAG_NONE, taskInfo);
+ }
+
+ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) {
+ return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */);
+ }
+
+ public TransitionInfoBuilder addChange(TransitionInfo.Change change) {
+ change.setDisplayId(DISPLAY_ID, DISPLAY_ID);
+ mInfo.addChange(change);
+ return this;
+ }
+
+ public TransitionInfo build() {
+ return mInfo;
+ }
+
+ private static SurfaceControl createMockSurface(boolean valid) {
+ SurfaceControl sc = mock(SurfaceControl.class);
+ doReturn(valid).when(sc).isValid();
+ doReturn("TestSurface").when(sc).toString();
+ return sc;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index 79070b1469be..4fca8b46a069 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -35,6 +35,8 @@ import android.window.TransitionInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.TransitionInfoBuilder;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -60,16 +62,17 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
@Test
public void testStartAnimation() {
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createChange();
- embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
- info.addChange(embeddingChange);
- doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
+ .build();
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(),
+ any());
mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
- verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
+ verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction),
+ eq(mFinishTransaction),
finishCallback.capture(), any());
verify(mStartTransaction).apply();
verify(mAnimator).start();
@@ -84,10 +87,9 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
@Test
public void testChangesBehindStartingWindow() {
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createChange();
- embeddingChange.setFlags(FLAG_IS_BEHIND_STARTING_WINDOW);
- info.addChange(embeddingChange);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createChange(FLAG_IS_BEHIND_STARTING_WINDOW))
+ .build();
final Animator animator = mAnimRunner.createAnimator(
info, mStartTransaction, mFinishTransaction,
() -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
index 54a12ab999c5..ab1ccd4599a2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -47,6 +47,7 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
@Mock
ShellInit mShellInit;
+
@Mock
Transitions mTransitions;
@Mock
@@ -82,9 +83,11 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
}
/** Creates a mock {@link TransitionInfo.Change}. */
- static TransitionInfo.Change createChange() {
- return new TransitionInfo.Change(mock(WindowContainerToken.class),
+ static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags) {
+ TransitionInfo.Change c = new TransitionInfo.Change(mock(WindowContainerToken.class),
mock(SurfaceControl.class));
+ c.setFlags(flags);
+ return c;
}
/**
@@ -93,8 +96,7 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
*/
static TransitionInfo.Change createEmbeddedChange(@NonNull Rect startBounds,
@NonNull Rect endBounds, @NonNull Rect taskBounds) {
- final TransitionInfo.Change change = createChange();
- change.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
+ final TransitionInfo.Change change = createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
change.setStartAbsBounds(startBounds);
change.setEndAbsBounds(endBounds);
if (taskBounds.width() == startBounds.width()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index 4d98b6ba4f7a..ba34f1f74cd3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.activityembedding;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
@@ -28,12 +29,18 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import android.animation.Animator;
+import android.animation.ValueAnimator;
import android.graphics.Rect;
+import android.view.SurfaceControl;
import android.window.TransitionInfo;
+import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.TransitionInfoBuilder;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -55,7 +62,8 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
@Before
public void setup() {
super.setUp();
- doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(),
+ any());
}
@Test
@@ -80,12 +88,14 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
@Test
public void testStartAnimation_containsNonActivityEmbeddingChange() {
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
- EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
- final TransitionInfo.Change nonEmbeddingChange = createChange();
- info.addChange(embeddingChange);
- info.addChange(nonEmbeddingChange);
+ final TransitionInfo.Change nonEmbeddedOpen = createChange(0 /* flags */);
+ final TransitionInfo.Change embeddedOpen = createEmbeddedChange(
+ EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
+ nonEmbeddedOpen.setMode(TRANSIT_OPEN);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(embeddedOpen)
+ .addChange(nonEmbeddedOpen)
+ .build();
// No-op because it contains non-embedded change.
assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
@@ -94,14 +104,29 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verifyNoMoreInteractions(mStartTransaction);
verifyNoMoreInteractions(mFinishTransaction);
verifyNoMoreInteractions(mFinishCallback);
+
+ final TransitionInfo.Change nonEmbeddedClose = createChange(0 /* flags */);
+ nonEmbeddedClose.setMode(TRANSIT_CLOSE);
+ nonEmbeddedClose.setEndAbsBounds(TASK_BOUNDS);
+ final TransitionInfo.Change embeddedOpen2 = createEmbeddedChange(
+ EMBEDDED_RIGHT_BOUNDS, EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS);
+ final TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(embeddedOpen)
+ .addChange(embeddedOpen2)
+ .addChange(nonEmbeddedClose)
+ .build();
+ // Ok to animate because nonEmbeddedClose is occluded by embeddedOpen and embeddedOpen2.
+ assertTrue(mController.startAnimation(mTransition, info2, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ // The non-embedded change is dropped to avoid affecting embedded animation.
+ assertFalse(info2.getChanges().contains(nonEmbeddedClose));
}
@Test
public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() {
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS, TASK_BOUNDS,
- TASK_BOUNDS);
- info.addChange(embeddingChange);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createEmbeddedChange(TASK_BOUNDS, TASK_BOUNDS, TASK_BOUNDS))
+ .build();
// No-op because it only contains embedded change that fills the Task. We will let the
// default handler to animate such transition.
@@ -116,10 +141,10 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
@Test
public void testStartAnimation_containsActivityEmbeddingSplitChange() {
// Change that occupies only part of the Task.
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
- EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
- info.addChange(embeddingChange);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createEmbeddedChange(
+ EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS))
+ .build();
// ActivityEmbeddingController will handle such transition.
assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
@@ -133,10 +158,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
@Test
public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() {
// Change that is entering ActivityEmbedding split.
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS,
- EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
- info.addChange(embeddingChange);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createEmbeddedChange(TASK_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS))
+ .build();
// ActivityEmbeddingController will handle such transition.
assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
@@ -150,10 +174,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
@Test
public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() {
// Change that is exiting ActivityEmbedding split.
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS,
- TASK_BOUNDS, TASK_BOUNDS);
- info.addChange(embeddingChange);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS))
+ .build();
// ActivityEmbeddingController will handle such transition.
assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
@@ -164,16 +187,54 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verifyNoMoreInteractions(mFinishTransaction);
}
+ @UiThreadTest
+ @Test
+ public void testMergeAnimation() {
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createEmbeddedChange(
+ EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS))
+ .build();
+
+ final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mController.onAnimationFinished(mTransition);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ doReturn(animator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
+ mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback);
+ verify(mFinishCallback, never()).onTransitionFinished(any(), any());
+ mController.mergeAnimation(mTransition, info, new SurfaceControl.Transaction(),
+ mTransition,
+ (wct, cb) -> {
+ });
+ verify(mFinishCallback).onTransitionFinished(any(), any());
+ }
+
@Test
public void testOnAnimationFinished() {
// Should not call finish when there is no transition.
assertThrows(IllegalStateException.class,
() -> mController.onAnimationFinished(mTransition));
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
- EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
- info.addChange(embeddingChange);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createEmbeddedChange(
+ EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS))
+ .build();
mController.startAnimation(mTransition, info, mStartTransaction,
mFinishTransaction, mFinishCallback);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 2754496a6f3f..3d8bd3854a45 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -18,15 +18,13 @@ package com.android.wm.shell.back;
import static android.window.BackNavigationInfo.KEY_TRIGGER_BACK;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
@@ -39,9 +37,8 @@ import android.app.WindowConfiguration;
import android.content.pm.ApplicationInfo;
import android.graphics.Point;
import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
+import android.os.Bundle;
import android.os.Handler;
-import android.os.IBinder;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.provider.Settings;
@@ -49,15 +46,17 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableContentResolver;
import android.testing.TestableContext;
import android.testing.TestableLooper;
+import android.view.IRemoteAnimationRunner;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
-import android.window.IBackNaviAnimationController;
+import android.window.IBackAnimationFinishedCallback;
import android.window.IOnBackInvokedCallback;
+import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -69,7 +68,6 @@ import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.sysui.ShellSharedConstants;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -94,23 +92,27 @@ public class BackAnimationControllerTest extends ShellTestCase {
new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
@Mock
- private SurfaceControl.Transaction mTransaction;
+ private IActivityTaskManager mActivityTaskManager;
@Mock
- private IActivityTaskManager mActivityTaskManager;
+ private IOnBackInvokedCallback mAppCallback;
+
+ @Mock
+ private IOnBackInvokedCallback mAnimatorCallback;
@Mock
- private IOnBackInvokedCallback mIOnBackInvokedCallback;
+ private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
@Mock
- private IBackNaviAnimationController mIBackNaviAnimationController;
+ private IRemoteAnimationRunner mBackAnimationRunner;
@Mock
private ShellController mShellController;
- private BackAnimationController mController;
+ @Mock
+ private BackAnimationBackground mAnimationBackground;
- private int mEventTime = 0;
+ private BackAnimationController mController;
private TestableContentResolver mContentResolver;
private TestableLooper mTestableLooper;
@@ -125,29 +127,23 @@ public class BackAnimationControllerTest extends ShellTestCase {
mTestableLooper = TestableLooper.get(this);
mShellInit = spy(new ShellInit(mShellExecutor));
mController = new BackAnimationController(mShellInit, mShellController,
- mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
+ mShellExecutor, new Handler(mTestableLooper.getLooper()),
mActivityTaskManager, mContext,
- mContentResolver);
+ mContentResolver, mAnimationBackground);
mController.setEnableUAnimation(true);
mShellInit.init();
- mEventTime = 0;
mShellExecutor.flushAll();
}
- private void createNavigationInfo(RemoteAnimationTarget topAnimationTarget,
- SurfaceControl screenshotSurface,
- HardwareBuffer hardwareBuffer,
- int backType,
- IOnBackInvokedCallback onBackInvokedCallback, boolean prepareAnimation) {
+ private void createNavigationInfo(int backType,
+ boolean enableAnimation,
+ boolean isAnimationCallback) {
BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
.setType(backType)
- .setDepartingAnimationTarget(topAnimationTarget)
- .setScreenshotSurface(screenshotSurface)
- .setScreenshotBuffer(hardwareBuffer)
- .setTaskWindowConfiguration(new WindowConfiguration())
.setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
- .setOnBackInvokedCallback(onBackInvokedCallback)
- .setPrepareAnimation(prepareAnimation);
+ .setOnBackInvokedCallback(mAppCallback)
+ .setPrepareRemoteAnimation(enableAnimation)
+ .setAnimationCallback(isAnimationCallback);
createNavigationInfo(builder);
}
@@ -155,7 +151,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
private void createNavigationInfo(BackNavigationInfo.Builder builder) {
try {
doReturn(builder.build()).when(mActivityTaskManager)
- .startBackNavigation(anyBoolean(), any(), any());
+ .startBackNavigation(any(), any());
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
@@ -173,6 +169,9 @@ public class BackAnimationControllerTest extends ShellTestCase {
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 0);
mController.setTriggerBack(true);
+ }
+
+ private void releaseBackGesture() {
doMotionEvent(MotionEvent.ACTION_UP, 0);
}
@@ -188,75 +187,87 @@ public class BackAnimationControllerTest extends ShellTestCase {
}
@Test
- @Ignore("b/207481538")
- public void crossActivity_screenshotAttachedAndVisible() {
- SurfaceControl screenshotSurface = new SurfaceControl();
- HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
- createNavigationInfo(createAnimationTarget(), screenshotSurface, hardwareBuffer,
- BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true);
- doMotionEvent(MotionEvent.ACTION_DOWN, 0);
- verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer);
- verify(mTransaction).setVisibility(screenshotSurface, true);
- verify(mTransaction).apply();
+ public void verifyNavigationFinishes() throws RemoteException {
+ final int[] testTypes = new int[] {BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ BackNavigationInfo.TYPE_CROSS_TASK,
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ BackNavigationInfo.TYPE_DIALOG_CLOSE,
+ BackNavigationInfo.TYPE_CALLBACK };
+
+ for (int type: testTypes) {
+ registerAnimation(type);
+ }
+
+ for (int type: testTypes) {
+ final ResultListener result = new ResultListener();
+ createNavigationInfo(new BackNavigationInfo.Builder()
+ .setType(type)
+ .setOnBackInvokedCallback(mAppCallback)
+ .setPrepareRemoteAnimation(true)
+ .setOnBackNavigationDone(new RemoteCallback(result)));
+ triggerBackGesture();
+ simulateRemoteAnimationStart(type);
+ mShellExecutor.flushAll();
+ releaseBackGesture();
+ simulateRemoteAnimationFinished();
+ mShellExecutor.flushAll();
+
+ assertTrue("Navigation Done callback not called for "
+ + BackNavigationInfo.typeToString(type), result.mBackNavigationDone);
+ assertTrue("TriggerBack should have been true", result.mTriggerBack);
+ }
}
@Test
- public void crossActivity_surfaceMovesWithGesture() {
- SurfaceControl screenshotSurface = new SurfaceControl();
- HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- createNavigationInfo(animationTarget, screenshotSurface, hardwareBuffer,
- BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true);
+ public void backToHome_dispatchesEvents() throws RemoteException {
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ /* enableAnimation = */ true,
+ /* isAnimationCallback = */ false);
+
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+
+ // Check that back start and progress is dispatched when first move.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
- // b/207481538, we check that the surface is not moved for now, we can re-enable this once
- // we implement the animation
- verify(mTransaction, never()).setScale(eq(screenshotSurface), anyInt(), anyInt());
- verify(mTransaction, never()).setPosition(
- animationTarget.leash, 100, 100);
- verify(mTransaction, atLeastOnce()).apply();
- }
- @Test
- public void verifyAnimationFinishes() {
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- boolean[] backNavigationDone = new boolean[]{false};
- boolean[] triggerBack = new boolean[]{false};
- createNavigationInfo(new BackNavigationInfo.Builder()
- .setDepartingAnimationTarget(animationTarget)
- .setType(BackNavigationInfo.TYPE_CROSS_ACTIVITY)
- .setOnBackNavigationDone(
- new RemoteCallback(result -> {
- backNavigationDone[0] = true;
- triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
- })));
- triggerBackGesture();
- assertTrue("Navigation Done callback not called", backNavigationDone[0]);
- assertTrue("TriggerBack should have been true", triggerBack[0]);
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+
+ verify(mAnimatorCallback).onBackStarted(any(BackMotionEvent.class));
+ verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
+ ArgumentCaptor<BackMotionEvent> backEventCaptor =
+ ArgumentCaptor.forClass(BackMotionEvent.class);
+ verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
+
+ // Check that back invocation is dispatched.
+ mController.setTriggerBack(true); // Fake trigger back
+ doMotionEvent(MotionEvent.ACTION_UP, 0);
+ verify(mAnimatorCallback).onBackInvoked();
}
@Test
- public void backToHome_dispatchesEvents() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
+ public void backToHomeWithAnimationCallback_dispatchesEvents() throws RemoteException {
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ /* enableAnimation = */ true,
+ /* isAnimationCallback = */ true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
// Check that back start and progress is dispatched when first move.
- doMotionEvent(MotionEvent.ACTION_MOVE, 100);
- simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
+ doMotionEvent(MotionEvent.ACTION_MOVE, 100, 3000);
+
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+
+ verify(mAnimatorCallback).onBackStarted(any(BackMotionEvent.class));
+ verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
ArgumentCaptor<BackMotionEvent> backEventCaptor =
ArgumentCaptor.forClass(BackMotionEvent.class);
- verify(mIOnBackInvokedCallback).onBackStarted(backEventCaptor.capture());
- assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget());
- verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackMotionEvent.class));
+ verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
// Check that back invocation is dispatched.
mController.setTriggerBack(true); // Fake trigger back
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verify(mIOnBackInvokedCallback).onBackInvoked();
+ verify(mAnimatorCallback).onBackInvoked();
}
@Test
@@ -265,124 +276,288 @@ public class BackAnimationControllerTest extends ShellTestCase {
Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0");
ShellInit shellInit = new ShellInit(mShellExecutor);
mController = new BackAnimationController(shellInit, mShellController,
- mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
+ mShellExecutor, new Handler(mTestableLooper.getLooper()),
mActivityTaskManager, mContext,
- mContentResolver);
+ mContentResolver, mAnimationBackground);
shellInit.init();
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
ArgumentCaptor<BackMotionEvent> backEventCaptor =
ArgumentCaptor.forClass(BackMotionEvent.class);
- createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback, false);
+
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ /* enableAnimation = */ false,
+ /* isAnimationCallback = */ false);
triggerBackGesture();
+ releaseBackGesture();
- verify(appCallback, never()).onBackStarted(any(BackMotionEvent.class));
- verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
- verify(appCallback, times(1)).onBackInvoked();
+ verify(mAppCallback, times(1)).onBackInvoked();
- verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackMotionEvent.class));
- verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
- verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ verify(mAnimatorCallback, never()).onBackStarted(any());
+ verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture());
+ verify(mAnimatorCallback, never()).onBackInvoked();
+ verify(mBackAnimationRunner, never()).onAnimationStart(
+ anyInt(), any(), any(), any(), any());
}
@Test
public void ignoresGesture_transitionInProgress() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ /* enableAnimation = */ true,
+ /* isAnimationCallback = */ false);
triggerBackGesture();
- simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ releaseBackGesture();
+
// Check that back invocation is dispatched.
- verify(mIOnBackInvokedCallback).onBackInvoked();
+ verify(mAnimatorCallback).onBackInvoked();
+ verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
+
+ reset(mAnimatorCallback);
+ reset(mBackAnimationRunner);
- reset(mIOnBackInvokedCallback);
// Verify that we prevent animation from restarting if another gestures happens before
// the previous transition is finished.
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
- verifyNoMoreInteractions(mIOnBackInvokedCallback);
- mController.onBackToLauncherAnimationFinished();
+ verifyNoMoreInteractions(mAnimatorCallback);
+
+ // Finish back navigation.
+ simulateRemoteAnimationFinished();
// Verify that more events from a rejected swipe cannot start animation.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verifyNoMoreInteractions(mIOnBackInvokedCallback);
+ verifyNoMoreInteractions(mAnimatorCallback);
// Verify that we start accepting gestures again once transition finishes.
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
- simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
+
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ verify(mAnimatorCallback).onBackStarted(any());
+ verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
}
@Test
public void acceptsGesture_transitionTimeout() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ /* enableAnimation = */ true,
+ /* isAnimationCallback = */ false);
+
+ // In case it is still running in animation.
+ doNothing().when(mAnimatorCallback).onBackInvoked();
triggerBackGesture();
- simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- reset(mIOnBackInvokedCallback);
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ mShellExecutor.flushAll();
+
+ releaseBackGesture();
// Simulate transition timeout.
mShellExecutor.flushAll();
+ reset(mAnimatorCallback);
+
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
- simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ verify(mAnimatorCallback).onBackStarted(any());
}
-
@Test
public void cancelBackInvokeWhenLostFocus() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
- RemoteAnimationTarget animationTarget = createAnimationTarget();
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ /* enableAnimation = */ true,
+ /* isAnimationCallback = */ false);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
// Check that back start and progress is dispatched when first move.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
- simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
+
+ simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ verify(mAnimatorCallback).onBackStarted(any());
+ verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
// Check that back invocation is dispatched.
mController.setTriggerBack(true); // Fake trigger back
// In case the focus has been changed.
- IBinder token = mock(IBinder.class);
- mController.mFocusObserver.focusLost(token);
+ mController.mNavigationObserver.sendResult(null);
mShellExecutor.flushAll();
- verify(mIOnBackInvokedCallback).onBackCancelled();
+ verify(mAnimatorCallback).onBackCancelled();
// No more back invoke.
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ verify(mAnimatorCallback, never()).onBackInvoked();
+ }
+
+ @Test
+ public void animationNotDefined() throws RemoteException {
+ final int[] testTypes = new int[] {
+ BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ BackNavigationInfo.TYPE_CROSS_TASK,
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ BackNavigationInfo.TYPE_DIALOG_CLOSE};
+
+ for (int type: testTypes) {
+ unregisterAnimation(type);
+ }
+
+ for (int type: testTypes) {
+ final ResultListener result = new ResultListener();
+ createNavigationInfo(new BackNavigationInfo.Builder()
+ .setType(type)
+ .setOnBackInvokedCallback(mAppCallback)
+ .setPrepareRemoteAnimation(true)
+ .setOnBackNavigationDone(new RemoteCallback(result)));
+ triggerBackGesture();
+ simulateRemoteAnimationStart(type);
+ mShellExecutor.flushAll();
+
+ releaseBackGesture();
+ mShellExecutor.flushAll();
+
+ assertTrue("Navigation Done callback not called for "
+ + BackNavigationInfo.typeToString(type), result.mBackNavigationDone);
+ assertTrue("TriggerBack should have been true", result.mTriggerBack);
+ }
+
+ verify(mAppCallback, never()).onBackStarted(any());
+ verify(mAppCallback, never()).onBackProgressed(any());
+ verify(mAppCallback, times(testTypes.length)).onBackInvoked();
+
+ verify(mAnimatorCallback, never()).onBackStarted(any());
+ verify(mAnimatorCallback, never()).onBackProgressed(any());
+ verify(mAnimatorCallback, never()).onBackInvoked();
+ }
+
+ @Test
+ public void callbackShouldDeliverProgress() throws RemoteException {
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+
+ final int type = BackNavigationInfo.TYPE_CALLBACK;
+ final ResultListener result = new ResultListener();
+ createNavigationInfo(new BackNavigationInfo.Builder()
+ .setType(type)
+ .setOnBackInvokedCallback(mAppCallback)
+ .setOnBackNavigationDone(new RemoteCallback(result)));
+ triggerBackGesture();
+ mShellExecutor.flushAll();
+ releaseBackGesture();
+ mShellExecutor.flushAll();
+
+ assertTrue("Navigation Done callback not called for "
+ + BackNavigationInfo.typeToString(type), result.mBackNavigationDone);
+ assertTrue("TriggerBack should have been true", result.mTriggerBack);
+
+ verify(mAppCallback, times(1)).onBackStarted(any());
+ verify(mAppCallback, times(1)).onBackProgressed(any());
+ verify(mAppCallback, times(1)).onBackInvoked();
+
+ verify(mAnimatorCallback, never()).onBackStarted(any());
+ verify(mAnimatorCallback, never()).onBackProgressed(any());
+ verify(mAnimatorCallback, never()).onBackInvoked();
+ }
+
+ @Test
+ public void testBackToActivity() throws RemoteException {
+ final CrossActivityAnimation animation = new CrossActivityAnimation(mContext,
+ mAnimationBackground);
+ verifySystemBackBehavior(
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.mBackAnimationRunner);
+ }
+
+ @Test
+ public void testBackToTask() throws RemoteException {
+ final CrossTaskBackAnimation animation = new CrossTaskBackAnimation(mContext,
+ mAnimationBackground);
+ verifySystemBackBehavior(
+ BackNavigationInfo.TYPE_CROSS_TASK, animation.mBackAnimationRunner);
+ }
+
+ private void verifySystemBackBehavior(int type, BackAnimationRunner animation)
+ throws RemoteException {
+ final BackAnimationRunner animationRunner = spy(animation);
+ final IRemoteAnimationRunner runner = spy(animationRunner.getRunner());
+ final IOnBackInvokedCallback callback = spy(animationRunner.getCallback());
+
+ // Set up the monitoring objects.
+ doNothing().when(runner).onAnimationStart(anyInt(), any(), any(), any(), any());
+ doReturn(runner).when(animationRunner).getRunner();
+ doReturn(callback).when(animationRunner).getCallback();
+
+ mController.registerAnimation(type, animationRunner);
+
+ createNavigationInfo(type,
+ /* enableAnimation = */ true,
+ /* isAnimationCallback = */ false);
+
+ doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+
+ // Check that back start and progress is dispatched when first move.
+ doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+
+ simulateRemoteAnimationStart(type);
+
+ verify(callback).onBackStarted(any(BackMotionEvent.class));
+ verify(animationRunner).startAnimation(any(), any(), any(), any());
+
+ // Check that back invocation is dispatched.
+ mController.setTriggerBack(true); // Fake trigger back
+ doMotionEvent(MotionEvent.ACTION_UP, 0);
+ verify(callback).onBackInvoked();
}
private void doMotionEvent(int actionDown, int coordinate) {
+ doMotionEvent(actionDown, coordinate, 0);
+ }
+
+ private void doMotionEvent(int actionDown, int coordinate, float velocity) {
mController.onMotionEvent(
- coordinate, coordinate,
- actionDown,
- BackEvent.EDGE_LEFT);
- mEventTime += 10;
+ /* touchX */ coordinate,
+ /* touchY */ coordinate,
+ /* velocityX = */ velocity,
+ /* velocityY = */ velocity,
+ /* keyAction */ actionDown,
+ /* swipeEdge */ BackEvent.EDGE_LEFT);
}
- private void simulateRemoteAnimationStart(int type, RemoteAnimationTarget animationTarget)
- throws RemoteException {
- if (mController.mIBackAnimationRunner != null) {
- final RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget};
- mController.mIBackAnimationRunner.onAnimationStart(mIBackNaviAnimationController, type,
- targets, null, null);
+ private void simulateRemoteAnimationStart(int type) throws RemoteException {
+ RemoteAnimationTarget animationTarget = createAnimationTarget();
+ RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget};
+ if (mController.mBackAnimationAdapter != null) {
+ mController.mBackAnimationAdapter.getRunner().onAnimationStart(
+ targets, null, null, mBackAnimationFinishedCallback);
mShellExecutor.flushAll();
}
}
+
+ private void simulateRemoteAnimationFinished() {
+ mController.onBackAnimationFinished();
+ mController.finishBackNavigation();
+ }
+
+ private void registerAnimation(int type) {
+ mController.registerAnimation(type,
+ new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner));
+ }
+
+ private void unregisterAnimation(int type) {
+ mController.unregisterAnimation(type);
+ }
+
+ private static class ResultListener implements RemoteCallback.OnResultListener {
+ boolean mBackNavigationDone = false;
+ boolean mTriggerBack = false;
+ @Override
+ public void onResult(@Nullable Bundle result) {
+ mBackNavigationDone = true;
+ mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK);
+ }
+ };
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
new file mode 100644
index 000000000000..874ef80c29f0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.back;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.window.BackEvent;
+import android.window.BackMotionEvent;
+import android.window.BackProgressAnimator;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class BackProgressAnimatorTest {
+ private BackProgressAnimator mProgressAnimator;
+ private BackEvent mReceivedBackEvent;
+ private float mTargetProgress = 0.5f;
+ private CountDownLatch mTargetProgressCalled = new CountDownLatch(1);
+ private Handler mMainThreadHandler;
+
+ private BackMotionEvent backMotionEventFrom(float touchX, float progress) {
+ return new BackMotionEvent(
+ /* touchX = */ touchX,
+ /* touchY = */ 0,
+ /* progress = */ progress,
+ /* velocityX = */ 0,
+ /* velocityY = */ 0,
+ /* swipeEdge = */ BackEvent.EDGE_LEFT,
+ /* departingAnimationTarget = */ null);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mMainThreadHandler = new Handler(Looper.getMainLooper());
+ final BackMotionEvent backEvent = backMotionEventFrom(0, 0);
+ mMainThreadHandler.post(
+ () -> {
+ mProgressAnimator = new BackProgressAnimator();
+ mProgressAnimator.onBackStarted(backEvent, this::onGestureProgress);
+ });
+ }
+
+ @Test
+ public void testBackProgressed() throws InterruptedException {
+ final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress);
+ mMainThreadHandler.post(
+ () -> mProgressAnimator.onBackProgressed(backEvent));
+
+ mTargetProgressCalled.await(1, TimeUnit.SECONDS);
+
+ assertNotNull(mReceivedBackEvent);
+ assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */);
+ }
+
+ @Test
+ public void testBackCancelled() throws InterruptedException {
+ // Give the animator some progress.
+ final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress);
+ mMainThreadHandler.post(
+ () -> mProgressAnimator.onBackProgressed(backEvent));
+ mTargetProgressCalled.await(1, TimeUnit.SECONDS);
+ assertNotNull(mReceivedBackEvent);
+
+ // Trigger animation cancel, the target progress should be 0.
+ mTargetProgress = 0;
+ mTargetProgressCalled = new CountDownLatch(1);
+ CountDownLatch cancelCallbackCalled = new CountDownLatch(1);
+ mMainThreadHandler.post(
+ () -> mProgressAnimator.onBackCancelled(() -> cancelCallbackCalled.countDown()));
+ cancelCallbackCalled.await(1, TimeUnit.SECONDS);
+ mTargetProgressCalled.await(1, TimeUnit.SECONDS);
+ assertNotNull(mReceivedBackEvent);
+ assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */);
+ }
+
+ private void onGestureProgress(BackEvent backEvent) {
+ if (mTargetProgress == backEvent.getProgress()) {
+ mReceivedBackEvent = backEvent;
+ mTargetProgressCalled.countDown();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
new file mode 100644
index 000000000000..e7d459893ce8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.back;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.app.WindowConfiguration;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Choreographer;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.BackNavigationInfo;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class CustomizeActivityAnimationTest extends ShellTestCase {
+ private static final int BOUND_SIZE = 100;
+ @Mock
+ private BackAnimationBackground mBackAnimationBackground;
+ @Mock
+ private Animation mMockCloseAnimation;
+ @Mock
+ private Animation mMockOpenAnimation;
+
+ private CustomizeActivityAnimation mCustomizeActivityAnimation;
+
+ @Before
+ public void setUp() throws Exception {
+ mCustomizeActivityAnimation = new CustomizeActivityAnimation(mContext,
+ mBackAnimationBackground, mock(SurfaceControl.Transaction.class),
+ mock(Choreographer.class));
+ spyOn(mCustomizeActivityAnimation);
+ spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation);
+ }
+
+ RemoteAnimationTarget createAnimationTarget(boolean open) {
+ SurfaceControl topWindowLeash = new SurfaceControl();
+ return new RemoteAnimationTarget(1,
+ open ? RemoteAnimationTarget.MODE_OPENING : RemoteAnimationTarget.MODE_CLOSING,
+ topWindowLeash, false, new Rect(), new Rect(), -1,
+ new Point(0, 0), new Rect(0, 0, BOUND_SIZE, BOUND_SIZE), new Rect(),
+ new WindowConfiguration(), true, null, null, null, false, -1);
+ }
+
+ @Test
+ public void receiveFinishAfterInvoke() throws InterruptedException {
+ spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader);
+ doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+ .loadAnimation(any(), eq(false));
+ doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+ .loadAnimation(any(), eq(true));
+
+ mCustomizeActivityAnimation.prepareNextAnimation(
+ new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+ final RemoteAnimationTarget close = createAnimationTarget(false);
+ final RemoteAnimationTarget open = createAnimationTarget(true);
+ // start animation with remote animation targets
+ final CountDownLatch finishCalled = new CountDownLatch(1);
+ final Runnable finishCallback = finishCalled::countDown;
+ mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+ new RemoteAnimationTarget[]{close, open}, null, null, finishCallback);
+ verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+ verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+
+ try {
+ mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked();
+ } catch (RemoteException r) {
+ fail("onBackInvoked throw remote exception");
+ }
+ verify(mCustomizeActivityAnimation).onGestureCommitted();
+ finishCalled.await(1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void receiveFinishAfterCancel() throws InterruptedException {
+ spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader);
+ doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+ .loadAnimation(any(), eq(false));
+ doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+ .loadAnimation(any(), eq(true));
+
+ mCustomizeActivityAnimation.prepareNextAnimation(
+ new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+ final RemoteAnimationTarget close = createAnimationTarget(false);
+ final RemoteAnimationTarget open = createAnimationTarget(true);
+ // start animation with remote animation targets
+ final CountDownLatch finishCalled = new CountDownLatch(1);
+ final Runnable finishCallback = finishCalled::countDown;
+ mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+ new RemoteAnimationTarget[]{close, open}, null, null, finishCallback);
+ verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+ verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+
+ try {
+ mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackCancelled();
+ } catch (RemoteException r) {
+ fail("onBackCancelled throw remote exception");
+ }
+ finishCalled.await(1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void receiveFinishWithoutAnimationAfterInvoke() throws InterruptedException {
+ mCustomizeActivityAnimation.prepareNextAnimation(
+ new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+ // start animation without any remote animation targets
+ final CountDownLatch finishCalled = new CountDownLatch(1);
+ final Runnable finishCallback = finishCalled::countDown;
+ mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+ new RemoteAnimationTarget[]{}, null, null, finishCallback);
+
+ try {
+ mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked();
+ } catch (RemoteException r) {
+ fail("onBackInvoked throw remote exception");
+ }
+ verify(mCustomizeActivityAnimation).onGestureCommitted();
+ finishCalled.await(1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void testLoadCustomAnimation() {
+ testLoadCustomAnimation(10, 20, 0);
+ }
+
+ @Test
+ public void testLoadCustomAnimationNoEnter() {
+ testLoadCustomAnimation(0, 10, 0);
+ }
+
+ @Test
+ public void testLoadWindowAnimations() {
+ testLoadCustomAnimation(0, 0, 30);
+ }
+
+ @Test
+ public void testCustomAnimationHigherThanWindowAnimations() {
+ testLoadCustomAnimation(10, 20, 30);
+ }
+
+ private void testLoadCustomAnimation(int enterResId, int exitResId, int windowAnimations) {
+ final String testPackage = "TestPackage";
+ BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
+ .setCustomAnimation(testPackage, enterResId, exitResId, Color.GREEN)
+ .setWindowAnimations(testPackage, windowAnimations);
+ final BackNavigationInfo.CustomAnimationInfo info = builder.build()
+ .getCustomAnimationInfo();
+
+ doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
+ .mTransitionAnimation)
+ .loadAppTransitionAnimation(eq(testPackage), eq(enterResId));
+ doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
+ .mTransitionAnimation)
+ .loadAppTransitionAnimation(eq(testPackage), eq(exitResId));
+ doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
+ .mTransitionAnimation)
+ .loadAnimationAttr(eq(testPackage), eq(windowAnimations), anyInt(), anyBoolean());
+ doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
+ .mTransitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean());
+
+ CustomizeActivityAnimation.AnimationLoadResult result =
+ mCustomizeActivityAnimation.mCustomAnimationLoader.loadAll(info);
+
+ if (exitResId != 0) {
+ if (enterResId == 0) {
+ verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation,
+ never()).loadAppTransitionAnimation(eq(testPackage), eq(enterResId));
+ verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation)
+ .loadDefaultAnimationAttr(anyInt(), anyBoolean());
+ } else {
+ assertEquals(result.mEnterAnimation, mMockOpenAnimation);
+ }
+ assertEquals(result.mBackgroundColor, Color.GREEN);
+ assertEquals(result.mCloseAnimation, mMockCloseAnimation);
+ verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, never())
+ .loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean());
+ } else if (windowAnimations != 0) {
+ verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation,
+ times(2)).loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean());
+ assertEquals(result.mCloseAnimation, mMockCloseAnimation);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
deleted file mode 100644
index ba9c159bad28..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.back;
-
-import static org.junit.Assert.assertEquals;
-
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-
-import org.junit.Before;
-import org.junit.Test;
-
-public class TouchTrackerTest {
- private static final float FAKE_THRESHOLD = 400;
- private static final float INITIAL_X_LEFT_EDGE = 5;
- private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE;
- private TouchTracker mTouchTracker;
-
- @Before
- public void setUp() throws Exception {
- mTouchTracker = new TouchTracker();
- mTouchTracker.setProgressThreshold(FAKE_THRESHOLD);
- }
-
- @Test
- public void generatesProgress_onStart() {
- mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
- BackMotionEvent event = mTouchTracker.createStartEvent(null);
- assertEquals(event.getProgress(), 0f, 0f);
- }
-
- @Test
- public void generatesProgress_leftEdge() {
- mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
- float touchX = 10;
-
- // Pre-commit
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
-
- // Post-commit
- touchX += 100;
- mTouchTracker.setTriggerBack(true);
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
-
- // Cancel
- touchX -= 10;
- mTouchTracker.setTriggerBack(false);
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), 0, 0f);
-
- // Cancel more
- touchX -= 10;
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), 0, 0f);
-
- // Restart
- touchX += 10;
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), 0, 0f);
-
- // Restarted, but pre-commit
- float restartX = touchX;
- touchX += 10;
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f);
-
- // Restarted, post-commit
- touchX += 10;
- mTouchTracker.setTriggerBack(true);
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
- }
-
- @Test
- public void generatesProgress_rightEdge() {
- mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT);
- float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge
-
- // Pre-commit
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
-
- // Post-commit
- touchX -= 100;
- mTouchTracker.setTriggerBack(true);
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
-
- // Cancel
- touchX += 10;
- mTouchTracker.setTriggerBack(false);
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), 0, 0f);
-
- // Cancel more
- touchX += 10;
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), 0, 0f);
-
- // Restart
- touchX -= 10;
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), 0, 0f);
-
- // Restarted, but pre-commit
- float restartX = touchX;
- touchX -= 10;
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f);
-
- // Restarted, post-commit
- touchX -= 10;
- mTouchTracker.setTriggerBack(true);
- mTouchTracker.update(touchX, 0);
- assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
- }
-
- private float getProgress() {
- return mTouchTracker.createProgressEvent().getProgress();
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt
new file mode 100644
index 000000000000..9088e8997e79
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+package com.android.wm.shell.back
+
+import android.util.MathUtils
+import android.window.BackEvent
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TouchTrackerTest {
+ private fun linearTouchTracker(): TouchTracker = TouchTracker().apply {
+ setProgressThresholds(MAX_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR)
+ }
+
+ private fun nonLinearTouchTracker(): TouchTracker = TouchTracker().apply {
+ setProgressThresholds(LINEAR_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR)
+ }
+
+ private fun TouchTracker.assertProgress(expected: Float) {
+ val actualProgress = createProgressEvent().progress
+ assertEquals(expected, actualProgress, /* delta = */ 0f)
+ }
+
+ @Test
+ fun generatesProgress_onStart() {
+ val linearTracker = linearTouchTracker()
+ linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
+ val event = linearTracker.createStartEvent(null)
+ assertEquals(0f, event.progress, 0f)
+ }
+
+ @Test
+ fun generatesProgress_leftEdge() {
+ val linearTracker = linearTouchTracker()
+ linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
+ var touchX = 10f
+ val velocityX = 0f
+ val velocityY = 0f
+
+ // Pre-commit
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
+
+ // Post-commit
+ touchX += 100f
+ linearTracker.setTriggerBack(true)
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
+
+ // Cancel
+ touchX -= 10f
+ linearTracker.setTriggerBack(false)
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress(0f)
+
+ // Cancel more
+ touchX -= 10f
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress(0f)
+
+ // Restart
+ touchX += 10f
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress(0f)
+
+ // Restarted, but pre-commit
+ val restartX = touchX
+ touchX += 10f
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((touchX - restartX) / MAX_DISTANCE)
+
+ // Restarted, post-commit
+ touchX += 10f
+ linearTracker.setTriggerBack(true)
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
+ }
+
+ @Test
+ fun generatesProgress_rightEdge() {
+ val linearTracker = linearTouchTracker()
+ linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT)
+ var touchX = INITIAL_X_RIGHT_EDGE - 10 // Fake right edge
+ val velocityX = 0f
+ val velocityY = 0f
+ val target = MAX_DISTANCE
+
+ // Pre-commit
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
+
+ // Post-commit
+ touchX -= 100f
+ linearTracker.setTriggerBack(true)
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
+
+ // Cancel
+ touchX += 10f
+ linearTracker.setTriggerBack(false)
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress(0f)
+
+ // Cancel more
+ touchX += 10f
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress(0f)
+
+ // Restart
+ touchX -= 10f
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress(0f)
+
+ // Restarted, but pre-commit
+ val restartX = touchX
+ touchX -= 10f
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((restartX - touchX) / target)
+
+ // Restarted, post-commit
+ touchX -= 10f
+ linearTracker.setTriggerBack(true)
+ linearTracker.update(touchX, 0f, velocityX, velocityY)
+ linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
+ }
+
+ @Test
+ fun generatesNonLinearProgress_leftEdge() {
+ val nonLinearTracker = nonLinearTouchTracker()
+ nonLinearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
+ var touchX = 10f
+ val velocityX = 0f
+ val velocityY = 0f
+ val linearTarget = LINEAR_DISTANCE + (MAX_DISTANCE - LINEAR_DISTANCE) * NON_LINEAR_FACTOR
+
+ // Pre-commit: linear progress
+ nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
+ nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
+
+ // Post-commit: still linear progress
+ touchX += 100f
+ nonLinearTracker.setTriggerBack(true)
+ nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
+ nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
+
+ // still linear progress
+ touchX = INITIAL_X_LEFT_EDGE + LINEAR_DISTANCE
+ nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
+ nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
+
+ // non linear progress
+ touchX += 10
+ nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
+ val nonLinearTouch = (touchX - INITIAL_X_LEFT_EDGE) - LINEAR_DISTANCE
+ val nonLinearProgress = nonLinearTouch / NON_LINEAR_DISTANCE
+ val nonLinearTarget = MathUtils.lerp(linearTarget, MAX_DISTANCE, nonLinearProgress)
+ nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / nonLinearTarget)
+ }
+
+ companion object {
+ private const val MAX_DISTANCE = 500f
+ private const val LINEAR_DISTANCE = 400f
+ private const val NON_LINEAR_DISTANCE = MAX_DISTANCE - LINEAR_DISTANCE
+ private const val NON_LINEAR_FACTOR = 0.2f
+ private const val INITIAL_X_LEFT_EDGE = 5f
+ private const val INITIAL_X_RIGHT_EDGE = MAX_DISTANCE - INITIAL_X_LEFT_EDGE
+ }
+} \ No newline at end of file
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 8b025cd7c246..4a55429eacb6 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
@@ -16,8 +16,6 @@
package com.android.wm.shell.bubbles;
-import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
-
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -185,7 +183,11 @@ public class BubbleDataTest extends ShellTestCase {
Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
appBubbleIntent.setPackage(mContext.getPackageName());
- mAppBubble = new Bubble(appBubbleIntent, new UserHandle(1), mMainExecutor);
+ mAppBubble = Bubble.createAppBubble(
+ appBubbleIntent,
+ new UserHandle(1),
+ mock(Icon.class),
+ mMainExecutor);
mPositioner = new TestableBubblePositioner(mContext,
mock(WindowManager.class));
@@ -1100,14 +1102,15 @@ public class BubbleDataTest extends ShellTestCase {
@Test
public void test_removeAppBubble_skipsOverflow() {
+ String appBubbleKey = mAppBubble.getKey();
mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/,
false /* showInShade */);
- assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isEqualTo(mAppBubble);
+ assertThat(mBubbleData.getBubbleInStackWithKey(appBubbleKey)).isEqualTo(mAppBubble);
- mBubbleData.dismissBubbleWithKey(KEY_APP_BUBBLE, Bubbles.DISMISS_USER_GESTURE);
+ mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_USER_GESTURE);
- assertThat(mBubbleData.getOverflowBubbleWithKey(KEY_APP_BUBBLE)).isNull();
- assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull();
+ assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull();
+ assertThat(mBubbleData.getBubbleInStackWithKey(appBubbleKey)).isNull();
}
private void verifyUpdateReceived() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
index e8f3f69ca64e..afec1ee12341 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
@@ -29,8 +29,11 @@ import static org.mockito.Mockito.when;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.os.Bundle;
+import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -40,6 +43,7 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.bubbles.BubbleInfo;
import org.junit.Before;
import org.junit.Test;
@@ -75,7 +79,7 @@ public class BubbleTest extends ShellTestCase {
Intent target = new Intent(mContext, BubblesTestActivity.class);
Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
PendingIntent.getActivity(mContext, 0, target, PendingIntent.FLAG_MUTABLE),
- Icon.createWithResource(mContext, R.drawable.bubble_ic_create_bubble))
+ Icon.createWithResource(mContext, R.drawable.bubble_ic_create_bubble))
.build();
when(mSbn.getNotification()).thenReturn(mNotif);
when(mNotif.getBubbleMetadata()).thenReturn(metadata);
@@ -162,4 +166,55 @@ public class BubbleTest extends ShellTestCase {
verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any());
}
+
+ @Test
+ public void testBubbleIsConversation_hasConversationShortcut() {
+ Bubble bubble = createBubbleWithShortcut();
+ assertThat(bubble.getShortcutInfo()).isNotNull();
+ assertThat(bubble.isConversation()).isTrue();
+ }
+
+ @Test
+ public void testBubbleIsConversation_hasNoShortcut() {
+ Bubble bubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor);
+ assertThat(bubble.getShortcutInfo()).isNull();
+ assertThat(bubble.isConversation()).isFalse();
+ }
+
+ @Test
+ public void testBubbleAsBubbleBarBubble_withShortcut() {
+ Bubble bubble = createBubbleWithShortcut();
+ BubbleInfo bubbleInfo = bubble.asBubbleBarBubble();
+
+ assertThat(bubble.getShortcutInfo()).isNotNull();
+ assertThat(bubbleInfo.getShortcutId()).isNotNull();
+ assertThat(bubbleInfo.getShortcutId()).isEqualTo(bubble.getShortcutId());
+ assertThat(bubbleInfo.getKey()).isEqualTo(bubble.getKey());
+ assertThat(bubbleInfo.getUserId()).isEqualTo(bubble.getUser().getIdentifier());
+ assertThat(bubbleInfo.getPackageName()).isEqualTo(bubble.getPackageName());
+ }
+
+ @Test
+ public void testBubbleAsBubbleBarBubble_withoutShortcut() {
+ Intent intent = new Intent(mContext, BubblesTestActivity.class);
+ intent.setPackage(mContext.getPackageName());
+ Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1 /* userId */),
+ null /* icon */, mMainExecutor);
+ BubbleInfo bubbleInfo = bubble.asBubbleBarBubble();
+
+ assertThat(bubble.getShortcutInfo()).isNull();
+ assertThat(bubbleInfo.getShortcutId()).isNull();
+ assertThat(bubbleInfo.getKey()).isEqualTo(bubble.getKey());
+ assertThat(bubbleInfo.getUserId()).isEqualTo(bubble.getUser().getIdentifier());
+ assertThat(bubbleInfo.getPackageName()).isEqualTo(bubble.getPackageName());
+ }
+
+ private Bubble createBubbleWithShortcut() {
+ ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext)
+ .setId("mockShortcutId")
+ .build();
+ return new Bubble("mockKey", shortcutInfo, 10, Resources.ID_NULL,
+ "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */,
+ mMainExecutor, mBubbleMetadataFlagListener);
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 262ee72d86fc..01e2f988fbfc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -17,7 +17,7 @@
package com.android.wm.shell.common;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.Surface.ROTATION_0;
import static android.view.WindowInsets.Type.ime;
@@ -41,7 +41,6 @@ import android.view.SurfaceControl;
import androidx.test.filters.SmallTest;
-import com.android.internal.view.IInputMethodManager;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.sysui.ShellInit;
@@ -58,8 +57,6 @@ public class DisplayImeControllerTest extends ShellTestCase {
@Mock
private SurfaceControl.Transaction mT;
@Mock
- private IInputMethodManager mMock;
- @Mock
private ShellInit mShellInit;
private DisplayImeController.PerDisplay mPerDisplay;
private Executor mExecutor;
@@ -79,10 +76,6 @@ public class DisplayImeControllerTest extends ShellTestCase {
}
}, mExecutor) {
@Override
- public IInputMethodManager getImms() {
- return mMock;
- }
- @Override
void removeImeSurface() { }
}.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
}
@@ -106,13 +99,13 @@ public class DisplayImeControllerTest extends ShellTestCase {
@Test
public void showInsets_schedulesNoWorkOnExecutor() {
- mPerDisplay.showInsets(ime(), true);
+ mPerDisplay.showInsets(ime(), true /* fromIme */, null /* statsToken */);
verifyZeroInteractions(mExecutor);
}
@Test
public void hideInsets_schedulesNoWorkOnExecutor() {
- mPerDisplay.hideInsets(ime(), true);
+ mPerDisplay.hideInsets(ime(), true /* fromIme */, null /* statsToken */);
verifyZeroInteractions(mExecutor);
}
@@ -145,14 +138,15 @@ public class DisplayImeControllerTest extends ShellTestCase {
private InsetsSourceControl[] insetsSourceControl() {
return new InsetsSourceControl[]{
new InsetsSourceControl(
- ITYPE_IME, mock(SurfaceControl.class), false, new Point(0, 0), Insets.NONE)
+ ID_IME, ime(), mock(SurfaceControl.class), false, new Point(0, 0),
+ Insets.NONE)
};
}
private InsetsState insetsStateWithIme(boolean visible) {
InsetsState state = new InsetsState();
- state.addSource(new InsetsSource(ITYPE_IME));
- state.setSourceVisible(ITYPE_IME, visible);
+ state.addSource(new InsetsSource(ID_IME, ime()));
+ state.setSourceVisible(ID_IME, visible);
return state;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 5f5a3c584ee0..956f1cd419c2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.SparseArray;
@@ -32,7 +33,8 @@ import android.view.IDisplayWindowInsetsController;
import android.view.IWindowManager;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
import androidx.test.filters.SmallTest;
@@ -108,11 +110,13 @@ public class DisplayInsetsControllerTest extends ShellTestCase {
mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener);
mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null,
- new InsetsVisibilities());
+ WindowInsets.Type.defaultVisible());
mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null);
mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null);
- mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false);
- mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false,
+ null /* statsToken */);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false,
+ null /* statsToken */);
mExecutor.flushAll();
assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -128,11 +132,13 @@ public class DisplayInsetsControllerTest extends ShellTestCase {
assertTrue(secondListener.hideInsetsCount == 0);
mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null,
- new InsetsVisibilities());
+ WindowInsets.Type.defaultVisible());
mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null);
mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null);
- mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false);
- mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false,
+ null /* statsToken */);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false,
+ null /* statsToken */);
mExecutor.flushAll();
assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -175,8 +181,7 @@ public class DisplayInsetsControllerTest extends ShellTestCase {
int hideInsetsCount = 0;
@Override
- public void topFocusedWindowChanged(ComponentName component,
- InsetsVisibilities requestedVisibilities) {
+ public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) {
topFocusedWindowChangedCount++;
}
@@ -192,12 +197,12 @@ public class DisplayInsetsControllerTest extends ShellTestCase {
}
@Override
- public void showInsets(int types, boolean fromIme) {
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
showInsetsCount++;
}
@Override
- public void hideInsets(int types, boolean fromIme) {
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
hideInsetsCount++;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
index 1347e061eb45..60ee918ee545 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
@@ -112,9 +112,9 @@ public class TaskStackListenerImplTest extends ShellTestCase {
@Test
public void testOnTaskProfileLocked() {
ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class);
- mImpl.onTaskProfileLocked(info);
- verify(mCallback).onTaskProfileLocked(eq(info));
- verify(mOtherCallback).onTaskProfileLocked(eq(info));
+ mImpl.onTaskProfileLocked(info, 0);
+ verify(mCallback).onTaskProfileLocked(eq(info), eq(0));
+ verify(mOtherCallback).onTaskProfileLocked(eq(info), eq(0));
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt
new file mode 100644
index 000000000000..432909f18813
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.common.bubbles
+
+import android.os.Parcel
+import android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BubbleInfoTest : ShellTestCase() {
+
+ @Test
+ fun bubbleInfo() {
+ val bubbleInfo =
+ BubbleInfo("key", 0, "shortcut id", null, 6, "com.some.package", "title", true)
+ val parcel = Parcel.obtain()
+ bubbleInfo.writeToParcel(parcel, PARCELABLE_WRITE_RETURN_VALUE)
+ parcel.setDataPosition(0)
+
+ val bubbleInfoFromParcel = BubbleInfo.CREATOR.createFromParcel(parcel)
+
+ assertThat(bubbleInfo.key).isEqualTo(bubbleInfoFromParcel.key)
+ assertThat(bubbleInfo.flags).isEqualTo(bubbleInfoFromParcel.flags)
+ assertThat(bubbleInfo.shortcutId).isEqualTo(bubbleInfoFromParcel.shortcutId)
+ assertThat(bubbleInfo.icon).isEqualTo(bubbleInfoFromParcel.icon)
+ assertThat(bubbleInfo.userId).isEqualTo(bubbleInfoFromParcel.userId)
+ assertThat(bubbleInfo.packageName).isEqualTo(bubbleInfoFromParcel.packageName)
+ assertThat(bubbleInfo.title).isEqualTo(bubbleInfoFromParcel.title)
+ assertThat(bubbleInfo.isImportantConversation)
+ .isEqualTo(bubbleInfoFromParcel.isImportantConversation)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 3d779481d361..443cea245a4f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -26,6 +26,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
@@ -41,6 +42,7 @@ import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import org.junit.Before;
@@ -57,6 +59,7 @@ import org.mockito.MockitoAnnotations;
public class SplitLayoutTests extends ShellTestCase {
@Mock SplitLayout.SplitLayoutHandler mSplitLayoutHandler;
@Mock SplitWindowManager.ParentContainerCallbacks mCallbacks;
+ @Mock DisplayController mDisplayController;
@Mock DisplayImeController mDisplayImeController;
@Mock ShellTaskOrganizer mTaskOrganizer;
@Mock WindowContainerTransaction mWct;
@@ -72,6 +75,7 @@ public class SplitLayoutTests extends ShellTestCase {
getConfiguration(),
mSplitLayoutHandler,
mCallbacks,
+ mDisplayController,
mDisplayImeController,
mTaskOrganizer,
SplitLayout.PARALLAX_NONE));
@@ -100,6 +104,10 @@ public class SplitLayoutTests extends ShellTestCase {
// Verify updateConfiguration returns true if the density changed.
config.densityDpi = 123;
assertThat(mSplitLayout.updateConfiguration(config)).isTrue();
+
+ // Verify updateConfiguration checks the current DisplayLayout
+ verify(mDisplayController, times(5)) // init * 1 + updateConfiguration * 4
+ .getDisplayLayout(anyInt());
}
@Test
@@ -168,6 +176,14 @@ public class SplitLayoutTests extends ShellTestCase {
verify(mWct).setSmallestScreenWidthDp(eq(task2.token), anyInt());
}
+ @Test
+ public void testRoateTo_checksDisplayLayout() {
+ mSplitLayout.rotateTo(90);
+
+ verify(mDisplayController, times(2)) // init * 1 + rotateTo * 1
+ .getDisplayLayout(anyInt());
+ }
+
private void waitDividerFlingFinished() {
verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(),
mRunnableCaptor.capture());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 4cf9e6acaec6..a6501f05475f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -18,7 +18,7 @@ package com.android.wm.shell.compatui;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
+import static android.view.WindowInsets.Type.navigationBars;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -331,7 +331,8 @@ public class CompatUIControllerTest extends ShellTestCase {
mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
InsetsState insetsState = new InsetsState();
- InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+ InsetsSource insetsSource = new InsetsSource(
+ InsetsSource.createId(null, 0, navigationBars()), navigationBars());
insetsSource.setFrame(0, 0, 1000, 1000);
insetsState.addSource(insetsSource);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 0c5edc3f59de..78c3cbdaace6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -20,7 +20,7 @@ import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
+import static android.view.WindowInsets.Type.navigationBars;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -28,7 +28,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -37,6 +36,7 @@ import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.app.TaskInfo;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.util.Pair;
@@ -337,8 +337,10 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
// Update if the insets change on the existing display layout
clearInvocations(mWindowManager);
InsetsState insetsState = new InsetsState();
- InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
- insetsSource.setFrame(0, 0, 1000, 1000);
+ insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000));
+ InsetsSource insetsSource = new InsetsSource(
+ InsetsSource.createId(null, 0, navigationBars()), navigationBars());
+ insetsSource.setFrame(0, 1800, 1000, 2000);
insetsState.addSource(insetsSource);
displayLayout.setInsets(mContext.getResources(), insetsState);
mWindowManager.updateDisplayLayout(displayLayout);
@@ -360,14 +362,14 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
mWindowManager.updateVisibility(/* canShow= */ false);
verify(mWindowManager, never()).createLayout(anyBoolean());
- verify(mLayout, atLeastOnce()).setVisibility(View.GONE);
+ verify(mLayout).setVisibility(View.GONE);
// Show button.
doReturn(View.GONE).when(mLayout).getVisibility();
mWindowManager.updateVisibility(/* canShow= */ true);
verify(mWindowManager, never()).createLayout(anyBoolean());
- verify(mLayout, atLeastOnce()).setVisibility(View.VISIBLE);
+ verify(mLayout).setVisibility(View.VISIBLE);
}
@Test
@@ -453,12 +455,21 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
verify(mLayout).setCameraCompatHintVisibility(/* show= */ true);
}
+ @Test
+ public void testWhenDockedStateHasChanged_needsToBeRecreated() {
+ ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+ newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK;
+
+ Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
+ }
+
private static TaskInfo createTaskInfo(boolean hasSizeCompat,
@TaskInfo.CameraCompatControlState int cameraCompatControlState) {
ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = TASK_ID;
taskInfo.topActivityInSizeCompat = hasSizeCompat;
taskInfo.cameraCompatControlState = cameraCompatControlState;
+ taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
index 12ceb0a9a9ba..9200b3c90f0d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.compatui;
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.google.common.truth.Truth.assertThat;
@@ -371,6 +373,25 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase {
verify(mAnimationController).cancelAnimation();
}
+ @Test
+ public void testDeviceThemeChange_educationDialogUnseen_recreated() {
+ LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
+ ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+ newTaskInfo.configuration.uiMode |= UI_MODE_NIGHT_YES;
+
+ assertTrue(windowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
+ }
+
+ @Test
+ public void testDeviceThemeHasChanged_educationDialogSeen_notRecreated() {
+ LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
+ mCompatUIConfiguration.setSeenLetterboxEducation(USER_ID_1);
+ ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+ newTaskInfo.configuration.uiMode |= UI_MODE_NIGHT_YES;
+
+ assertFalse(windowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
+ }
+
private void verifyLayout(LetterboxEduDialogLayout layout, ViewGroup.LayoutParams params,
int expectedWidth, int expectedHeight, int expectedExtraTopMargin,
int expectedExtraBottomMargin) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
index 5bcc72e73cb9..973a99c269ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertNull;
import android.app.ActivityManager;
import android.app.TaskInfo;
+import android.content.res.Configuration;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
@@ -31,6 +32,8 @@ import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
+import junit.framework.Assert;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -46,63 +49,61 @@ import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class ReachabilityEduWindowManagerTest extends ShellTestCase {
-
- private static final int USER_ID = 1;
- private static final int TASK_ID = 1;
-
@Mock
private SyncTransactionQueue mSyncTransactionQueue;
@Mock
private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock
- private CompatUIController.CompatUICallback mCallback;
- @Mock
private CompatUIConfiguration mCompatUIConfiguration;
@Mock
private DisplayLayout mDisplayLayout;
-
private TestShellExecutor mExecutor;
+ private TaskInfo mTaskInfo;
+ private ReachabilityEduWindowManager mWindowManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mExecutor = new TestShellExecutor();
+ mTaskInfo = new ActivityManager.RunningTaskInfo();
+ mTaskInfo.configuration.uiMode =
+ (mTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK)
+ | Configuration.UI_MODE_NIGHT_NO;
+ mTaskInfo.configuration.uiMode =
+ (mTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_TYPE_MASK)
+ | Configuration.UI_MODE_TYPE_NORMAL;
+ mWindowManager = createReachabilityEduWindowManager(mTaskInfo);
}
@Test
public void testCreateLayout_notEligible_doesNotCreateLayout() {
- final ReachabilityEduWindowManager windowManager = createReachabilityEduWindowManager(
- createTaskInfo(/* userId= */ USER_ID, /*isLetterboxDoubleTapEnabled */ false));
-
- assertFalse(windowManager.createLayout(/* canShow= */ true));
+ assertFalse(mWindowManager.createLayout(/* canShow= */ true));
- assertNull(windowManager.mLayout);
+ assertNull(mWindowManager.mLayout);
}
- private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) {
- return new ReachabilityEduWindowManager(mContext, taskInfo, mSyncTransactionQueue,
- mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor);
+ @Test
+ public void testWhenDockedStateHasChanged_needsToBeRecreated() {
+ ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+ newTaskInfo.configuration.uiMode =
+ (newTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_TYPE_MASK)
+ | Configuration.UI_MODE_TYPE_DESK;
+
+ Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
}
- private static TaskInfo createTaskInfo(int userId, boolean isLetterboxDoubleTapEnabled) {
- return createTaskInfo(userId, /* isLetterboxDoubleTapEnabled */ isLetterboxDoubleTapEnabled,
- /* topActivityLetterboxVerticalPosition */ -1,
- /* topActivityLetterboxHorizontalPosition */ -1,
- /* topActivityLetterboxWidth */ -1,
- /* topActivityLetterboxHeight */ -1);
+ @Test
+ public void testWhenDarkLightThemeHasChanged_needsToBeRecreated() {
+ ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+ mTaskInfo.configuration.uiMode =
+ (mTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK)
+ | Configuration.UI_MODE_NIGHT_YES;
+
+ Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
}
- private static TaskInfo createTaskInfo(int userId, boolean isLetterboxDoubleTapEnabled,
- int topActivityLetterboxVerticalPosition, int topActivityLetterboxHorizontalPosition,
- int topActivityLetterboxWidth, int topActivityLetterboxHeight) {
- ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
- taskInfo.userId = userId;
- taskInfo.taskId = TASK_ID;
- taskInfo.isLetterboxDoubleTapEnabled = isLetterboxDoubleTapEnabled;
- taskInfo.topActivityLetterboxVerticalPosition = topActivityLetterboxVerticalPosition;
- taskInfo.topActivityLetterboxHorizontalPosition = topActivityLetterboxHorizontalPosition;
- taskInfo.topActivityLetterboxWidth = topActivityLetterboxWidth;
- taskInfo.topActivityLetterboxHeight = topActivityLetterboxHeight;
- return taskInfo;
+ private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) {
+ return new ReachabilityEduWindowManager(mContext, taskInfo, mSyncTransactionQueue,
+ mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor);
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
new file mode 100644
index 000000000000..9f109a1d0f50
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.compatui;
+
+import android.app.ActivityManager;
+import android.app.TaskInfo;
+import android.content.res.Configuration;
+import android.testing.AndroidTestingRunner;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.transition.Transitions;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link RestartDialogWindowManager}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:RestartDialogWindowManagerTest
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class RestartDialogWindowManagerTest extends ShellTestCase {
+
+ @Mock
+ private SyncTransactionQueue mSyncTransactionQueue;
+ @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
+ @Mock private CompatUIConfiguration mCompatUIConfiguration;
+ @Mock private Transitions mTransitions;
+ @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartCallback;
+ @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback;
+ private RestartDialogWindowManager mWindowManager;
+ private TaskInfo mTaskInfo;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTaskInfo = new ActivityManager.RunningTaskInfo();
+ mTaskInfo.configuration.uiMode =
+ (mTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK)
+ | Configuration.UI_MODE_NIGHT_NO;
+ mTaskInfo.configuration.uiMode =
+ (mTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_TYPE_MASK)
+ | Configuration.UI_MODE_TYPE_NORMAL;
+ mWindowManager = new RestartDialogWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
+ mTaskListener, new DisplayLayout(), mTransitions, mOnRestartCallback,
+ mOnDismissCallback, mCompatUIConfiguration);
+ }
+
+ @Test
+ public void testWhenDockedStateHasChanged_needsToBeRecreated() {
+ ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+ newTaskInfo.configuration.uiMode =
+ (newTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_TYPE_MASK)
+ | Configuration.UI_MODE_TYPE_DESK;
+
+ Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
+ }
+
+ @Test
+ public void testWhenDarkLightThemeHasChanged_needsToBeRecreated() {
+ ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+ mTaskInfo.configuration.uiMode =
+ (mTaskInfo.configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK)
+ | Configuration.UI_MODE_NIGHT_YES;
+
+ Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index 43f8f7b074bf..d6387ee5ae13 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_NONE;
@@ -41,6 +42,7 @@ import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.app.ActivityManager.RunningTaskInfo;
@@ -81,6 +83,8 @@ import java.util.Arrays;
@RunWith(AndroidTestingRunner.class)
public class DesktopModeControllerTest extends ShellTestCase {
+ private static final int SECOND_DISPLAY = 2;
+
@Mock
private ShellController mShellController;
@Mock
@@ -247,22 +251,22 @@ public class DesktopModeControllerTest extends ShellTestCase {
public void testShowDesktopApps_allAppsInvisible_bringsToFront() {
// Set up two active tasks on desktop, task2 is on top of task1.
RunningTaskInfo freeformTask1 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, freeformTask1.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask1.taskId);
mDesktopModeTaskRepository.updateVisibleFreeformTasks(
- freeformTask1.taskId, false /* visible */);
+ DEFAULT_DISPLAY, freeformTask1.taskId, false /* visible */);
RunningTaskInfo freeformTask2 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, freeformTask2.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask2.taskId);
mDesktopModeTaskRepository.updateVisibleFreeformTasks(
- freeformTask2.taskId, false /* visible */);
+ DEFAULT_DISPLAY, freeformTask2.taskId, false /* visible */);
when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn(
freeformTask1);
when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn(
freeformTask2);
// Run show desktop apps logic
- mController.showDesktopApps();
+ mController.showDesktopApps(DEFAULT_DISPLAY);
final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
// Check wct has reorder calls
@@ -282,17 +286,19 @@ public class DesktopModeControllerTest extends ShellTestCase {
@Test
public void testShowDesktopApps_appsAlreadyVisible_bringsToFront() {
final RunningTaskInfo task1 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task1.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task1.taskId,
+ true /* visible */);
when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1);
final RunningTaskInfo task2 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task2.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task2.taskId,
+ true /* visible */);
when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2);
- mController.showDesktopApps();
+ mController.showDesktopApps(DEFAULT_DISPLAY);
final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
// Check wct has reorder calls
@@ -311,17 +317,19 @@ public class DesktopModeControllerTest extends ShellTestCase {
@Test
public void testShowDesktopApps_someAppsInvisible_reordersAll() {
final RunningTaskInfo task1 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task1.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, false /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task1.taskId,
+ false /* visible */);
when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1);
final RunningTaskInfo task2 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task2.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task2.taskId,
+ true /* visible */);
when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2);
- mController.showDesktopApps();
+ mController.showDesktopApps(DEFAULT_DISPLAY);
final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
// Both tasks should be reordered to top, even if one was already visible.
@@ -335,38 +343,87 @@ public class DesktopModeControllerTest extends ShellTestCase {
}
@Test
+ public void testShowDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay() {
+ RunningTaskInfo taskDefaultDisplay = createFreeformTask(DEFAULT_DISPLAY);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, taskDefaultDisplay.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(taskDefaultDisplay.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(
+ DEFAULT_DISPLAY, taskDefaultDisplay.taskId, false /* visible */);
+ when(mShellTaskOrganizer.getRunningTaskInfo(taskDefaultDisplay.taskId)).thenReturn(
+ taskDefaultDisplay);
+
+ RunningTaskInfo taskSecondDisplay = createFreeformTask(SECOND_DISPLAY);
+ mDesktopModeTaskRepository.addActiveTask(SECOND_DISPLAY, taskSecondDisplay.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(taskSecondDisplay.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(
+ SECOND_DISPLAY, taskSecondDisplay.taskId, false /* visible */);
+ when(mShellTaskOrganizer.getRunningTaskInfo(taskSecondDisplay.taskId)).thenReturn(
+ taskSecondDisplay);
+
+ mController.showDesktopApps(DEFAULT_DISPLAY);
+
+ WindowContainerTransaction wct = getBringAppsToFrontTransaction();
+ assertThat(wct.getHierarchyOps()).hasSize(1);
+ HierarchyOp op = wct.getHierarchyOps().get(0);
+ assertThat(op.getContainer()).isEqualTo(taskDefaultDisplay.token.asBinder());
+ }
+
+ @Test
public void testGetVisibleTaskCount_noTasks_returnsZero() {
- assertThat(mController.getVisibleTaskCount()).isEqualTo(0);
+ assertThat(mController.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0);
}
@Test
public void testGetVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
RunningTaskInfo task1 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task1.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task1.taskId,
+ true /* visible */);
RunningTaskInfo task2 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task2.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task2.taskId,
+ true /* visible */);
- assertThat(mController.getVisibleTaskCount()).isEqualTo(2);
+ assertThat(mController.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2);
}
@Test
public void testGetVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
RunningTaskInfo task1 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task1.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task1.taskId,
+ true /* visible */);
RunningTaskInfo task2 = createFreeformTask();
- mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, task2.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
- mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, false /* visible */);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task2.taskId,
+ false /* visible */);
+
+ assertThat(mController.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1);
+ }
- assertThat(mController.getVisibleTaskCount()).isEqualTo(1);
+ @Test
+ public void testGetVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
+ RunningTaskInfo taskDefaultDisplay = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(DEFAULT_DISPLAY, taskDefaultDisplay.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(taskDefaultDisplay.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY,
+ taskDefaultDisplay.taskId,
+ true /* visible */);
+
+ RunningTaskInfo taskSecondDisplay = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(SECOND_DISPLAY, taskSecondDisplay.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(taskSecondDisplay.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(SECOND_DISPLAY,
+ taskSecondDisplay.taskId,
+ true /* visible */);
+
+ assertThat(mController.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1);
}
@Test
@@ -418,6 +475,17 @@ public class DesktopModeControllerTest extends ShellTestCase {
assertThat(wct).isNotNull();
}
+ @Test
+ public void testHandleTransitionRequest_taskOpen_doesNotStartAnotherTransition() {
+ RunningTaskInfo trigger = new RunningTaskInfo();
+ trigger.token = new MockToken().token();
+ trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ mController.handleRequest(
+ mock(IBinder.class),
+ new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */));
+ verifyZeroInteractions(mTransitions);
+ }
+
private DesktopModeController createController() {
return new DesktopModeController(mContext, mShellInit, mShellController,
mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 45cb3a062cc5..3bc2f0e8674e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -17,10 +17,12 @@
package com.android.wm.shell.desktopmode
import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,8 +43,8 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
val listener = TestListener()
repo.addActiveTaskListener(listener)
- repo.addActiveTask(1)
- assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1)
+ assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(1)
assertThat(repo.isActiveTask(1)).isTrue()
}
@@ -51,9 +53,9 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
val listener = TestListener()
repo.addActiveTaskListener(listener)
- repo.addActiveTask(1)
- repo.addActiveTask(1)
- assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1)
+ assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(1)
}
@Test
@@ -61,9 +63,22 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
val listener = TestListener()
repo.addActiveTaskListener(listener)
- repo.addActiveTask(1)
- repo.addActiveTask(2)
- assertThat(listener.activeTaskChangedCalls).isEqualTo(2)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 2)
+ assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2)
+ }
+
+ @Test
+ fun addActiveTask_multipleDisplays_notifiesCorrectListener() {
+ val listener = TestListener()
+ repo.addActiveTaskListener(listener)
+
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 2)
+ repo.addActiveTask(SECOND_DISPLAY, taskId = 3)
+
+ assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2)
+ assertThat(listener.activeChangesOnSecondaryDisplay).isEqualTo(1)
}
@Test
@@ -71,10 +86,10 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
val listener = TestListener()
repo.addActiveTaskListener(listener)
- repo.addActiveTask(1)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1)
repo.removeActiveTask(1)
// Notify once for add and once for remove
- assertThat(listener.activeTaskChangedCalls).isEqualTo(2)
+ assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2)
assertThat(repo.isActiveTask(1)).isFalse()
}
@@ -83,7 +98,17 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
val listener = TestListener()
repo.addActiveTaskListener(listener)
repo.removeActiveTask(99)
- assertThat(listener.activeTaskChangedCalls).isEqualTo(0)
+ assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(0)
+ }
+
+ @Test
+ fun remoteActiveTask_listenerForOtherDisplayNotNotified() {
+ val listener = TestListener()
+ repo.addActiveTaskListener(listener)
+ repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1)
+ repo.removeActiveTask(1)
+ assertThat(listener.activeChangesOnSecondaryDisplay).isEqualTo(0)
+ assertThat(repo.isActiveTask(1)).isFalse()
}
@Test
@@ -93,14 +118,27 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
@Test
fun addListener_notifiesVisibleFreeformTask() {
- repo.updateVisibleFreeformTasks(1, true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
repo.addVisibleTasksListener(listener, executor)
executor.flushAll()
- assertThat(listener.hasVisibleFreeformTasks).isTrue()
- assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1)
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ }
+
+ @Test
+ fun addListener_tasksOnDifferentDisplay_doesNotNotify() {
+ repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true)
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+ executor.flushAll()
+
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+ // One call as adding listener notifies it
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0)
}
@Test
@@ -108,13 +146,61 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
repo.addVisibleTasksListener(listener, executor)
- repo.updateVisibleFreeformTasks(1, true)
- repo.updateVisibleFreeformTasks(2, true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+ executor.flushAll()
+
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ }
+
+ @Test
+ fun updateVisibleFreeformTasks_addVisibleTaskNotifiesListenerForThatDisplay() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ executor.flushAll()
+
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isFalse()
+ assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0)
+
+ repo.updateVisibleFreeformTasks(displayId = 1, taskId = 2, visible = true)
+ executor.flushAll()
+
+ // Listener for secondary display is notified
+ assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
+ assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
+ // No changes to listener for default display
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ }
+
+ @Test
+ fun updateVisibleFreeformTasks_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
executor.flushAll()
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+
+ // Mark task 1 visible on secondary display
+ repo.updateVisibleFreeformTasks(displayId = 1, taskId = 1, visible = true)
+ executor.flushAll()
+
+ // Default display should have 2 calls
+ // 1 - visible task added
+ // 2 - visible task removed
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
- assertThat(listener.hasVisibleFreeformTasks).isTrue()
- // Equal to 2 because adding the listener notifies the current state
- assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+ // Secondary display should have 1 call for visible task added
+ assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
+ assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
}
@Test
@@ -122,52 +208,83 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
repo.addVisibleTasksListener(listener, executor)
- repo.updateVisibleFreeformTasks(1, true)
- repo.updateVisibleFreeformTasks(2, true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
executor.flushAll()
- assertThat(listener.hasVisibleFreeformTasks).isTrue()
- repo.updateVisibleFreeformTasks(1, false)
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
executor.flushAll()
- // Equal to 2 because adding the listener notifies the current state
- assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
- repo.updateVisibleFreeformTasks(2, false)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
executor.flushAll()
- assertThat(listener.hasVisibleFreeformTasks).isFalse()
- assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3)
+ assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
}
@Test
fun getVisibleTaskCount() {
// No tasks, count is 0
- assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
// New task increments count to 1
- repo.updateVisibleFreeformTasks(taskId = 1, visible = true)
- assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
// Visibility update to same task does not increase count
- repo.updateVisibleFreeformTasks(taskId = 1, visible = true)
- assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
// Second task visible increments count
- repo.updateVisibleFreeformTasks(taskId = 2, visible = true)
- assertThat(repo.getVisibleTaskCount()).isEqualTo(2)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
// Hiding a task decrements count
- repo.updateVisibleFreeformTasks(taskId = 1, visible = false)
- assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
// Hiding all tasks leaves count at 0
- repo.updateVisibleFreeformTasks(taskId = 2, visible = false)
- assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
+ assertThat(repo.getVisibleTaskCount(displayId = 9)).isEqualTo(0)
// Hiding a not existing task, count remains at 0
- repo.updateVisibleFreeformTasks(taskId = 999, visible = false)
- assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 999, visible = false)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+ }
+
+ @Test
+ fun getVisibleTaskCount_multipleDisplays() {
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+ assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
+
+ // New task on default display increments count for that display only
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+ assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
+
+ // New task on secondary display, increments count for that display only
+ repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 2, visible = true)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+ assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
+
+ // Marking task visible on another display, updates counts for both displays
+ repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+ assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
+
+ // Marking task that is on secondary display, hidden on default display, does not affect
+ // secondary display
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+ assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
+
+ // Hiding a task on that display, decrements count
+ repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = false)
+ assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+ assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
}
@Test
@@ -197,19 +314,40 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
}
class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
- var activeTaskChangedCalls = 0
- override fun onActiveTasksChanged() {
- activeTaskChangedCalls++
+ var activeChangesOnDefaultDisplay = 0
+ var activeChangesOnSecondaryDisplay = 0
+ override fun onActiveTasksChanged(displayId: Int) {
+ when (displayId) {
+ DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++
+ SECOND_DISPLAY -> activeChangesOnSecondaryDisplay++
+ else -> fail("Active task listener received unexpected display id: $displayId")
+ }
}
}
class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
- var hasVisibleFreeformTasks = false
- var visibleFreeformTaskChangedCalls = 0
-
- override fun onVisibilityChanged(hasVisibleTasks: Boolean) {
- hasVisibleFreeformTasks = hasVisibleTasks
- visibleFreeformTaskChangedCalls++
+ var hasVisibleTasksOnDefaultDisplay = false
+ var hasVisibleTasksOnSecondaryDisplay = false
+
+ var visibleChangesOnDefaultDisplay = 0
+ var visibleChangesOnSecondaryDisplay = 0
+
+ override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+ when (displayId) {
+ DEFAULT_DISPLAY -> {
+ hasVisibleTasksOnDefaultDisplay = hasVisibleFreeformTasks
+ visibleChangesOnDefaultDisplay++
+ }
+ SECOND_DISPLAY -> {
+ hasVisibleTasksOnSecondaryDisplay = hasVisibleFreeformTasks
+ visibleChangesOnSecondaryDisplay++
+ }
+ else -> fail("Visible task listener received unexpected display id: $displayId")
+ }
}
}
+
+ companion object {
+ const val SECOND_DISPLAY = 1
+ }
}
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 95e78a8b7bcc..1335ebf105a6 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
@@ -25,11 +25,13 @@ import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.os.Binder
import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.DisplayAreaInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
@@ -37,11 +39,14 @@ import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask
@@ -73,13 +78,18 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var shellController: ShellController
+ @Mock lateinit var displayController: DisplayController
@Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
+ @Mock lateinit var syncQueue: SyncTransactionQueue
+ @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock lateinit var transitions: Transitions
+ @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
+ @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
- lateinit var mockitoSession: StaticMockitoSession
- lateinit var controller: DesktopTasksController
- lateinit var shellInit: ShellInit
- lateinit var desktopModeTaskRepository: DesktopModeTaskRepository
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var controller: DesktopTasksController
+ private lateinit var shellInit: ShellInit
+ private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository
// Mock running tasks are registered here so we can get the list from mock shell task organizer
private val runningTasks = mutableListOf<RunningTaskInfo>()
@@ -105,8 +115,13 @@ class DesktopTasksControllerTest : ShellTestCase() {
context,
shellInit,
shellController,
+ displayController,
shellTaskOrganizer,
+ syncQueue,
+ rootTaskDisplayAreaOrganizer,
transitions,
+ enterDesktopTransitionHandler,
+ exitDesktopTransitionHandler,
desktopModeTaskRepository,
TestShellExecutor()
)
@@ -142,7 +157,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
markTaskHidden(task1)
markTaskHidden(task2)
- controller.showDesktopApps()
+ controller.showDesktopApps(DEFAULT_DISPLAY)
val wct = getLatestWct(expectTransition = TRANSIT_NONE)
assertThat(wct.hierarchyOps).hasSize(3)
@@ -160,7 +175,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
markTaskVisible(task1)
markTaskVisible(task2)
- controller.showDesktopApps()
+ controller.showDesktopApps(DEFAULT_DISPLAY)
val wct = getLatestWct(expectTransition = TRANSIT_NONE)
assertThat(wct.hierarchyOps).hasSize(3)
@@ -178,7 +193,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
markTaskHidden(task1)
markTaskVisible(task2)
- controller.showDesktopApps()
+ controller.showDesktopApps(DEFAULT_DISPLAY)
val wct = getLatestWct(expectTransition = TRANSIT_NONE)
assertThat(wct.hierarchyOps).hasSize(3)
@@ -192,7 +207,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
fun showDesktopApps_noActiveTasks_reorderHomeToTop() {
val homeTask = setUpHomeTask()
- controller.showDesktopApps()
+ controller.showDesktopApps(DEFAULT_DISPLAY)
val wct = getLatestWct(expectTransition = TRANSIT_NONE)
assertThat(wct.hierarchyOps).hasSize(1)
@@ -200,8 +215,26 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay() {
+ val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
+ val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
+ setUpHomeTask(SECOND_DISPLAY)
+ val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
+ markTaskHidden(taskDefaultDisplay)
+ markTaskHidden(taskSecondDisplay)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY)
+
+ val wct = getLatestWct(expectTransition = TRANSIT_NONE)
+ assertThat(wct.hierarchyOps).hasSize(2)
+ // Expect order to be from bottom: home, task
+ wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
+ wct.assertReorderAt(index = 1, taskDefaultDisplay)
+ }
+
+ @Test
fun getVisibleTaskCount_noTasks_returnsZero() {
- assertThat(controller.getVisibleTaskCount()).isEqualTo(0)
+ assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
}
@Test
@@ -209,7 +242,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
setUpHomeTask()
setUpFreeformTask().also(::markTaskVisible)
setUpFreeformTask().also(::markTaskVisible)
- assertThat(controller.getVisibleTaskCount()).isEqualTo(2)
+ assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
}
@Test
@@ -217,7 +250,15 @@ class DesktopTasksControllerTest : ShellTestCase() {
setUpHomeTask()
setUpFreeformTask().also(::markTaskVisible)
setUpFreeformTask().also(::markTaskHidden)
- assertThat(controller.getVisibleTaskCount()).isEqualTo(1)
+ assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+ }
+
+ @Test
+ fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
+ setUpHomeTask()
+ setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible)
+ setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible)
+ assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
}
@Test
@@ -245,6 +286,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller.moveToDesktop(fullscreenTask)
with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+ // Operations should include home task, freeform task
assertThat(hierarchyOps).hasSize(3)
assertReorderSequence(homeTask, freeformTask, fullscreenTask)
assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
@@ -253,6 +295,28 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() {
+ setUpHomeTask(displayId = DEFAULT_DISPLAY)
+ val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ markTaskHidden(freeformTaskDefault)
+
+ val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY)
+ val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
+ markTaskHidden(freeformTaskSecond)
+
+ controller.moveToDesktop(fullscreenTaskDefault)
+
+ with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+ // Check that hierarchy operations do not include tasks from second display
+ assertThat(hierarchyOps.map { it.container })
+ .doesNotContain(homeTaskSecond.token.asBinder())
+ assertThat(hierarchyOps.map { it.container })
+ .doesNotContain(freeformTaskSecond.token.asBinder())
+ }
+ }
+
+ @Test
fun moveToFullscreen() {
val task = setUpFreeformTask()
controller.moveToFullscreen(task)
@@ -268,6 +332,70 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() {
+ val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
+
+ controller.moveToFullscreen(taskDefaultDisplay)
+
+ with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+ assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
+ assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
+ }
+ }
+
+ @Test
+ fun moveToNextDisplay_noOtherDisplays() {
+ whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+ verifyWCTNotExecuted()
+ }
+
+ @Test
+ fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
+ // Set up two display ids
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: second display
+ val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
+ .thenReturn(secondDisplayArea)
+
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+ with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+ assertThat(hierarchyOps).hasSize(1)
+ assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
+ assertThat(hierarchyOps[0].isReparent).isTrue()
+ assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder())
+ assertThat(hierarchyOps[0].toTop).isTrue()
+ }
+ }
+
+ @Test
+ fun moveToNextDisplay_moveFromSecondToFirstDisplay() {
+ // Set up two display ids
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: default display
+ val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .thenReturn(defaultDisplayArea)
+
+ val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+
+ with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+ assertThat(hierarchyOps).hasSize(1)
+ assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
+ assertThat(hierarchyOps[0].isReparent).isTrue()
+ assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder())
+ assertThat(hierarchyOps[0].toTop).isTrue()
+ }
+ }
+
+ @Test
fun getTaskWindowingMode() {
val fullscreenTask = setUpFullscreenTask()
val freeformTask = setUpFreeformTask()
@@ -311,6 +439,18 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
+ createFreeformTask(displayId = SECOND_DISPLAY)
+
+ val result =
+ controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay))
+ assertThat(result).isNull()
+ }
+
+ @Test
fun handleRequest_freeformTask_freeformVisible_returnNull() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -349,6 +489,18 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_returnSwitchToFullscreenWCT() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
+ createFreeformTask(displayId = SECOND_DISPLAY)
+
+ val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
+ assertThat(result?.changes?.get(taskDefaultDisplay.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ }
+
+ @Test
fun handleRequest_notOpenOrToFrontTransition_returnNull() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -387,35 +539,43 @@ class DesktopTasksControllerTest : ShellTestCase() {
assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
}
- private fun setUpFreeformTask(): RunningTaskInfo {
- val task = createFreeformTask()
+ private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ val task = createFreeformTask(displayId)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
- desktopModeTaskRepository.addActiveTask(task.taskId)
+ desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
desktopModeTaskRepository.addOrMoveFreeformTaskToTop(task.taskId)
runningTasks.add(task)
return task
}
- private fun setUpHomeTask(): RunningTaskInfo {
- val task = createHomeTask()
+ private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ val task = createHomeTask(displayId)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
runningTasks.add(task)
return task
}
- private fun setUpFullscreenTask(): RunningTaskInfo {
- val task = createFullscreenTask()
+ private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ val task = createFullscreenTask(displayId)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
runningTasks.add(task)
return task
}
private fun markTaskVisible(task: RunningTaskInfo) {
- desktopModeTaskRepository.updateVisibleFreeformTasks(task.taskId, visible = true)
+ desktopModeTaskRepository.updateVisibleFreeformTasks(
+ task.displayId,
+ task.taskId,
+ visible = true
+ )
}
private fun markTaskHidden(task: RunningTaskInfo) {
- desktopModeTaskRepository.updateVisibleFreeformTasks(task.taskId, visible = false)
+ desktopModeTaskRepository.updateVisibleFreeformTasks(
+ task.displayId,
+ task.taskId,
+ visible = false
+ )
}
private fun getLatestWct(
@@ -444,6 +604,10 @@ class DesktopTasksControllerTest : ShellTestCase() {
): TransitionRequestInfo {
return TransitionRequestInfo(type, task, null /* remoteTransition */)
}
+
+ companion object {
+ const val SECOND_DISPLAY = 2
+ }
}
private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index dc91d756842e..cf1ff3214d87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -21,14 +21,17 @@ 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.view.Display.DEFAULT_DISPLAY
import com.android.wm.shell.TestRunningTaskInfoBuilder
class DesktopTestHelpers {
companion object {
/** Create a task that has windowing mode set to [WINDOWING_MODE_FREEFORM] */
@JvmStatic
- fun createFreeformTask(): RunningTaskInfo {
+ @JvmOverloads
+ fun createFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
+ .setDisplayId(displayId)
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
@@ -38,8 +41,10 @@ class DesktopTestHelpers {
/** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */
@JvmStatic
- fun createFullscreenTask(): RunningTaskInfo {
+ @JvmOverloads
+ fun createFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
+ .setDisplayId(displayId)
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
@@ -49,8 +54,10 @@ class DesktopTestHelpers {
/** Create a new home task */
@JvmStatic
- fun createHomeTask(): RunningTaskInfo {
+ @JvmOverloads
+ fun createHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
+ .setDisplayId(displayId)
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_HOME)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
new file mode 100644
index 000000000000..8592dea19289
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+
+import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IWindowContainerToken;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.transition.Transitions;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Supplier;
+
+/** Tests of {@link com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler} */
+@SmallTest
+public class EnterDesktopTaskTransitionHandlerTest {
+
+ @Mock
+ private Transitions mTransitions;
+ @Mock
+ IBinder mToken;
+ @Mock
+ Supplier<SurfaceControl.Transaction> mTransactionFactory;
+ @Mock
+ SurfaceControl.Transaction mStartT;
+ @Mock
+ SurfaceControl.Transaction mFinishT;
+ @Mock
+ SurfaceControl.Transaction mAnimationT;
+ @Mock
+ Transitions.TransitionFinishCallback mTransitionFinishCallback;
+ @Mock
+ ShellExecutor mExecutor;
+ @Mock
+ SurfaceControl mSurfaceControl;
+
+ private EnterDesktopTaskTransitionHandler mEnterDesktopTaskTransitionHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(mExecutor).when(mTransitions).getMainExecutor();
+ doReturn(mAnimationT).when(mTransactionFactory).get();
+
+ mEnterDesktopTaskTransitionHandler = new EnterDesktopTaskTransitionHandler(mTransitions,
+ mTransactionFactory);
+ }
+
+ @Test
+ public void testEnterFreeformAnimation() {
+ final int transitionType = Transitions.TRANSIT_ENTER_FREEFORM;
+ final int taskId = 1;
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ doReturn(mToken).when(mTransitions)
+ .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler);
+ mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct, null);
+
+ TransitionInfo.Change change =
+ createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
+ TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_FREEFORM, change);
+
+
+ assertTrue(mEnterDesktopTaskTransitionHandler
+ .startAnimation(mToken, info, mStartT, mFinishT, mTransitionFinishCallback));
+
+ verify(mStartT).setWindowCrop(mSurfaceControl, null);
+ verify(mStartT).apply();
+ }
+
+ @Test
+ public void testTransitEnterDesktopModeAnimation() throws Throwable {
+ final int transitionType = Transitions.TRANSIT_ENTER_DESKTOP_MODE;
+ final int taskId = 1;
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ doReturn(mToken).when(mTransitions)
+ .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler);
+ mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct, null);
+
+ TransitionInfo.Change change =
+ createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
+ change.setEndAbsBounds(new Rect(0, 0, 1, 1));
+ TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_DESKTOP_MODE, change);
+
+ runOnUiThread(() -> {
+ try {
+ assertTrue(mEnterDesktopTaskTransitionHandler
+ .startAnimation(mToken, info, mStartT, mFinishT,
+ mTransitionFinishCallback));
+ } catch (Exception e) {
+ throw new AssertionFailedError(e.getMessage());
+ }
+ });
+
+ verify(mStartT).setWindowCrop(mSurfaceControl, change.getEndAbsBounds().width(),
+ change.getEndAbsBounds().height());
+ verify(mStartT).apply();
+ }
+
+ private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId,
+ @WindowConfiguration.WindowingMode int windowingMode) {
+ final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.taskId = taskId;
+ taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+ final TransitionInfo.Change change = new TransitionInfo.Change(
+ new WindowContainerToken(mock(IWindowContainerToken.class)), mSurfaceControl);
+ change.setMode(type);
+ change.setTaskInfo(taskInfo);
+ return change;
+ }
+
+ private static TransitionInfo createTransitionInfo(
+ @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change) {
+ TransitionInfo info = new TransitionInfo(type, 0);
+ info.addChange(change);
+ return info;
+ }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
new file mode 100644
index 000000000000..0d0a08cb0ffb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.desktopmode;
+
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.IBinder;
+import android.util.DisplayMetrics;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IWindowContainerToken;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.function.Supplier;
+
+/** Tests of {@link com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler} */
+@SmallTest
+public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase {
+
+ @Mock
+ private Transitions mTransitions;
+ @Mock
+ IBinder mToken;
+ @Mock
+ Supplier<SurfaceControl.Transaction> mTransactionFactory;
+ @Mock
+ Context mContext;
+ @Mock
+ DisplayMetrics mDisplayMetrics;
+ @Mock
+ Resources mResources;
+ @Mock
+ Transitions.TransitionFinishCallback mTransitionFinishCallback;
+ @Mock
+ ShellExecutor mExecutor;
+
+ private Point mPoint;
+ private ExitDesktopTaskTransitionHandler mExitDesktopTaskTransitionHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(mExecutor).when(mTransitions).getMainExecutor();
+ doReturn(new SurfaceControl.Transaction()).when(mTransactionFactory).get();
+ doReturn(mResources).when(mContext).getResources();
+ doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics();
+ when(mResources.getDisplayMetrics())
+ .thenReturn(getContext().getResources().getDisplayMetrics());
+
+ mExitDesktopTaskTransitionHandler = new ExitDesktopTaskTransitionHandler(mTransitions,
+ mContext);
+ mPoint = new Point(0, 0);
+ }
+
+ @Test
+ public void testTransitExitDesktopModeAnimation() throws Throwable {
+ final int transitionType = Transitions.TRANSIT_EXIT_DESKTOP_MODE;
+ final int taskId = 1;
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ doReturn(mToken).when(mTransitions)
+ .startTransition(transitionType, wct, mExitDesktopTaskTransitionHandler);
+
+ mExitDesktopTaskTransitionHandler.startTransition(transitionType, wct, mPoint,
+ null);
+
+ TransitionInfo.Change change =
+ createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN);
+ TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_EXIT_DESKTOP_MODE, change);
+ ArrayList<Exception> exceptions = new ArrayList<>();
+ runOnUiThread(() -> {
+ try {
+ assertTrue(mExitDesktopTaskTransitionHandler
+ .startAnimation(mToken, info,
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mTransitionFinishCallback));
+ } catch (Exception e) {
+ exceptions.add(e);
+ }
+ });
+ if (!exceptions.isEmpty()) {
+ throw exceptions.get(0);
+ }
+ }
+
+ private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId,
+ @WindowConfiguration.WindowingMode int windowingMode) {
+ final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.taskId = taskId;
+ taskInfo.token = new WindowContainerToken(mock(IWindowContainerToken.class));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+ SurfaceControl.Builder b = new SurfaceControl.Builder()
+ .setName("test task");
+ final TransitionInfo.Change change = new TransitionInfo.Change(
+ taskInfo.token, b.build());
+ change.setMode(type);
+ change.setTaskInfo(taskInfo);
+ return change;
+ }
+
+ private static TransitionInfo createTransitionInfo(
+ @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change) {
+ TransitionInfo info = new TransitionInfo(type, 0);
+ info.addChange(change);
+ return info;
+ }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index 523cb6629d9a..54f36f61859d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -48,6 +48,7 @@ import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -71,6 +72,8 @@ public class DragAndDropControllerTest extends ShellTestCase {
@Mock
private ShellController mShellController;
@Mock
+ private ShellCommandHandler mShellCommandHandler;
+ @Mock
private DisplayController mDisplayController;
@Mock
private UiEventLogger mUiEventLogger;
@@ -89,7 +92,8 @@ public class DragAndDropControllerTest extends ShellTestCase {
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
mController = new DragAndDropController(mContext, mShellInit, mShellController,
- mDisplayController, mUiEventLogger, mIconProvider, mMainExecutor);
+ mShellCommandHandler, mDisplayController, mUiEventLogger, mIconProvider,
+ mMainExecutor);
mController.onInit();
}
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 9e988e8e8726..7c1da35888b8 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
@@ -211,7 +211,7 @@ public class DragAndDropPolicyTest extends ShellTestCase {
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
- verify(mSplitScreenStarter).startIntent(any(), any(),
+ verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
eq(SPLIT_POSITION_UNDEFINED), any());
}
@@ -223,12 +223,12 @@ public class DragAndDropPolicyTest extends ShellTestCase {
mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData);
- verify(mSplitScreenStarter).startIntent(any(), any(),
+ verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
eq(SPLIT_POSITION_TOP_OR_LEFT), any());
reset(mSplitScreenStarter);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
- verify(mSplitScreenStarter).startIntent(any(), any(),
+ verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
}
@@ -240,12 +240,12 @@ public class DragAndDropPolicyTest extends ShellTestCase {
mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData);
- verify(mSplitScreenStarter).startIntent(any(), any(),
+ verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
eq(SPLIT_POSITION_TOP_OR_LEFT), any());
reset(mSplitScreenStarter);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData);
- verify(mSplitScreenStarter).startIntent(any(), any(),
+ verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 48415d47304c..69f664a3a89d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -37,6 +37,7 @@ import android.window.WindowContainerToken;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.TransitionInfoBuilder;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -94,8 +95,8 @@ public class FreeformTaskTransitionObserverTest {
public void testCreatesWindowDecorOnOpenTransition_freeform() {
final TransitionInfo.Change change =
createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
- info.addChange(change);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(change).build();
final IBinder transition = mock(IBinder.class);
final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
@@ -111,8 +112,8 @@ public class FreeformTaskTransitionObserverTest {
public void testPreparesWindowDecorOnCloseTransition_freeform() {
final TransitionInfo.Change change =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
- info.addChange(change);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(change).build();
final IBinder transition = mock(IBinder.class);
final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
@@ -128,8 +129,8 @@ public class FreeformTaskTransitionObserverTest {
public void testDoesntCloseWindowDecorDuringCloseTransition() throws Exception {
final TransitionInfo.Change change =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
- info.addChange(change);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(change).build();
final IBinder transition = mock(IBinder.class);
final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
@@ -144,8 +145,8 @@ public class FreeformTaskTransitionObserverTest {
public void testClosesWindowDecorAfterCloseTransition() throws Exception {
final TransitionInfo.Change change =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
- info.addChange(change);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(change).build();
final AutoCloseable windowDecor = mock(AutoCloseable.class);
@@ -164,8 +165,8 @@ public class FreeformTaskTransitionObserverTest {
// The playing transition
final TransitionInfo.Change change1 =
createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info1 = new TransitionInfo(TRANSIT_OPEN, 0);
- info1.addChange(change1);
+ final TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(change1).build();
final IBinder transition1 = mock(IBinder.class);
final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
@@ -176,8 +177,8 @@ public class FreeformTaskTransitionObserverTest {
// The merged transition
final TransitionInfo.Change change2 =
createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0);
- info2.addChange(change2);
+ final TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(change2).build();
final IBinder transition2 = mock(IBinder.class);
final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
@@ -195,8 +196,8 @@ public class FreeformTaskTransitionObserverTest {
// The playing transition
final TransitionInfo.Change change1 =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info1 = new TransitionInfo(TRANSIT_CLOSE, 0);
- info1.addChange(change1);
+ final TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(change1).build();
final IBinder transition1 = mock(IBinder.class);
final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
@@ -207,8 +208,8 @@ public class FreeformTaskTransitionObserverTest {
// The merged transition
final TransitionInfo.Change change2 =
createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM);
- final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0);
- info2.addChange(change2);
+ final TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(change2).build();
final IBinder transition2 = mock(IBinder.class);
final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
deleted file mode 100644
index 58e91cb50c7a..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.kidsmode;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ParceledListSlice;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.view.InsetsState;
-import android.view.SurfaceControl;
-import android.window.ITaskOrganizerController;
-import android.window.TaskAppearedInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.sysui.ShellCommandHandler;
-import com.android.wm.shell.sysui.ShellInit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.Optional;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class KidsModeTaskOrganizerTest extends ShellTestCase {
- @Mock private ITaskOrganizerController mTaskOrganizerController;
- @Mock private Context mContext;
- @Mock private Handler mHandler;
- @Mock private SyncTransactionQueue mSyncTransactionQueue;
- @Mock private ShellExecutor mTestExecutor;
- @Mock private DisplayController mDisplayController;
- @Mock private SurfaceControl mLeash;
- @Mock private WindowContainerToken mToken;
- @Mock private WindowContainerTransaction mTransaction;
- @Mock private KidsModeSettingsObserver mObserver;
- @Mock private ShellInit mShellInit;
- @Mock private ShellCommandHandler mShellCommandHandler;
- @Mock private DisplayInsetsController mDisplayInsetsController;
- @Mock private Resources mResources;
-
- KidsModeTaskOrganizer mOrganizer;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- try {
- doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList())
- .when(mTaskOrganizerController).registerTaskOrganizer(any());
- } catch (RemoteException e) {
- }
- // NOTE: KidsModeTaskOrganizer should have a null CompatUIController.
- doReturn(mResources).when(mContext).getResources();
- final KidsModeTaskOrganizer kidsModeTaskOrganizer = new KidsModeTaskOrganizer(mContext,
- mShellInit, mShellCommandHandler, mTaskOrganizerController, mSyncTransactionQueue,
- mDisplayController, mDisplayInsetsController, Optional.empty(), Optional.empty(),
- mObserver, mTestExecutor, mHandler);
- mOrganizer = spy(kidsModeTaskOrganizer);
- doReturn(mTransaction).when(mOrganizer).getWindowContainerTransaction();
- doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY);
- }
-
- @Test
- public void instantiateController_addInitCallback() {
- verify(mShellInit, times(1)).addInitCallback(any(), any());
- }
-
- @Test
- public void testKidsModeOn() {
- doReturn(true).when(mObserver).isEnabled();
-
- mOrganizer.updateKidsModeState();
-
- verify(mOrganizer, times(1)).enable();
- verify(mOrganizer, times(1)).registerOrganizer();
- verify(mOrganizer, times(1)).createRootTask(
- eq(DEFAULT_DISPLAY), eq(WINDOWING_MODE_FULLSCREEN), eq(mOrganizer.mCookie));
- verify(mOrganizer, times(1))
- .setOrientationRequestPolicy(eq(true), any(), any());
-
- final ActivityManager.RunningTaskInfo rootTask = createTaskInfo(12,
- WINDOWING_MODE_FULLSCREEN, mOrganizer.mCookie);
- mOrganizer.onTaskAppeared(rootTask, mLeash);
-
- assertThat(mOrganizer.mLaunchRootLeash).isEqualTo(mLeash);
- assertThat(mOrganizer.mLaunchRootTask).isEqualTo(rootTask);
- }
-
- @Test
- public void testKidsModeOff() {
- doReturn(true).when(mObserver).isEnabled();
- mOrganizer.updateKidsModeState();
- final ActivityManager.RunningTaskInfo rootTask = createTaskInfo(12,
- WINDOWING_MODE_FULLSCREEN, mOrganizer.mCookie);
- mOrganizer.onTaskAppeared(rootTask, mLeash);
-
- doReturn(false).when(mObserver).isEnabled();
- mOrganizer.updateKidsModeState();
-
- verify(mOrganizer, times(1)).disable();
- verify(mOrganizer, times(1)).unregisterOrganizer();
- verify(mOrganizer, times(1)).deleteRootTask(rootTask.token);
- verify(mOrganizer, times(1))
- .setOrientationRequestPolicy(eq(false), any(), any());
- assertThat(mOrganizer.mLaunchRootLeash).isNull();
- assertThat(mOrganizer.mLaunchRootTask).isNull();
- }
-
- private ActivityManager.RunningTaskInfo createTaskInfo(
- int taskId, int windowingMode, IBinder cookies) {
- ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
- taskInfo.taskId = taskId;
- taskInfo.token = mToken;
- taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
- final ArrayList<IBinder> launchCookies = new ArrayList<>();
- if (cookies != null) {
- launchCookies.add(cookies);
- }
- taskInfo.launchCookies = launchCookies;
- return taskInfo;
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index ec264a643785..addc2338144f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -55,25 +55,28 @@ public class PipBoundsAlgorithmTest extends ShellTestCase {
private static final float MAX_ASPECT_RATIO = 2f;
private static final int DEFAULT_MIN_EDGE_SIZE = 100;
+ /** The minimum possible size of the override min size's width or height */
+ private static final int OVERRIDABLE_MIN_SIZE = 40;
+
private PipBoundsAlgorithm mPipBoundsAlgorithm;
private DisplayInfo mDefaultDisplayInfo;
- private PipBoundsState mPipBoundsState;
- private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipBoundsState mPipBoundsState; private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
@Before
public void setUp() throws Exception {
initializeMockResources();
- mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
- mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
+ mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState);
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {},
mPipSizeSpecHandler);
DisplayLayout layout =
new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true);
- mPipBoundsState.setDisplayLayout(layout);
- mPipSizeSpecHandler.setDisplayLayout(layout);
+ mPipDisplayLayoutState.setDisplayLayout(layout);
}
private void initializeMockResources() {
@@ -88,6 +91,9 @@ public class PipBoundsAlgorithmTest extends ShellTestCase {
R.dimen.default_minimal_size_pip_resizable_task,
DEFAULT_MIN_EDGE_SIZE);
res.addOverride(
+ R.dimen.overridable_minimal_size_pip_resizable_task,
+ OVERRIDABLE_MIN_SIZE);
+ res.addOverride(
R.string.config_defaultPictureInPictureScreenEdgeInsets,
"16x16");
res.addOverride(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
index 341a451eeb43..f32000445ca9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
@@ -27,11 +27,13 @@ import android.content.ComponentName;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.testing.TestableResources;
import android.util.Size;
import androidx.test.filters.SmallTest;
import com.android.internal.util.function.TriConsumer;
+import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
@@ -52,13 +54,23 @@ public class PipBoundsStateTest extends ShellTestCase {
private static final Size DEFAULT_SIZE = new Size(10, 10);
private static final float DEFAULT_SNAP_FRACTION = 1.0f;
+ /** The minimum possible size of the override min size's width or height */
+ private static final int OVERRIDABLE_MIN_SIZE = 40;
+
private PipBoundsState mPipBoundsState;
private ComponentName mTestComponentName1;
private ComponentName mTestComponentName2;
@Before
public void setUp() {
- mPipBoundsState = new PipBoundsState(mContext, new PipSizeSpecHandler(mContext));
+ final TestableResources res = mContext.getOrCreateTestableResources();
+ res.addOverride(
+ R.dimen.overridable_minimal_size_pip_resizable_task,
+ OVERRIDABLE_MIN_SIZE);
+
+ PipDisplayLayoutState pipDisplayLayoutState = new PipDisplayLayoutState(mContext);
+ mPipBoundsState = new PipBoundsState(mContext,
+ new PipSizeSpecHandler(mContext, pipDisplayLayoutState), pipDisplayLayoutState);
mTestComponentName1 = new ComponentName(mContext, "component1");
mTestComponentName2 = new ComponentName(mContext, "component2");
}
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 e907cd3ca0ad..842c699fa42d 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
@@ -88,6 +88,7 @@ public class PipTaskOrganizerTest extends ShellTestCase {
private PipTransitionState mPipTransitionState;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
private ComponentName mComponent1;
private ComponentName mComponent2;
@@ -97,15 +98,16 @@ public class PipTaskOrganizerTest extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mComponent1 = new ComponentName(mContext, "component1");
mComponent2 = new ComponentName(mContext, "component2");
- mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
- mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
+ mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState);
mPipTransitionState = new PipTransitionState();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {},
mPipSizeSpecHandler);
mMainExecutor = new TestShellExecutor();
mPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockSyncTransactionQueue,
- mPipTransitionState, mPipBoundsState, mPipSizeSpecHandler,
+ mPipTransitionState, mPipBoundsState, mPipDisplayLayoutState,
mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController,
mMockPipSurfaceTransactionHelper, mMockPipTransitionController,
mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController,
@@ -259,9 +261,9 @@ public class PipTaskOrganizerTest extends ShellTestCase {
final DisplayInfo info = new DisplayInfo();
DisplayLayout layout = new DisplayLayout(info,
mContext.getResources(), true, true);
- mPipBoundsState.setDisplayLayout(layout);
- mPipSizeSpecHandler.setDisplayLayout(layout);
- mPipTaskOrganizer.setOneShotAnimationType(PipAnimationController.ANIM_TYPE_ALPHA);
+ mPipDisplayLayoutState.setDisplayLayout(layout);
+ doReturn(PipAnimationController.ANIM_TYPE_ALPHA).when(mMockPipAnimationController)
+ .takeOneShotEnterAnimationType();
mPipTaskOrganizer.setSurfaceControlTransactionFactory(
MockSurfaceControlHelper::createMockSurfaceControlTransaction);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
index 4d7e9e450ceb..cc9e26b2c4f1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java
@@ -18,6 +18,9 @@ package com.android.wm.shell.pip.phone;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.when;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
@@ -26,10 +29,13 @@ import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.pip.PipBoundsState;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import java.util.Set;
@@ -42,6 +48,10 @@ import java.util.Set;
public class PhonePipKeepClearAlgorithmTest extends ShellTestCase {
private PhonePipKeepClearAlgorithm mPipKeepClearAlgorithm;
+
+ @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm;
+ @Mock private PipBoundsState mMockPipBoundsState;
+
private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000);
@Before
@@ -73,7 +83,6 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase {
@Test
public void findUnoccludedPosition_withCollidingUnrestrictedKeepClearArea_moveBounds() {
- // TODO(b/183746978): update this test to accommodate for the updated algorithm
final Rect inBounds = new Rect(0, 0, 100, 100);
final Rect keepClearRect = new Rect(50, 50, 150, 150);
@@ -93,4 +102,202 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase {
assertEquals(inBounds, outBounds);
}
+
+ @Test
+ public void adjust_withCollidingRestrictedKeepClearArea_moveBounds() {
+ final Rect pipBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(50, 50, 150, 150);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect));
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertFalse(outBounds.contains(keepClearRect));
+ }
+
+ @Test
+ public void adjust_withNonCollidingRestrictedKeepClearArea_boundsUnchanged() {
+ final Rect pipBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(100, 100, 150, 150);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect));
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertFalse(outBounds.contains(keepClearRect));
+ }
+
+ @Test
+ public void adjust_withCollidingRestrictedKeepClearArea_whileStashed_boundsUnchanged() {
+ final Rect pipBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(50, 50, 150, 150);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ when(mMockPipBoundsState.isStashed()).thenReturn(true);
+ when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect));
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(pipBounds, outBounds);
+ }
+
+ @Test
+ public void adjust_withNonCollidingRestrictedKeepClearArea_whileStashed_boundsUnchanged() {
+ final Rect pipBounds = new Rect(0, 0, 100, 100);
+ final Rect keepClearRect = new Rect(100, 100, 150, 150);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ when(mMockPipBoundsState.isStashed()).thenReturn(true);
+ when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect));
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(pipBounds, outBounds);
+ }
+
+ @Test
+ public void adjust_aboveDisplayBounds_onLeftEdge_appliesBottomLeftGravity() {
+ final Rect pipBounds = new Rect(
+ 0, DISPLAY_BOUNDS.top - 50, 100, DISPLAY_BOUNDS.top + 50);
+ final Rect expected = new Rect(
+ 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+ when(mMockPipBoundsAlgorithm.getSnapFraction(any(Rect.class))).thenReturn(0f);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(expected, outBounds);
+ }
+
+ @Test
+ public void adjust_belowDisplayBounds_onLeftEdge_appliesBottomLeftGravity() {
+ final Rect pipBounds = new Rect(
+ 0, DISPLAY_BOUNDS.bottom - 50, 100, DISPLAY_BOUNDS.bottom + 50);
+ final Rect expected = new Rect(
+ 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+ when(mMockPipBoundsAlgorithm.getSnapFraction(any(Rect.class))).thenReturn(3f);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(expected, outBounds);
+ }
+
+ @Test
+ public void adjust_aboveDisplayBounds_onRightEdge_appliesBottomRightGravity() {
+ final Rect pipBounds = new Rect(
+ DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.top - 50,
+ DISPLAY_BOUNDS.right, DISPLAY_BOUNDS.top + 50);
+ final Rect expected = new Rect(
+ DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.bottom - 100,
+ DISPLAY_BOUNDS.right, DISPLAY_BOUNDS.bottom);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+ when(mMockPipBoundsAlgorithm.getSnapFraction(any(Rect.class))).thenReturn(1f);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(expected, outBounds);
+ }
+
+ @Test
+ public void adjust_belowDisplayBounds_onRightEdge_appliesBottomRightGravity() {
+ final Rect pipBounds = new Rect(
+ DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.bottom - 50,
+ DISPLAY_BOUNDS.right, DISPLAY_BOUNDS.bottom + 50);
+ final Rect expected = new Rect(
+ DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.bottom - 100,
+ DISPLAY_BOUNDS.right, DISPLAY_BOUNDS.bottom);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+ when(mMockPipBoundsAlgorithm.getSnapFraction(any(Rect.class))).thenReturn(2f);
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(expected, outBounds);
+ }
+
+ @Test
+ public void adjust_whileStashed_aboveDisplayBounds_alignsToBottomInset() {
+ final Rect pipBounds = new Rect(
+ 0, DISPLAY_BOUNDS.top - 50, 100, DISPLAY_BOUNDS.top + 50);
+ final Rect expected = new Rect(
+ 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ when(mMockPipBoundsState.isStashed()).thenReturn(true);
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(expected, outBounds);
+ }
+
+ @Test
+ public void adjust_whileStashed_belowDisplayBounds_alignsToBottomInset() {
+ final Rect pipBounds = new Rect(
+ 0, DISPLAY_BOUNDS.bottom - 50, 100, DISPLAY_BOUNDS.bottom + 50);
+ final Rect expected = new Rect(
+ 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom);
+ when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds);
+ when(mMockPipBoundsState.isStashed()).thenReturn(true);
+ doAnswer(invocation -> {
+ Rect arg0 = invocation.getArgument(0);
+ arg0.set(DISPLAY_BOUNDS);
+ return null;
+ }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class));
+
+ final Rect outBounds = mPipKeepClearAlgorithm.adjust(
+ mMockPipBoundsState, mMockPipBoundsAlgorithm);
+
+ assertEquals(expected, outBounds);
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index a41a30e73baa..85167cb97501 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -60,6 +60,7 @@ import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -109,6 +110,7 @@ public class PipControllerTest extends ShellTestCase {
@Mock private WindowManagerShellWrapper mMockWindowManagerShellWrapper;
@Mock private PipBoundsState mMockPipBoundsState;
@Mock private PipSizeSpecHandler mMockPipSizeSpecHandler;
+ @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState;
@Mock private TaskStackListenerImpl mMockTaskStackListener;
@Mock private ShellExecutor mMockExecutor;
@Mock private Optional<OneHandedController> mMockOneHandedController;
@@ -127,17 +129,18 @@ public class PipControllerTest extends ShellTestCase {
return null;
}).when(mMockExecutor).execute(any());
mShellInit = spy(new ShellInit(mMockExecutor));
- mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler,
+ mShellController = spy(new ShellController(mContext, mShellInit, mMockShellCommandHandler,
mMockExecutor));
mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
mShellController, mMockDisplayController, mMockPipAnimationController,
mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
- mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper,
- mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer,
- mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController,
- mMockWindowManagerShellWrapper, mMockTaskStackListener,
- mMockPipParamsChangedForwarder, mMockDisplayInsetsController,
- mMockTabletopModeController, mMockOneHandedController, mMockExecutor);
+ mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipDisplayLayoutState,
+ mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController,
+ mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler,
+ mMockPipTransitionController, mMockWindowManagerShellWrapper,
+ mMockTaskStackListener, mMockPipParamsChangedForwarder,
+ mMockDisplayInsetsController, mMockTabletopModeController,
+ mMockOneHandedController, mMockExecutor);
mShellInit.init();
when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
@@ -223,12 +226,13 @@ public class PipControllerTest extends ShellTestCase {
assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
mShellController, mMockDisplayController, mMockPipAnimationController,
mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
- mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipMotionHelper,
- mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer,
- mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController,
- mMockWindowManagerShellWrapper, mMockTaskStackListener,
- mMockPipParamsChangedForwarder, mMockDisplayInsetsController,
- mMockTabletopModeController, mMockOneHandedController, mMockExecutor));
+ mMockPipBoundsState, mMockPipSizeSpecHandler, mMockPipDisplayLayoutState,
+ mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController,
+ mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler,
+ mMockPipTransitionController, mMockWindowManagerShellWrapper,
+ mMockTaskStackListener, mMockPipParamsChangedForwarder,
+ mMockDisplayInsetsController, mMockTabletopModeController,
+ mMockOneHandedController, mMockExecutor));
}
@Test
@@ -264,7 +268,7 @@ public class PipControllerTest extends ShellTestCase {
}
@Test
- public void saveReentryState_userHasResized_savesSize() {
+ public void saveReentryState_nonEmptyUserResizeBounds_savesSize() {
final Rect bounds = new Rect(0, 0, 10, 10);
final Rect resizedBounds = new Rect(0, 0, 30, 30);
when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f);
@@ -277,6 +281,19 @@ public class PipControllerTest extends ShellTestCase {
}
@Test
+ public void saveReentryState_emptyUserResizeBounds_savesSize() {
+ final Rect bounds = new Rect(0, 0, 10, 10);
+ final Rect resizedBounds = new Rect(0, 0, 0, 0);
+ when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f);
+ when(mMockPipTouchHandler.getUserResizeBounds()).thenReturn(resizedBounds);
+ when(mMockPipBoundsState.hasUserResizedPip()).thenReturn(true);
+
+ mPipController.saveReentryState(bounds);
+
+ verify(mMockPipBoundsState).saveReentryState(new Size(10, 10), 1.0f);
+ }
+
+ @Test
public void onDisplayConfigurationChanged_inPip_movePip() {
final int displayId = 1;
final Rect bounds = new Rect(0, 0, 10, 10);
@@ -285,8 +302,8 @@ public class PipControllerTest extends ShellTestCase {
when(mMockPipBoundsState.getMinSize()).thenReturn(new Point(1, 1));
when(mMockPipBoundsState.getMaxSize()).thenReturn(new Point(MAX_VALUE, MAX_VALUE));
when(mMockPipBoundsState.getBounds()).thenReturn(bounds);
- when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId);
- when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1);
+ when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId);
+ when(mMockPipDisplayLayoutState.getDisplayLayout()).thenReturn(mMockDisplayLayout1);
when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2);
when(mMockPipTaskOrganizer.isInPip()).thenReturn(true);
@@ -301,8 +318,8 @@ public class PipControllerTest extends ShellTestCase {
final int displayId = 1;
final Rect bounds = new Rect(0, 0, 10, 10);
when(mMockPipBoundsAlgorithm.getDefaultBounds()).thenReturn(bounds);
- when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId);
- when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1);
+ when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId);
+ when(mMockPipDisplayLayoutState.getDisplayLayout()).thenReturn(mMockDisplayLayout1);
when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2);
when(mMockPipTaskOrganizer.isInPip()).thenReturn(false);
@@ -317,7 +334,7 @@ public class PipControllerTest extends ShellTestCase {
mPipController.setEnablePipKeepClearAlgorithm(false);
final int displayId = 1;
final Rect keepClearArea = new Rect(0, 0, 10, 10);
- when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId);
+ when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId);
mPipController.mDisplaysChangedListener.onKeepClearAreasChanged(
displayId, Set.of(keepClearArea), Set.of());
@@ -330,7 +347,7 @@ public class PipControllerTest extends ShellTestCase {
mPipController.setEnablePipKeepClearAlgorithm(true);
final int displayId = 1;
final Rect keepClearArea = new Rect(0, 0, 10, 10);
- when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId);
+ when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(displayId);
mPipController.mDisplaysChangedListener.onKeepClearAreasChanged(
displayId, Set.of(keepClearArea), Set.of());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index 5f356c93d0a4..1dfdbf6514ba 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -29,6 +29,7 @@ import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
+import android.view.ViewConfiguration;
import androidx.test.filters.SmallTest;
@@ -37,6 +38,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
@@ -87,11 +89,16 @@ public class PipResizeGestureHandlerTest extends ShellTestCase {
private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
+
+ private PipTouchState mPipTouchState;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
- mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
+ mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState);
final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm();
final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm =
new PipKeepClearAlgorithmInterface() {};
@@ -100,8 +107,12 @@ public class PipResizeGestureHandlerTest extends ShellTestCase {
final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
mMockPipTransitionController, mFloatingContentCoordinator);
+
+ mPipTouchState = new PipTouchState(ViewConfiguration.get(mContext),
+ () -> {}, () -> {}, mMainExecutor);
mPipResizeGestureHandler = new PipResizeGestureHandler(mContext, pipBoundsAlgorithm,
- mPipBoundsState, motionHelper, mPipTaskOrganizer, mPipDismissTargetHandler,
+ mPipBoundsState, motionHelper, mPipTouchState, mPipTaskOrganizer,
+ mPipDismissTargetHandler,
(Rect bounds) -> new Rect(), () -> {}, mPipUiEventLogger, mPhonePipMenuController,
mMainExecutor) {
@Override
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java
index d9ff7d1f1089..1379aedc2284 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipSizeSpecHandlerTest.java
@@ -18,7 +18,6 @@ package com.android.wm.shell.pip.phone;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@@ -33,6 +32,7 @@ import android.view.DisplayInfo;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import org.junit.After;
import org.junit.Assert;
@@ -74,7 +74,8 @@ public class PipSizeSpecHandlerTest extends ShellTestCase {
@Mock private Context mContext;
@Mock private Resources mResources;
- private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
+ private TestPipSizeSpecHandler mPipSizeSpecHandler;
/**
* Sets up static Mockito session for SystemProperties and mocks necessary static methods.
@@ -82,8 +83,6 @@ public class PipSizeSpecHandlerTest extends ShellTestCase {
private static void setUpStaticSystemPropertiesSession() {
sStaticMockitoSession = mockitoSession()
.mockStatic(SystemProperties.class).startMocking();
- // make sure the feature flag is on
- when(SystemProperties.getBoolean(anyString(), anyBoolean())).thenReturn(true);
when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> {
String property = invocation.getArgument(0);
if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) {
@@ -107,21 +106,21 @@ public class PipSizeSpecHandlerTest extends ShellTestCase {
sExpectedDefaultSizes = new HashMap<>();
sExpectedMinSizes = new HashMap<>();
- sExpectedMaxSizes.put(16f / 9, new Size(1000, 562));
- sExpectedDefaultSizes.put(16f / 9, new Size(600, 337));
- sExpectedMinSizes.put(16f / 9, new Size(499, 281));
+ sExpectedMaxSizes.put(16f / 9, new Size(1000, 563));
+ sExpectedDefaultSizes.put(16f / 9, new Size(600, 338));
+ sExpectedMinSizes.put(16f / 9, new Size(501, 282));
sExpectedMaxSizes.put(4f / 3, new Size(892, 669));
sExpectedDefaultSizes.put(4f / 3, new Size(535, 401));
- sExpectedMinSizes.put(4f / 3, new Size(445, 334));
+ sExpectedMinSizes.put(4f / 3, new Size(447, 335));
sExpectedMaxSizes.put(3f / 4, new Size(669, 892));
sExpectedDefaultSizes.put(3f / 4, new Size(401, 535));
- sExpectedMinSizes.put(3f / 4, new Size(334, 445));
+ sExpectedMinSizes.put(3f / 4, new Size(335, 447));
sExpectedMaxSizes.put(9f / 16, new Size(562, 999));
sExpectedDefaultSizes.put(9f / 16, new Size(337, 599));
- sExpectedMinSizes.put(9f / 16, new Size(281, 499));
+ sExpectedMinSizes.put(9f / 16, new Size(281, 500));
}
private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes,
@@ -137,7 +136,6 @@ public class PipSizeSpecHandlerTest extends ShellTestCase {
@Before
public void setUp() {
initExpectedSizes();
- setUpStaticSystemPropertiesSession();
when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE);
when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO);
@@ -148,11 +146,6 @@ public class PipSizeSpecHandlerTest extends ShellTestCase {
// set up the mock context for spec handler specifically
when(mContext.getResources()).thenReturn(mResources);
- mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
-
- // no overridden min edge size by default
- mPipSizeSpecHandler.setOverrideMinSize(null);
-
DisplayInfo displayInfo = new DisplayInfo();
displayInfo.logicalWidth = DISPLAY_EDGE_SIZE;
displayInfo.logicalHeight = DISPLAY_EDGE_SIZE;
@@ -161,7 +154,14 @@ public class PipSizeSpecHandlerTest extends ShellTestCase {
// this is done to avoid unnecessary mocking while allowing for custom display dimensions
DisplayLayout displayLayout = new DisplayLayout(displayInfo, getContext().getResources(),
false, false);
- mPipSizeSpecHandler.setDisplayLayout(displayLayout);
+ mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
+ mPipDisplayLayoutState.setDisplayLayout(displayLayout);
+
+ setUpStaticSystemPropertiesSession();
+ mPipSizeSpecHandler = new TestPipSizeSpecHandler(mContext, mPipDisplayLayoutState);
+
+ // no overridden min edge size by default
+ mPipSizeSpecHandler.setOverrideMinSize(null);
}
@After
@@ -211,4 +211,16 @@ public class PipSizeSpecHandlerTest extends ShellTestCase {
Assert.assertEquals(expectedSize, actualSize);
}
+
+ static class TestPipSizeSpecHandler extends PipSizeSpecHandler {
+
+ TestPipSizeSpecHandler(Context context, PipDisplayLayoutState displayLayoutState) {
+ super(context, displayLayoutState);
+ }
+
+ @Override
+ boolean supportsPipSizeLargeScreen() {
+ return true;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 1515d6057baf..10b1ddf1b868 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -35,6 +35,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
@@ -92,6 +93,7 @@ public class PipTouchHandlerTest extends ShellTestCase {
private PipMotionHelper mMotionHelper;
private PipResizeGestureHandler mPipResizeGestureHandler;
private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
private DisplayLayout mDisplayLayout;
private Rect mInsetBounds;
@@ -105,8 +107,9 @@ public class PipTouchHandlerTest extends ShellTestCase {
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mPipSizeSpecHandler = new PipSizeSpecHandler(mContext);
- mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler);
+ mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState);
+ mPipBoundsState = new PipBoundsState(mContext, mPipSizeSpecHandler, mPipDisplayLayoutState);
mPipSnapAlgorithm = new PipSnapAlgorithm();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
new PipKeepClearAlgorithmInterface() {}, mPipSizeSpecHandler);
@@ -124,8 +127,7 @@ public class PipTouchHandlerTest extends ShellTestCase {
mPipTouchHandler.setPipResizeGestureHandler(mPipResizeGestureHandler);
mDisplayLayout = new DisplayLayout(mContext, mContext.getDisplay());
- mPipBoundsState.setDisplayLayout(mDisplayLayout);
- mPipSizeSpecHandler.setDisplayLayout(mDisplayLayout);
+ mPipDisplayLayoutState.setDisplayLayout(mDisplayLayout);
mInsetBounds = new Rect(mPipBoundsState.getDisplayBounds().left + INSET,
mPipBoundsState.getDisplayBounds().top + INSET,
mPipBoundsState.getDisplayBounds().right - INSET,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS
new file mode 100644
index 000000000000..736d4cff6ce8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS
@@ -0,0 +1,3 @@
+# WM shell sub-module TV pip owners
+galinap@google.com
+bronger@google.com \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
new file mode 100644
index 000000000000..02e6b8c71663
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
@@ -0,0 +1,382 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.graphics.drawable.Icon;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Log;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.PipMediaController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link TvPipActionsProvider}
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class TvPipActionProviderTest extends ShellTestCase {
+ private static final String TAG = TvPipActionProviderTest.class.getSimpleName();
+ private TvPipActionsProvider mActionsProvider;
+
+ @Mock
+ private PipMediaController mMockPipMediaController;
+ @Mock
+ private TvPipActionsProvider.Listener mMockListener;
+ @Mock
+ private TvPipAction.SystemActionsHandler mMockSystemActionsHandler;
+ @Mock
+ private Icon mMockIcon;
+ @Mock
+ private PendingIntent mMockPendingIntent;
+
+ private RemoteAction createRemoteAction(int identifier) {
+ return new RemoteAction(mMockIcon, "" + identifier, "" + identifier, mMockPendingIntent);
+ }
+
+ private List<RemoteAction> createRemoteActions(int numberOfActions) {
+ List<RemoteAction> actions = new ArrayList<>();
+ for (int i = 0; i < numberOfActions; i++) {
+ actions.add(createRemoteAction(i));
+ }
+ return actions;
+ }
+
+ private boolean checkActionsMatch(List<TvPipAction> actions, int[] actionTypes) {
+ for (int i = 0; i < actions.size(); i++) {
+ int type = actions.get(i).getActionType();
+ if (type != actionTypes[i]) {
+ Log.e(TAG, "Action at index " + i + ": found " + type
+ + ", expected " + actionTypes[i]);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Before
+ public void setUp() {
+ if (!isTelevision()) {
+ return;
+ }
+ MockitoAnnotations.initMocks(this);
+ mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController,
+ mMockSystemActionsHandler);
+ }
+
+ @Test
+ public void defaultSystemActions_regularPip() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE}));
+ }
+
+ @Test
+ public void defaultSystemActions_expandedPip() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(true);
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+ }
+
+ @Test
+ public void expandedPip_enableExpansion_enable() {
+ assumeTelevision();
+ // PiP has expanded PiP disabled.
+ mActionsProvider.updateExpansionEnabled(false);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.updateExpansionEnabled(true);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+ verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 0, /* index= */ 3);
+ }
+
+ @Test
+ public void expandedPip_enableExpansion_disable() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(true);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.updateExpansionEnabled(false);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 0, /* index= */ 3);
+ }
+
+ @Test
+ public void expandedPip_enableExpansion_AlreadyEnabled() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(true);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.updateExpansionEnabled(true);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+ }
+
+ private void check_expandedPip_updateExpansionState(
+ boolean startExpansion, boolean endExpansion, boolean updateExpected) {
+
+ mActionsProvider.updateExpansionEnabled(true);
+ mActionsProvider.updatePipExpansionState(startExpansion);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.updatePipExpansionState(endExpansion);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+
+ if (updateExpected) {
+ verify(mMockListener).onActionsChanged(0, 1, 3);
+ } else {
+ verify(mMockListener, times(0))
+ .onActionsChanged(anyInt(), anyInt(), anyInt());
+ }
+ }
+
+ @Test
+ public void expandedPip_toggleExpansion_collapse() {
+ assumeTelevision();
+ check_expandedPip_updateExpansionState(
+ /* startExpansion= */ true,
+ /* endExpansion= */ false,
+ /* updateExpected= */ true);
+ }
+
+ @Test
+ public void expandedPip_toggleExpansion_expand() {
+ assumeTelevision();
+ check_expandedPip_updateExpansionState(
+ /* startExpansion= */ false,
+ /* endExpansion= */ true,
+ /* updateExpected= */ true);
+ }
+
+ @Test
+ public void expandedPiP_updateExpansionState_alreadyExpanded() {
+ assumeTelevision();
+ check_expandedPip_updateExpansionState(
+ /* startExpansion= */ true,
+ /* endExpansion= */ true,
+ /* updateExpected= */ false);
+ }
+
+ @Test
+ public void expandedPiP_updateExpansionState_alreadyCollapsed() {
+ assumeTelevision();
+ check_expandedPip_updateExpansionState(
+ /* startExpansion= */ false,
+ /* endExpansion= */ false,
+ /* updateExpected= */ false);
+ }
+
+ @Test
+ public void regularPiP_updateExpansionState_setCollapsed() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ mActionsProvider.updatePipExpansionState(/* expanded= */ false);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.updatePipExpansionState(/* expanded= */ false);
+
+ verify(mMockListener, times(0))
+ .onActionsChanged(anyInt(), anyInt(), anyInt());
+ }
+
+ @Test
+ public void customActions_added() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ mActionsProvider.addListener(mMockListener);
+
+ mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+ ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2);
+ }
+
+ @Test
+ public void customActions_replacedMore() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.setAppActions(createRemoteActions(3), null);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+ ACTION_CUSTOM, ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 2, /* index= */ 2);
+ }
+
+ @Test
+ public void customActions_replacedLess() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.setAppActions(createRemoteActions(0), null);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ -2, /* updated= */ 0, /* index= */ 2);
+ }
+
+ @Test
+ public void customCloseAdded() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+
+ List<RemoteAction> customActions = new ArrayList<>();
+ mActionsProvider.setAppActions(customActions, null);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.setAppActions(customActions, createRemoteAction(0));
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1);
+ }
+
+ @Test
+ public void customClose_matchesOtherCustomAction() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+
+ List<RemoteAction> customActions = createRemoteActions(2);
+ RemoteAction customClose = createRemoteAction(/* id= */ 10);
+ customActions.add(customClose);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.setAppActions(customActions, customClose);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+ ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1);
+ verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2);
+ }
+
+ @Test
+ public void mediaActions_added_whileCustomActionsExist() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.onMediaActionsChanged(createRemoteActions(3));
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+ ACTION_MOVE}));
+ verify(mMockListener, times(0)).onActionsChanged(anyInt(), anyInt(), anyInt());
+ }
+
+ @Test
+ public void customActions_removed_whileMediaActionsExist() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ mActionsProvider.onMediaActionsChanged(createRemoteActions(2));
+ mActionsProvider.setAppActions(createRemoteActions(3), null);
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.setAppActions(createRemoteActions(0), null);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+ ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 2, /* index= */ 2);
+ }
+
+ @Test
+ public void customCloseOnly_mediaActionsShowing() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+ mActionsProvider.onMediaActionsChanged(createRemoteActions(2));
+
+ mActionsProvider.addListener(mMockListener);
+ mActionsProvider.setAppActions(createRemoteActions(0), createRemoteAction(5));
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+ ACTION_MOVE}));
+ verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1);
+ }
+
+ @Test
+ public void customActions_showDisabledActions() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+
+ List<RemoteAction> customActions = createRemoteActions(2);
+ customActions.get(0).setEnabled(false);
+ mActionsProvider.setAppActions(customActions, null);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+ ACTION_MOVE}));
+ }
+
+ @Test
+ public void mediaActions_hideDisabledActions() {
+ assumeTelevision();
+ mActionsProvider.updateExpansionEnabled(false);
+
+ List<RemoteAction> customActions = createRemoteActions(2);
+ customActions.get(0).setEnabled(false);
+ mActionsProvider.onMediaActionsChanged(customActions);
+
+ assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+ new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_MOVE}));
+ }
+
+}
+
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt
index 05e472245b4a..7370ed71bbdd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt
@@ -24,6 +24,7 @@ import android.os.test.TestLooper
import android.testing.AndroidTestingRunner
import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
import com.android.wm.shell.pip.tv.TvPipBoundsController.POSITION_DEBOUNCE_TIMEOUT_MILLIS
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
@@ -43,7 +44,7 @@ import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
-class TvPipBoundsControllerTest {
+class TvPipBoundsControllerTest : ShellTestCase() {
val ANIMATION_DURATION = 100
val STASH_DURATION = 5000
val FAR_FUTURE = 60 * 60000L
@@ -71,7 +72,7 @@ class TvPipBoundsControllerTest {
var inMoveMode = false
@Mock
- lateinit var context: Context
+ lateinit var mockContext: Context
@Mock
lateinit var resources: Resources
@Mock
@@ -83,6 +84,9 @@ class TvPipBoundsControllerTest {
@Before
fun setUp() {
+ if (!isTelevision) {
+ return
+ }
MockitoAnnotations.initMocks(this)
time = 0L
inMenu = false
@@ -91,13 +95,13 @@ class TvPipBoundsControllerTest {
testLooper = TestLooper { time }
mainHandler = Handler(testLooper.getLooper())
- whenever(context.resources).thenReturn(resources)
+ whenever(mockContext.resources).thenReturn(resources)
whenever(resources.getInteger(R.integer.config_pipStashDuration)).thenReturn(STASH_DURATION)
whenever(tvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(any()))
.then(returnsFirstArg<Rect>())
boundsController = TvPipBoundsController(
- context,
+ mockContext,
{ time },
mainHandler,
tvPipBoundsState,
@@ -107,6 +111,7 @@ class TvPipBoundsControllerTest {
@Test
fun testPlacement_MovedAfterDebounceTimeout() {
+ assumeTelevision()
triggerPlacement(MOVED_PLACEMENT)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS)
assertNoMovementUpTo(time + FAR_FUTURE)
@@ -114,6 +119,7 @@ class TvPipBoundsControllerTest {
@Test
fun testStashedPlacement_MovedAfterDebounceTimeout_Unstashes() {
+ assumeTelevision()
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS)
@@ -121,6 +127,7 @@ class TvPipBoundsControllerTest {
@Test
fun testDebounceSamePlacement_MovesDebounceTimeoutAfterFirstPlacement() {
+ assumeTelevision()
triggerPlacement(MOVED_PLACEMENT)
advanceTimeTo(POSITION_DEBOUNCE_TIMEOUT_MILLIS / 2)
triggerPlacement(MOVED_PLACEMENT)
@@ -130,6 +137,7 @@ class TvPipBoundsControllerTest {
@Test
fun testNoMovementUntilPlacementStabilizes() {
+ assumeTelevision()
triggerPlacement(ANCHOR_PLACEMENT)
advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10)
triggerPlacement(MOVED_PLACEMENT)
@@ -143,6 +151,7 @@ class TvPipBoundsControllerTest {
@Test
fun testUnstashIfStashNoLongerNecessary() {
+ assumeTelevision()
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
@@ -152,6 +161,7 @@ class TvPipBoundsControllerTest {
@Test
fun testRestashingPlacementDelaysUnstash() {
+ assumeTelevision()
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
@@ -163,6 +173,7 @@ class TvPipBoundsControllerTest {
@Test
fun testNonRestashingPlacementDoesNotDelayUnstash() {
+ assumeTelevision()
triggerPlacement(STASHED_PLACEMENT_RESTASH)
assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
@@ -173,13 +184,26 @@ class TvPipBoundsControllerTest {
@Test
fun testImmediatePlacement() {
+ assumeTelevision()
triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH)
assertMovement(STASHED_BOUNDS)
assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS)
}
@Test
+ fun testImmediatePlacement_DoNotStashIfAlreadyUnstashed() {
+ assumeTelevision()
+ triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH)
+ assertMovement(STASHED_BOUNDS)
+ assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS)
+
+ triggerImmediatePlacement(STASHED_PLACEMENT)
+ assertNoMovementUpTo(time + FAR_FUTURE)
+ }
+
+ @Test
fun testInMoveMode_KeepAtAnchor() {
+ assumeTelevision()
startMoveMode()
triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
assertMovement(ANCHOR_BOUNDS)
@@ -188,6 +212,7 @@ class TvPipBoundsControllerTest {
@Test
fun testInMenu_Unstashed() {
+ assumeTelevision()
openPipMenu()
triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
assertMovement(MOVED_BOUNDS)
@@ -196,6 +221,7 @@ class TvPipBoundsControllerTest {
@Test
fun testCloseMenu_DoNotRestash() {
+ assumeTelevision()
openPipMenu()
triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
assertMovement(MOVED_BOUNDS)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java
new file mode 100644
index 000000000000..f9b772345b14
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java
@@ -0,0 +1,348 @@
+/*
+ * 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.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
+import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
+import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
+import static android.view.KeyEvent.KEYCODE_DPAD_UP;
+
+import static org.junit.Assert.assertEquals;
+
+import android.view.Gravity;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip.PipSnapAlgorithm;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Locale;
+
+public class TvPipGravityTest extends ShellTestCase {
+
+ private static final float VERTICAL_EXPANDED_ASPECT_RATIO = 1f / 3;
+ private static final float HORIZONTAL_EXPANDED_ASPECT_RATIO = 3f;
+
+ @Mock
+ private PipSnapAlgorithm mMockPipSnapAlgorithm;
+
+ private TvPipBoundsState mTvPipBoundsState;
+ private TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
+ private PipSizeSpecHandler mPipSizeSpecHandler;
+ private PipDisplayLayoutState mPipDisplayLayoutState;
+
+ @Before
+ public void setUp() {
+ if (!isTelevision()) {
+ return;
+ }
+ MockitoAnnotations.initMocks(this);
+ mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
+ mPipSizeSpecHandler = new PipSizeSpecHandler(mContext, mPipDisplayLayoutState);
+ mTvPipBoundsState = new TvPipBoundsState(mContext, mPipSizeSpecHandler,
+ mPipDisplayLayoutState);
+ mTvPipBoundsAlgorithm = new TvPipBoundsAlgorithm(mContext, mTvPipBoundsState,
+ mMockPipSnapAlgorithm, mPipSizeSpecHandler);
+
+ setRTL(false);
+ }
+
+ private void checkGravity(int gravityActual, int gravityExpected) {
+ assertEquals(gravityExpected, gravityActual);
+ }
+
+ private void setRTL(boolean isRtl) {
+ mContext.getResources().getConfiguration().setLayoutDirection(
+ isRtl ? new Locale("ar") : Locale.ENGLISH);
+ mTvPipBoundsState.onConfigurationChanged();
+ mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
+ }
+
+ private void assertGravityAfterExpansion(int gravityFrom, int gravityTo) {
+ mTvPipBoundsState.setTvPipExpanded(false);
+ mTvPipBoundsState.setTvPipGravity(gravityFrom);
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true);
+ checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo);
+ }
+
+ private void assertGravityAfterCollapse(int gravityFrom, int gravityTo) {
+ mTvPipBoundsState.setTvPipExpanded(true);
+ mTvPipBoundsState.setTvPipGravity(gravityFrom);
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false);
+ checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo);
+ }
+
+ private void assertGravityAfterExpandAndCollapse(int gravityStartAndEnd) {
+ mTvPipBoundsState.setTvPipGravity(gravityStartAndEnd);
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true);
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false);
+ checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityStartAndEnd);
+ }
+
+ @Test
+ public void regularPip_defaultGravity() {
+ assumeTelevision();
+ checkGravity(mTvPipBoundsState.getDefaultGravity(), Gravity.RIGHT | Gravity.BOTTOM);
+ }
+
+ @Test
+ public void regularPip_defaultGravity_RTL() {
+ assumeTelevision();
+ setRTL(true);
+ checkGravity(mTvPipBoundsState.getDefaultGravity(), Gravity.LEFT | Gravity.BOTTOM);
+ }
+
+ @Test
+ public void updateGravity_expand_vertical() {
+ assumeTelevision();
+ // Vertical expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+
+ assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.RIGHT,
+ Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+ assertGravityAfterExpansion(Gravity.TOP | Gravity.RIGHT,
+ Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+ assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.LEFT,
+ Gravity.CENTER_VERTICAL | Gravity.LEFT);
+ assertGravityAfterExpansion(Gravity.TOP | Gravity.LEFT,
+ Gravity.CENTER_VERTICAL | Gravity.LEFT);
+ }
+
+ @Test
+ public void updateGravity_expand_horizontal() {
+ assumeTelevision();
+ // Horizontal expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+
+ assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.RIGHT,
+ Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+ assertGravityAfterExpansion(Gravity.TOP | Gravity.RIGHT,
+ Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+ assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.LEFT,
+ Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+ assertGravityAfterExpansion(Gravity.TOP | Gravity.LEFT,
+ Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+ }
+
+ @Test
+ public void updateGravity_collapse() {
+ assumeTelevision();
+ // Vertical expansion
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+ assertGravityAfterCollapse(Gravity.CENTER_VERTICAL | Gravity.RIGHT,
+ Gravity.BOTTOM | Gravity.RIGHT);
+ assertGravityAfterCollapse(Gravity.CENTER_VERTICAL | Gravity.LEFT,
+ Gravity.BOTTOM | Gravity.LEFT);
+
+ // Horizontal expansion
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+ assertGravityAfterCollapse(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+ Gravity.TOP | Gravity.RIGHT);
+ assertGravityAfterCollapse(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
+ Gravity.BOTTOM | Gravity.RIGHT);
+ }
+
+ @Test
+ public void updateGravity_collapse_RTL() {
+ assumeTelevision();
+ setRTL(true);
+
+ // Horizontal expansion
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+ assertGravityAfterCollapse(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+ Gravity.TOP | Gravity.LEFT);
+ assertGravityAfterCollapse(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
+ Gravity.BOTTOM | Gravity.LEFT);
+ }
+
+ @Test
+ public void updateGravity_expand_collapse() {
+ assumeTelevision();
+ // Vertical expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+
+ assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.RIGHT);
+ assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.LEFT);
+ assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.LEFT);
+ assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.RIGHT);
+
+ // Horizontal expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+
+ assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.RIGHT);
+ assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.LEFT);
+ assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.LEFT);
+ assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.RIGHT);
+ }
+
+ @Test
+ public void updateGravity_expand_move_collapse() {
+ assumeTelevision();
+ // Vertical expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+ expandMoveCollapseCheck(Gravity.TOP | Gravity.RIGHT, KEYCODE_DPAD_LEFT,
+ Gravity.TOP | Gravity.LEFT);
+
+ // Horizontal expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+ expandMoveCollapseCheck(Gravity.BOTTOM | Gravity.LEFT, KEYCODE_DPAD_UP,
+ Gravity.TOP | Gravity.LEFT);
+ }
+
+ private void expandMoveCollapseCheck(int gravityFrom, int keycode, int gravityTo) {
+ // Expand
+ mTvPipBoundsState.setTvPipExpanded(false);
+ mTvPipBoundsState.setTvPipGravity(gravityFrom);
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true);
+ // Move
+ mTvPipBoundsAlgorithm.updateGravity(keycode);
+ // Collapse
+ mTvPipBoundsState.setTvPipExpanded(true);
+ mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false);
+
+ checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo);
+ }
+
+ private void moveAndCheckGravity(int keycode, int gravityEnd, boolean expectChange) {
+ assertEquals(expectChange, mTvPipBoundsAlgorithm.updateGravity(keycode));
+ checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityEnd);
+ }
+
+ @Test
+ public void updateGravity_move_regular_valid() {
+ assumeTelevision();
+ mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.RIGHT);
+ // clockwise
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.LEFT, true);
+ moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.LEFT, true);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.TOP | Gravity.RIGHT, true);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.RIGHT, true);
+ // anti-clockwise
+ moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.RIGHT, true);
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.TOP | Gravity.LEFT, true);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.LEFT, true);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.BOTTOM | Gravity.RIGHT, true);
+ }
+
+ @Test
+ public void updateGravity_move_expanded_valid() {
+ assumeTelevision();
+ mTvPipBoundsState.setTvPipExpanded(true);
+
+ // Vertical expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, true);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, true);
+
+ // Horizontal expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+ moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, true);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, true);
+ }
+
+ @Test
+ public void updateGravity_move_regular_invalid() {
+ assumeTelevision();
+ int gravity = Gravity.BOTTOM | Gravity.RIGHT;
+ mTvPipBoundsState.setTvPipGravity(gravity);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, gravity, false);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, gravity, false);
+
+ gravity = Gravity.BOTTOM | Gravity.LEFT;
+ mTvPipBoundsState.setTvPipGravity(gravity);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, gravity, false);
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, gravity, false);
+
+ gravity = Gravity.TOP | Gravity.LEFT;
+ mTvPipBoundsState.setTvPipGravity(gravity);
+ moveAndCheckGravity(KEYCODE_DPAD_UP, gravity, false);
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, gravity, false);
+
+ gravity = Gravity.TOP | Gravity.RIGHT;
+ mTvPipBoundsState.setTvPipGravity(gravity);
+ moveAndCheckGravity(KEYCODE_DPAD_UP, gravity, false);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, gravity, false);
+ }
+
+ @Test
+ public void updateGravity_move_expanded_invalid() {
+ assumeTelevision();
+ mTvPipBoundsState.setTvPipExpanded(true);
+
+ // Vertical expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
+ moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
+
+ mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, false);
+ moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.LEFT, false);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.CENTER_VERTICAL | Gravity.LEFT, false);
+
+ // Horizontal expanded PiP.
+ mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+ moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
+
+ mTvPipBoundsState.setTvPipGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+ moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false);
+ moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false);
+ moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false);
+ }
+
+ @Test
+ public void previousCollapsedGravity_defaultValue() {
+ assumeTelevision();
+ assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+ mTvPipBoundsState.getDefaultGravity());
+ setRTL(true);
+ assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+ mTvPipBoundsState.getDefaultGravity());
+ }
+
+ @Test
+ public void previousCollapsedGravity_changes_on_RTL() {
+ assumeTelevision();
+ mTvPipBoundsState.setTvPipPreviousCollapsedGravity(Gravity.TOP | Gravity.LEFT);
+ setRTL(true);
+ assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+ Gravity.TOP | Gravity.RIGHT);
+ setRTL(false);
+ assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+ Gravity.TOP | Gravity.LEFT);
+
+ mTvPipBoundsState.setTvPipPreviousCollapsedGravity(Gravity.BOTTOM | Gravity.RIGHT);
+ setRTL(true);
+ assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+ Gravity.BOTTOM | Gravity.LEFT);
+ setRTL(false);
+ assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+ Gravity.BOTTOM | Gravity.RIGHT);
+ }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt
index 0fcc5cf384c9..aedf65ddc269 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt
@@ -21,23 +21,25 @@ import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.util.Size
import android.view.Gravity
-import org.junit.runner.RunWith
-import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE
+import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM
+import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_TOP
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
-import org.junit.Before
-import org.junit.Test
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNull
import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
-class TvPipKeepClearAlgorithmTest {
+class TvPipKeepClearAlgorithmTest : ShellTestCase() {
private val DEFAULT_PIP_SIZE = Size(384, 216)
- private val EXPANDED_WIDE_PIP_SIZE = Size(384*2, 216)
+ private val EXPANDED_WIDE_PIP_SIZE = Size(384 * 2, 216)
+ private val EXPANDED_TALL_PIP_SIZE = Size(384, 216 * 4)
private val DASHBOARD_WIDTH = 484
private val BOTTOM_SHEET_HEIGHT = 524
private val STASH_OFFSET = 64
@@ -54,6 +56,9 @@ class TvPipKeepClearAlgorithmTest {
@Before
fun setup() {
+ if (!isTelevision) {
+ return
+ }
movementBounds = Rect(0, 0, SCREEN_SIZE.width, SCREEN_SIZE.height)
movementBounds.inset(SCREEN_EDGE_INSET, SCREEN_EDGE_INSET)
@@ -73,72 +78,84 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun testAnchorPosition_BottomRight() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
testAnchorPosition()
}
@Test
fun testAnchorPosition_TopRight() {
+ assumeTelevision()
gravity = Gravity.TOP or Gravity.RIGHT
testAnchorPosition()
}
@Test
fun testAnchorPosition_TopLeft() {
+ assumeTelevision()
gravity = Gravity.TOP or Gravity.LEFT
testAnchorPosition()
}
@Test
fun testAnchorPosition_BottomLeft() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.LEFT
testAnchorPosition()
}
@Test
fun testAnchorPosition_Right() {
+ assumeTelevision()
gravity = Gravity.RIGHT
testAnchorPosition()
}
@Test
fun testAnchorPosition_Left() {
+ assumeTelevision()
gravity = Gravity.LEFT
testAnchorPosition()
}
@Test
fun testAnchorPosition_Top() {
+ assumeTelevision()
gravity = Gravity.TOP
testAnchorPosition()
}
@Test
fun testAnchorPosition_Bottom() {
+ assumeTelevision()
gravity = Gravity.BOTTOM
testAnchorPosition()
}
@Test
fun testAnchorPosition_TopCenterHorizontal() {
+ assumeTelevision()
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
testAnchorPosition()
}
@Test
fun testAnchorPosition_BottomCenterHorizontal() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
testAnchorPosition()
}
@Test
fun testAnchorPosition_RightCenterVertical() {
+ assumeTelevision()
gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
testAnchorPosition()
}
@Test
fun testAnchorPosition_LeftCenterVertical() {
+ assumeTelevision()
gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
testAnchorPosition()
}
@@ -152,6 +169,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomRight_KeepClearNotObstructing_StayAtAnchor() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.LEFT)
@@ -166,6 +184,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomRight_UnrestrictedRightSidebar_PushedLeft() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
@@ -180,6 +199,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorTopRight_UnrestrictedRightSidebar_PushedLeft() {
+ assumeTelevision()
gravity = Gravity.TOP or Gravity.RIGHT
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
@@ -194,6 +214,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomLeft_UnrestrictedRightSidebar_StayAtAnchor() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.LEFT
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
@@ -208,6 +229,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() {
+ assumeTelevision()
gravity = Gravity.BOTTOM
val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
@@ -222,6 +244,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun testExpanded_AnchorBottom_UnrestrictedRightSidebar_StayAtAnchor() {
+ assumeTelevision()
pipSize = EXPANDED_WIDE_PIP_SIZE
gravity = Gravity.BOTTOM
@@ -237,6 +260,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomRight_RestrictedSmallBottomBar_PushedUp() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(96)
@@ -252,6 +276,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomRight_RestrictedBottomSheet_StashDownAtAnchor() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -269,6 +294,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomRight_UnrestrictedBottomSheet_PushUp() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -284,6 +310,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomRight_UnrestrictedBottomSheet_RestrictedSidebar_StashAboveBottomSheet() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -309,6 +336,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_AnchorBottomRight_UnrestrictedBottomSheet_UnrestrictedSidebar_PushUpLeft() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -331,6 +359,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_Stashed_UnstashBoundsBecomeUnobstructed_Unstashes() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -361,6 +390,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -392,6 +422,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_Stashed_UnstashBoundsObstructionChanges_UnstashTimeExtended() {
+ assumeTelevision()
gravity = Gravity.BOTTOM or Gravity.RIGHT
val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -431,6 +462,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_ExpandedPiPHeightExceedsMovementBounds_AtAnchor() {
+ assumeTelevision()
gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height)
testAnchorPosition()
@@ -438,6 +470,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_ExpandedPiPHeightExceedsMovementBounds_BottomBar_StashedUp() {
+ assumeTelevision()
gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height)
val bottomBar = makeBottomBar(96)
@@ -453,6 +486,7 @@ class TvPipKeepClearAlgorithmTest {
@Test
fun test_PipInsets() {
+ assumeTelevision()
val insets = Insets.of(-1, -2, -3, -4)
algorithm.setPipPermanentDecorInsets(insets)
@@ -485,6 +519,41 @@ class TvPipKeepClearAlgorithmTest {
testAnchorPositionWithInsets(insets)
}
+ @Test
+ fun test_AnchorRightExpandedPiP_UnrestrictedRightSidebar_PushedLeft() {
+ assumeTelevision()
+ pipSize = EXPANDED_TALL_PIP_SIZE
+ gravity = Gravity.RIGHT
+
+ val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
+ unrestrictedAreas.add(sidebar)
+
+ val expectedBounds = anchorBoundsOffsetBy(SCREEN_EDGE_INSET - sidebar.width() - PADDING, 0)
+
+ val placement = getActualPlacement()
+ assertEquals(expectedBounds, placement.bounds)
+ assertNotStashed(placement)
+ }
+
+ @Test
+ fun test_AnchorRightExpandedPiP_RestrictedRightSidebar_StashedRight() {
+ assumeTelevision()
+ assumeTelevision()
+ pipSize = EXPANDED_TALL_PIP_SIZE
+ gravity = Gravity.RIGHT
+
+ val sidebar = makeSideBar(DASHBOARD_WIDTH, Gravity.RIGHT)
+ restrictedAreas.add(sidebar)
+
+ val expectedBounds = getExpectedAnchorBounds()
+ expectedBounds.offsetTo(SCREEN_SIZE.width - STASH_OFFSET, expectedBounds.top)
+
+ val placement = getActualPlacement()
+ assertEquals(expectedBounds, placement.bounds)
+ assertEquals(STASH_TYPE_RIGHT, placement.stashType)
+ assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds)
+ }
+
private fun testAnchorPositionWithInsets(insets: Insets) {
var pipRect = Rect(0, 0, pipSize.width, pipSize.height)
pipRect.inset(insets)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java
new file mode 100644
index 000000000000..3a08d32bc430
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.pip.tv;
+
+import static android.view.KeyEvent.KEYCODE_DPAD_UP;
+
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU;
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU;
+import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.Handler;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.SystemWindows;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class TvPipMenuControllerTest extends ShellTestCase {
+ private static final int TEST_MOVE_KEYCODE = KEYCODE_DPAD_UP;
+
+ @Mock
+ private TvPipMenuController.Delegate mMockDelegate;
+ @Mock
+ private TvPipBoundsState mMockTvPipBoundsState;
+ @Mock
+ private SystemWindows mMockSystemWindows;
+ @Mock
+ private SurfaceControl mMockPipLeash;
+ @Mock
+ private Handler mMockHandler;
+ @Mock
+ private TvPipActionsProvider mMockActionsProvider;
+ @Mock
+ private TvPipMenuView mMockTvPipMenuView;
+ @Mock
+ private TvPipBackgroundView mMockTvPipBackgroundView;
+
+ private TvPipMenuController mTvPipMenuController;
+
+ @Before
+ public void setUp() {
+ assumeTrue(isTelevision());
+
+ MockitoAnnotations.initMocks(this);
+
+ mTvPipMenuController = new TestTvPipMenuController();
+ mTvPipMenuController.setDelegate(mMockDelegate);
+ mTvPipMenuController.setTvPipActionsProvider(mMockActionsProvider);
+ mTvPipMenuController.attach(mMockPipLeash);
+ }
+
+ @Test
+ public void testMenuNotOpenByDefault() {
+ assertMenuIsOpen(false);
+ }
+
+ @Test
+ public void testSwitch_FromNoMenuMode_ToMoveMode() {
+ showAndAssertMoveMenu();
+ }
+
+ @Test
+ public void testSwitch_FromNoMenuMode_ToAllActionsMode() {
+ showAndAssertAllActionsMenu();
+ }
+
+ @Test
+ public void testSwitch_FromMoveMode_ToAllActionsMode() {
+ showAndAssertMoveMenu();
+ showAndAssertAllActionsMenu();
+ }
+
+ @Test
+ public void testSwitch_FromAllActionsMode_ToMoveMode() {
+ showAndAssertAllActionsMenu();
+ showAndAssertMoveMenu();
+ }
+
+ @Test
+ public void testCloseMenu_NoMenuMode() {
+ mTvPipMenuController.closeMenu();
+ assertMenuIsOpen(false);
+ verify(mMockDelegate, never()).onMenuClosed();
+ }
+
+ @Test
+ public void testCloseMenu_MoveMode() {
+ showAndAssertMoveMenu();
+
+ closeMenuAndAssertMenuClosed();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+ }
+
+ @Test
+ public void testCloseMenu_AllActionsMode() {
+ showAndAssertAllActionsMenu();
+
+ closeMenuAndAssertMenuClosed();
+ }
+
+ @Test
+ public void testCloseMenu_MoveModeFollowedByAllActionsMode() {
+ showAndAssertMoveMenu();
+ showAndAssertAllActionsMenu();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+
+ closeMenuAndAssertMenuClosed();
+ }
+
+ @Test
+ public void testCloseMenu_AllActionsModeFollowedByMoveMode() {
+ showAndAssertAllActionsMenu();
+ showAndAssertMoveMenu();
+
+ closeMenuAndAssertMenuClosed();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+ }
+
+ @Test
+ public void testExitMoveMode_NoMenuMode() {
+ mTvPipMenuController.onExitMoveMode();
+ assertMenuIsOpen(false);
+ verify(mMockDelegate, never()).onMenuClosed();
+ }
+
+ @Test
+ public void testExitMoveMode_MoveMode() {
+ showAndAssertMoveMenu();
+
+ mTvPipMenuController.onExitMoveMode();
+ assertMenuClosed();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+ }
+
+ @Test
+ public void testExitMoveMode_AllActionsMode() {
+ showAndAssertAllActionsMenu();
+
+ mTvPipMenuController.onExitMoveMode();
+ assertMenuIsInAllActionsMode();
+
+ }
+
+ @Test
+ public void testExitMoveMode_AllActionsModeFollowedByMoveMode() {
+ showAndAssertAllActionsMenu();
+ showAndAssertMoveMenu();
+
+ mTvPipMenuController.onExitMoveMode();
+ assertMenuIsInAllActionsMode();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+ verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false));
+ verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU));
+ }
+
+ @Test
+ public void testOnBackPress_NoMenuMode() {
+ mTvPipMenuController.onBackPress();
+ assertMenuIsOpen(false);
+ verify(mMockDelegate, never()).onMenuClosed();
+ }
+
+ @Test
+ public void testOnBackPress_MoveMode() {
+ showAndAssertMoveMenu();
+
+ pressBackAndAssertMenuClosed();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+ }
+
+ @Test
+ public void testOnBackPress_AllActionsMode() {
+ showAndAssertAllActionsMenu();
+
+ pressBackAndAssertMenuClosed();
+ }
+
+ @Test
+ public void testOnBackPress_MoveModeFollowedByAllActionsMode() {
+ showAndAssertMoveMenu();
+ showAndAssertAllActionsMenu();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+
+ pressBackAndAssertMenuClosed();
+ }
+
+ @Test
+ public void testOnBackPress_AllActionsModeFollowedByMoveMode() {
+ showAndAssertAllActionsMenu();
+ showAndAssertMoveMenu();
+
+ mTvPipMenuController.onBackPress();
+ assertMenuIsInAllActionsMode();
+ verify(mMockDelegate, times(2)).onInMoveModeChanged();
+ verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false));
+ verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU));
+
+ pressBackAndAssertMenuClosed();
+ }
+
+ @Test
+ public void testOnPipMovement_NoMenuMode() {
+ assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE));
+ }
+
+ @Test
+ public void testOnPipMovement_MoveMode() {
+ showAndAssertMoveMenu();
+ assertPipMoveSuccessful(true, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE));
+ verify(mMockDelegate).movePip(eq(TEST_MOVE_KEYCODE));
+ }
+
+ @Test
+ public void testOnPipMovement_AllActionsMode() {
+ showAndAssertAllActionsMenu();
+ assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE));
+ }
+
+ @Test
+ public void testOnPipWindowFocusChanged_NoMenuMode() {
+ mTvPipMenuController.onPipWindowFocusChanged(false);
+ assertMenuIsOpen(false);
+ }
+
+ @Test
+ public void testOnPipWindowFocusChanged_MoveMode() {
+ showAndAssertMoveMenu();
+ mTvPipMenuController.onPipWindowFocusChanged(false);
+ assertMenuClosed();
+ }
+
+ @Test
+ public void testOnPipWindowFocusChanged_AllActionsMode() {
+ showAndAssertAllActionsMenu();
+ mTvPipMenuController.onPipWindowFocusChanged(false);
+ assertMenuClosed();
+ }
+
+ private void showAndAssertMoveMenu() {
+ mTvPipMenuController.showMovementMenu();
+ assertMenuIsInMoveMode();
+ verify(mMockDelegate).onInMoveModeChanged();
+ verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_MOVE_MENU), eq(false));
+ verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_MOVE_MENU));
+ }
+
+ private void showAndAssertAllActionsMenu() {
+ mTvPipMenuController.showMenu();
+ assertMenuIsInAllActionsMode();
+ verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(true));
+ verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU));
+ }
+
+ private void closeMenuAndAssertMenuClosed() {
+ mTvPipMenuController.closeMenu();
+ assertMenuClosed();
+ }
+
+ private void pressBackAndAssertMenuClosed() {
+ mTvPipMenuController.onBackPress();
+ assertMenuClosed();
+ }
+
+ private void assertMenuClosed() {
+ assertMenuIsOpen(false);
+ verify(mMockDelegate).onMenuClosed();
+ verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_NO_MENU), eq(false));
+ verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_NO_MENU));
+ }
+
+ private void assertMenuIsOpen(boolean open) {
+ assertTrue("The TV PiP menu should " + (open ? "" : "not ") + "be open, but it"
+ + " is in mode " + mTvPipMenuController.getMenuModeString(),
+ mTvPipMenuController.isMenuOpen() == open);
+ }
+
+ private void assertMenuIsInMoveMode() {
+ assertTrue("Expected MODE_MOVE_MENU, but got " + mTvPipMenuController.getMenuModeString(),
+ mTvPipMenuController.isInMoveMode());
+ assertMenuIsOpen(true);
+ }
+
+ private void assertMenuIsInAllActionsMode() {
+ assertTrue("Expected MODE_ALL_ACTIONS_MENU, but got "
+ + mTvPipMenuController.getMenuModeString(),
+ mTvPipMenuController.isInAllActionsMode());
+ assertMenuIsOpen(true);
+ }
+
+ private void assertPipMoveSuccessful(boolean expected, boolean actual) {
+ assertTrue("Should " + (expected ? "" : "not ") + "move PiP when the menu is in mode "
+ + mTvPipMenuController.getMenuModeString(), expected == actual);
+ }
+
+ private class TestTvPipMenuController extends TvPipMenuController {
+
+ TestTvPipMenuController() {
+ super(mContext, mMockTvPipBoundsState, mMockSystemWindows, mMockHandler);
+ }
+
+ @Override
+ TvPipMenuView createTvPipMenuView() {
+ return mMockTvPipMenuView;
+ }
+
+ @Override
+ TvPipBackgroundView createTvPipBackgroundView() {
+ return mMockTvPipBackgroundView;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index b542fae060d1..2c69522413d5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -107,7 +107,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
mMainExecutor = new TestShellExecutor();
when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
mShellInit = spy(new ShellInit(mMainExecutor));
- mShellController = spy(new ShellController(mShellInit, mShellCommandHandler,
+ mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
mMainExecutor));
mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index d0e26019f9bf..fb17d8799bda 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -111,7 +111,7 @@ public class SplitScreenControllerTests extends ShellTestCase {
public void setup() {
assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
MockitoAnnotations.initMocks(this);
- mShellController = spy(new ShellController(mShellInit, mShellCommandHandler,
+ mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
mMainExecutor));
mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
@@ -181,7 +181,8 @@ public class SplitScreenControllerTests extends ShellTestCase {
PendingIntent pendingIntent =
PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
- mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+ mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
+ SPLIT_POSITION_TOP_OR_LEFT, null);
verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
@@ -200,7 +201,8 @@ public class SplitScreenControllerTests extends ShellTestCase {
createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
- mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+ mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
+ SPLIT_POSITION_TOP_OR_LEFT, null);
verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
@@ -221,9 +223,10 @@ public class SplitScreenControllerTests extends ShellTestCase {
doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
// Put the same component into a task in the background
ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
- doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
+ doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt());
- mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+ mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
+ SPLIT_POSITION_TOP_OR_LEFT, null);
verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
isNull());
@@ -244,9 +247,10 @@ public class SplitScreenControllerTests extends ShellTestCase {
SPLIT_POSITION_BOTTOM_OR_RIGHT);
// Put the same component into a task in the background
doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
- .findTaskInBackground(any());
+ .findTaskInBackground(any(), anyInt());
- mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+ mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
+ SPLIT_POSITION_TOP_OR_LEFT, null);
verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
isNull());
@@ -265,7 +269,8 @@ public class SplitScreenControllerTests extends ShellTestCase {
doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
SPLIT_POSITION_BOTTOM_OR_RIGHT);
- mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+ mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
+ SPLIT_POSITION_TOP_OR_LEFT, null);
verify(mStageCoordinator).switchSplitPosition(anyString());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index ae69b3ddd042..4e446c684d86 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -44,11 +44,15 @@ public class SplitTestUtils {
static SplitLayout createMockSplitLayout() {
final Rect dividerBounds = new Rect(48, 0, 52, 100);
+ final Rect bounds1 = new Rect(0, 0, 40, 100);
+ final Rect bounds2 = new Rect(60, 0, 100, 100);
final SurfaceControl leash = createMockSurface();
SplitLayout out = mock(SplitLayout.class);
doReturn(dividerBounds).when(out).getDividerBounds();
doReturn(dividerBounds).when(out).getRefDividerBounds();
doReturn(leash).when(out).getDividerLeash();
+ doReturn(bounds1).when(out).getBounds1();
+ doReturn(bounds2).when(out).getBounds2();
return out;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 652f9b38c88f..44c76de945b0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -35,12 +35,14 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_P
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -64,13 +66,16 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TransitionInfoBuilder;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -97,6 +102,7 @@ public class SplitTransitionTests extends ShellTestCase {
@Mock private SurfaceSession mSurfaceSession;
@Mock private IconProvider mIconProvider;
@Mock private ShellExecutor mMainExecutor;
+ @Mock private DefaultMixedHandler mMixedHandler;
private SplitLayout mSplitLayout;
private MainStage mMainStage;
private SideStage mSideStage;
@@ -115,18 +121,19 @@ public class SplitTransitionTests extends ShellTestCase {
doReturn(mockExecutor).when(mTransitions).getAnimExecutor();
doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
mSplitLayout = SplitTestUtils.createMockSplitLayout();
- mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
+ mMainStage = spy(new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
- mIconProvider);
+ mIconProvider));
mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
- mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
+ mSideStage = spy(new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
- mIconProvider);
+ mIconProvider));
mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
mTransactionPool, mMainExecutor, Optional.empty());
+ mStageCoordinator.setMixedHandler(mMixedHandler);
mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
.when(mTransitions).startTransition(anyInt(), any(), any());
@@ -135,6 +142,8 @@ public class SplitTransitionTests extends ShellTestCase {
.setParentTaskId(mMainStage.mRootTaskInfo.taskId).build();
mSideChild = new TestRunningTaskInfoBuilder()
.setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
+ doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
+ doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
}
@Test
@@ -156,12 +165,10 @@ public class SplitTransitionTests extends ShellTestCase {
assertTrue(containsSplitEnter(result));
// simulate the transition
- TransitionInfo.Change openChange = createChange(TRANSIT_OPEN, newTask);
- TransitionInfo.Change reparentChange = createChange(TRANSIT_CHANGE, reparentTask);
-
- TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0);
- info.addChange(openChange);
- info.addChange(reparentChange);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_FRONT, 0)
+ .addChange(TRANSIT_OPEN, newTask)
+ .addChange(TRANSIT_CHANGE, reparentTask)
+ .build();
mSideStage.onTaskAppeared(newTask, createMockSurface());
mMainStage.onTaskAppeared(reparentTask, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -180,8 +187,9 @@ public class SplitTransitionTests extends ShellTestCase {
TestRemoteTransition testRemote = new TestRemoteTransition();
IBinder transition = mSplitScreenTransitions.startEnterTransition(
- TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
- new RemoteTransition(testRemote), mStageCoordinator, null, null);
+ TRANSIT_OPEN, new WindowContainerTransaction(),
+ new RemoteTransition(testRemote, "Test"), mStageCoordinator,
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false);
mMainStage.onTaskAppeared(mMainChild, createMockSurface());
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -216,12 +224,10 @@ public class SplitTransitionTests extends ShellTestCase {
assertFalse(containsSplitExit(result));
// simulate the transition
- TransitionInfo.Change openChange = createChange(TRANSIT_TO_FRONT, newTask);
- TransitionInfo.Change hideChange = createChange(TRANSIT_TO_BACK, mSideChild);
-
- TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0);
- info.addChange(openChange);
- info.addChange(hideChange);
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_FRONT, 0)
+ .addChange(TRANSIT_TO_FRONT, newTask)
+ .addChange(TRANSIT_TO_BACK, mSideChild)
+ .build();
mSideStage.onTaskAppeared(newTask, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
@@ -237,12 +243,10 @@ public class SplitTransitionTests extends ShellTestCase {
assertNotNull(result);
assertFalse(containsSplitExit(result));
- TransitionInfo.Change showChange = createChange(TRANSIT_TO_FRONT, mSideChild);
- TransitionInfo.Change closeChange = createChange(TRANSIT_CLOSE, newTask);
-
- info = new TransitionInfo(TRANSIT_CLOSE, 0);
- info.addChange(showChange);
- info.addChange(closeChange);
+ info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(TRANSIT_TO_FRONT, mSideChild)
+ .addChange(TRANSIT_CLOSE, newTask)
+ .build();
mSideStage.onTaskVanished(newTask);
accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
@@ -254,7 +258,7 @@ public class SplitTransitionTests extends ShellTestCase {
@Test
@UiThreadTest
- public void testEnterRecents() {
+ public void testEnterRecentsAndCommit() {
enterSplit();
ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder()
@@ -263,69 +267,66 @@ public class SplitTransitionTests extends ShellTestCase {
.build();
// Create a request to bring home forward
- TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, null);
+ TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask,
+ mock(RemoteTransition.class));
IBinder transition = mock(IBinder.class);
WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request);
-
- assertTrue(result.isEmpty());
+ // Don't handle recents opening
+ assertNull(result);
// make sure we haven't made any local changes yet (need to wait until transition is ready)
assertTrue(mStageCoordinator.isSplitScreenVisible());
- // simulate the transition
- TransitionInfo.Change homeChange = createChange(TRANSIT_TO_FRONT, homeTask);
- TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild);
- TransitionInfo.Change sideChange = createChange(TRANSIT_TO_BACK, mSideChild);
-
- TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0);
- info.addChange(homeChange);
- info.addChange(mainChange);
- info.addChange(sideChange);
+ // simulate the start of recents transition
mMainStage.onTaskVanished(mMainChild);
mSideStage.onTaskVanished(mSideChild);
- mStageCoordinator.startAnimation(transition, info,
- mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class),
- mock(Transitions.TransitionFinishCallback.class));
+ mStageCoordinator.onRecentsInSplitAnimationStart(mock(TransitionInfo.class));
assertTrue(mStageCoordinator.isSplitScreenVisible());
+
+ // Make sure it cleans-up if recents doesn't restore
+ WindowContainerTransaction commitWCT = new WindowContainerTransaction();
+ mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT,
+ mock(SurfaceControl.Transaction.class));
+ assertFalse(mStageCoordinator.isSplitScreenVisible());
}
@Test
@UiThreadTest
- public void testDismissFromBeingOccluded() {
+ public void testEnterRecentsAndRestore() {
enterSplit();
- ActivityManager.RunningTaskInfo normalTask = new TestRunningTaskInfoBuilder()
+ ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder()
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .setActivityType(ACTIVITY_TYPE_HOME)
.build();
- // Create a request to bring a normal task forward
- TransitionRequestInfo request =
- new TransitionRequestInfo(TRANSIT_TO_FRONT, normalTask, null);
+ // Create a request to bring home forward
+ TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask,
+ mock(RemoteTransition.class));
IBinder transition = mock(IBinder.class);
WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request);
-
- assertTrue(containsSplitExit(result));
+ // Don't handle recents opening
+ assertNull(result);
// make sure we haven't made any local changes yet (need to wait until transition is ready)
assertTrue(mStageCoordinator.isSplitScreenVisible());
- // simulate the transition
- TransitionInfo.Change normalChange = createChange(TRANSIT_TO_FRONT, normalTask);
- TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild);
- TransitionInfo.Change sideChange = createChange(TRANSIT_TO_BACK, mSideChild);
-
- TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0);
- info.addChange(normalChange);
- info.addChange(mainChange);
- info.addChange(sideChange);
+ // simulate the start of recents transition
mMainStage.onTaskVanished(mMainChild);
mSideStage.onTaskVanished(mSideChild);
- mStageCoordinator.startAnimation(transition, info,
- mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class),
- mock(Transitions.TransitionFinishCallback.class));
- assertFalse(mStageCoordinator.isSplitScreenVisible());
+ mStageCoordinator.onRecentsInSplitAnimationStart(mock(TransitionInfo.class));
+ assertTrue(mStageCoordinator.isSplitScreenVisible());
+
+ // Make sure we remain in split after recents restores.
+ WindowContainerTransaction restoreWCT = new WindowContainerTransaction();
+ restoreWCT.reorder(mMainChild.token, true /* toTop */);
+ restoreWCT.reorder(mSideChild.token, true /* toTop */);
+ // simulate the restoreWCT being applied:
+ mMainStage.onTaskAppeared(mMainChild, mock(SurfaceControl.class));
+ mSideStage.onTaskAppeared(mSideChild, mock(SurfaceControl.class));
+ mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT,
+ mock(SurfaceControl.Transaction.class));
+ assertTrue(mStageCoordinator.isSplitScreenVisible());
}
@Test
@@ -334,11 +335,10 @@ public class SplitTransitionTests extends ShellTestCase {
enterSplit();
// simulate the transition
- TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild);
- TransitionInfo.Change sideChange = createChange(TRANSIT_TO_BACK, mSideChild);
- TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0);
- info.addChange(mainChange);
- info.addChange(sideChange);
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_BACK, 0)
+ .addChange(TRANSIT_TO_BACK, mMainChild)
+ .addChange(TRANSIT_TO_BACK, mSideChild)
+ .build();
IBinder transition = mSplitScreenTransitions.startDismissTransition(
new WindowContainerTransaction(), mStageCoordinator,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, STAGE_TYPE_SIDE);
@@ -356,12 +356,10 @@ public class SplitTransitionTests extends ShellTestCase {
enterSplit();
// simulate the transition
- TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild);
- TransitionInfo.Change sideChange = createChange(TRANSIT_CHANGE, mSideChild);
-
- TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0);
- info.addChange(mainChange);
- info.addChange(sideChange);
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_BACK, 0)
+ .addChange(TRANSIT_TO_BACK, mMainChild)
+ .addChange(TRANSIT_CHANGE, mSideChild)
+ .build();
IBinder transition = mSplitScreenTransitions.startDismissTransition(
new WindowContainerTransaction(), mStageCoordinator, EXIT_REASON_DRAG_DIVIDER,
STAGE_TYPE_SIDE);
@@ -391,12 +389,10 @@ public class SplitTransitionTests extends ShellTestCase {
assertTrue(mStageCoordinator.isSplitScreenVisible());
// simulate the transition
- TransitionInfo.Change mainChange = createChange(TRANSIT_CHANGE, mMainChild);
- TransitionInfo.Change sideChange = createChange(TRANSIT_CLOSE, mSideChild);
-
- TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
- info.addChange(mainChange);
- info.addChange(sideChange);
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(TRANSIT_CHANGE, mMainChild)
+ .addChange(TRANSIT_CLOSE, mSideChild)
+ .build();
mMainStage.onTaskVanished(mMainChild);
mSideStage.onTaskVanished(mSideChild);
boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -408,20 +404,18 @@ public class SplitTransitionTests extends ShellTestCase {
}
private TransitionInfo createEnterPairInfo() {
- TransitionInfo.Change mainChange = createChange(TRANSIT_OPEN, mMainChild);
- TransitionInfo.Change sideChange = createChange(TRANSIT_OPEN, mSideChild);
-
- TransitionInfo info = new TransitionInfo(TRANSIT_SPLIT_SCREEN_PAIR_OPEN, 0);
- info.addChange(mainChange);
- info.addChange(sideChange);
- return info;
+ return new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(TRANSIT_OPEN, mMainChild)
+ .addChange(TRANSIT_OPEN, mSideChild)
+ .build();
}
private void enterSplit() {
TransitionInfo enterInfo = createEnterPairInfo();
IBinder enterTransit = mSplitScreenTransitions.startEnterTransition(
- TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
- new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null, null);
+ TRANSIT_OPEN, new WindowContainerTransaction(),
+ new RemoteTransition(new TestRemoteTransition(), "Test"),
+ mStageCoordinator, TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false);
mMainStage.onTaskAppeared(mMainChild, createMockSurface());
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
mStageCoordinator.startAnimation(enterTransit, enterInfo,
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 0b528ba3a9ed..2dcdc74e8ae7 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
@@ -68,6 +68,7 @@ import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
import com.android.wm.shell.sysui.ShellController;
@@ -113,6 +114,7 @@ public class StageCoordinatorTests extends ShellTestCase {
private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mRootLeash;
+ private SurfaceControl mDividerLeash;
private ActivityManager.RunningTaskInfo mRootTask;
private StageCoordinator mStageCoordinator;
private Transitions mTransitions;
@@ -129,11 +131,14 @@ public class StageCoordinatorTests extends ShellTestCase {
mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
mMainExecutor, Optional.empty()));
+ mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build();
when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds);
when(mSplitLayout.isLandscape()).thenReturn(false);
+ when(mSplitLayout.applyTaskChanges(any(), any(), any())).thenReturn(true);
+ when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash);
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
@@ -141,6 +146,8 @@ public class StageCoordinatorTests extends ShellTestCase {
mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
+ doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
+ doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
}
@Test
@@ -148,10 +155,12 @@ public class StageCoordinatorTests extends ShellTestCase {
when(mStageCoordinator.isSplitActive()).thenReturn(true);
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
- final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final WindowContainerTransaction wct = spy(new WindowContainerTransaction());
mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
- verify(mSideStage).addTask(eq(task), eq(wct));
+ verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task),
+ eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false));
+ verify(mMainStage).reparentTopTask(eq(wct));
assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition());
assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition());
}
@@ -167,14 +176,10 @@ public class StageCoordinatorTests extends ShellTestCase {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
- verify(mMainStage).addTask(eq(task), eq(wct));
+ verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task),
+ eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false));
assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition());
assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition());
-
- mStageCoordinator.moveToStage(task, SPLIT_POSITION_TOP_OR_LEFT, wct);
- verify(mSideStage).addTask(eq(task), eq(wct));
- assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition());
- assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition());
}
@Test
@@ -184,7 +189,7 @@ public class StageCoordinatorTests extends ShellTestCase {
mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task),
- eq(SPLIT_POSITION_BOTTOM_OR_RIGHT));
+ eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false));
assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index b31e20fcc438..1a1bebd28aef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -61,7 +61,7 @@ import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
public final class StageTaskListenerTests extends ShellTestCase {
private static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
@Mock
private ShellTaskOrganizer mTaskOrganizer;
@@ -126,6 +126,12 @@ public final class StageTaskListenerTests extends ShellTestCase {
verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));
}
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnknownTaskVanished() {
+ final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
+ mStageTaskListener.onTaskVanished(task);
+ }
+
@Test
public void testTaskVanished() {
// With shell transitions, the transition manages status changes, so skip this test.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index e5ae2962e6e4..ee9f88663326 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -24,8 +24,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION;
-import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION;
+import static com.android.wm.shell.startingsurface.SplashscreenContentDrawer.MAX_ANIMATION_DURATION;
+import static com.android.wm.shell.startingsurface.SplashscreenContentDrawer.MINIMAL_ANIMATION_DURATION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
@@ -56,11 +56,9 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.UserHandle;
import android.testing.TestableContext;
-import android.view.Display;
import android.view.IWindowSession;
import android.view.InsetsState;
import android.view.Surface;
-import android.view.View;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.WindowMetrics;
@@ -106,36 +104,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
private ShellExecutor mTestExecutor;
private final TestableContext mTestContext = new TestContext(
InstrumentationRegistry.getInstrumentation().getTargetContext());
- TestStartingSurfaceDrawer mStartingSurfaceDrawer;
-
- static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{
- int mAddWindowForTask = 0;
-
- TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
- IconProvider iconProvider, TransactionPool pool) {
- super(context, splashScreenExecutor, iconProvider, pool);
- }
-
- @Override
- protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
- WindowManager.LayoutParams params, int suggestType) {
- // listen for addView
- mAddWindowForTask = taskId;
- saveSplashScreenRecord(appToken, taskId, view, suggestType);
- // Do not wait for background color
- return false;
- }
-
- @Override
- protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo,
- boolean immediately) {
- // listen for removeView
- if (mAddWindowForTask == removalInfo.taskId) {
- mAddWindowForTask = 0;
- }
- mStartingWindowRecords.remove(removalInfo.taskId);
- }
- }
+ StartingSurfaceDrawer mStartingSurfaceDrawer;
private static class TestContext extends TestableContext {
TestContext(Context context) {
@@ -165,44 +134,51 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
doReturn(metrics).when(mMockWindowManager).getMaximumWindowMetrics();
doNothing().when(mMockWindowManager).addView(any(), any());
mTestExecutor = new HandlerExecutor(mTestHandler);
+ mStartingSurfaceDrawer = new StartingSurfaceDrawer(mTestContext, mTestExecutor,
+ mIconProvider, mTransactionPool);
mStartingSurfaceDrawer = spy(
- new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider,
+ new StartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider,
mTransactionPool));
+ spyOn(mStartingSurfaceDrawer.mSplashscreenWindowCreator);
+ spyOn(mStartingSurfaceDrawer.mWindowRecords);
+ spyOn(mStartingSurfaceDrawer.mWindowlessRecords);
}
@Test
public void testAddSplashScreenSurface() {
final int taskId = 1;
final StartingWindowInfo windowInfo =
- createWindowInfo(taskId, android.R.style.Theme);
- mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder,
+ createWindowInfo(taskId, android.R.style.Theme, mBinder);
+ mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo,
STARTING_WINDOW_TYPE_SPLASH_SCREEN);
waitHandlerIdle(mTestHandler);
- verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(),
+ verify(mStartingSurfaceDrawer.mSplashscreenWindowCreator).addWindow(
+ eq(taskId), eq(mBinder), any(), any(), any(),
eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN));
- assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId);
StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo();
removalInfo.taskId = windowInfo.taskInfo.taskId;
mStartingSurfaceDrawer.removeStartingWindow(removalInfo);
waitHandlerIdle(mTestHandler);
- verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(false));
- assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0);
+ verify(mStartingSurfaceDrawer.mWindowRecords).removeWindow(any(), eq(false));
+ assertEquals(mStartingSurfaceDrawer.mWindowRecords.recordSize(), 0);
}
@Test
public void testFallbackDefaultTheme() {
final int taskId = 1;
final StartingWindowInfo windowInfo =
- createWindowInfo(taskId, 0);
+ createWindowInfo(taskId, 0, mBinder);
final int[] theme = new int[1];
doAnswer(invocation -> theme[0] = (Integer) invocation.callRealMethod())
- .when(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any());
+ .when(mStartingSurfaceDrawer.mSplashscreenWindowCreator)
+ .getSplashScreenTheme(eq(0), any());
- mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder,
+ mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo,
STARTING_WINDOW_TYPE_SPLASH_SCREEN);
waitHandlerIdle(mTestHandler);
- verify(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any());
+ verify(mStartingSurfaceDrawer.mSplashscreenWindowCreator)
+ .getSplashScreenTheme(eq(0), any());
assertNotEquals(theme[0], 0);
}
@@ -241,7 +217,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
public void testRemoveTaskSnapshotWithImeSurfaceWhenOnImeDrawn() throws Exception {
final int taskId = 1;
final StartingWindowInfo windowInfo =
- createWindowInfo(taskId, android.R.style.Theme);
+ createWindowInfo(taskId, android.R.style.Theme, mBinder);
TaskSnapshot snapshot = createTaskSnapshot(100, 100, new Point(100, 100),
new Rect(0, 0, 0, 50), true /* hasImeSurface */);
final IWindowSession session = WindowManagerGlobal.getWindowSession();
@@ -249,7 +225,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
doReturn(WindowManagerGlobal.ADD_OKAY).when(session).addToDisplay(
any() /* window */, any() /* attrs */,
anyInt() /* viewVisibility */, anyInt() /* displayId */,
- any() /* requestedVisibility */, any() /* outInputChannel */,
+ anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */,
any() /* outInsetsState */, any() /* outActiveControls */,
any() /* outAttachedFrame */, any() /* outSizeCompatScale */);
TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo,
@@ -270,7 +246,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
when(TaskSnapshotWindow.create(eq(windowInfo), eq(mBinder), eq(snapshot), any(),
any())).thenReturn(mockSnapshotWindow);
// Simulate a task snapshot window created with IME snapshot shown.
- mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, mBinder, snapshot);
+ mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, snapshot);
waitHandlerIdle(mTestHandler);
// Verify the task snapshot with IME snapshot will be removed when received the real IME
@@ -278,27 +254,36 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
// makeTaskSnapshotWindow shall call removeWindowSynced before there add a new
// StartingWindowRecord for the task.
mStartingSurfaceDrawer.onImeDrawnOnTask(1);
- verify(mStartingSurfaceDrawer, times(2))
- .removeWindowSynced(any(), eq(true));
+ verify(mStartingSurfaceDrawer.mWindowRecords, times(2))
+ .removeWindow(any(), eq(true));
}
}
@Test
public void testClearAllWindows() {
final int taskId = 1;
- final StartingWindowInfo windowInfo =
- createWindowInfo(taskId, android.R.style.Theme);
- mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder,
- STARTING_WINDOW_TYPE_SPLASH_SCREEN);
- waitHandlerIdle(mTestHandler);
- verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(),
- eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN));
- assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId);
-
+ mStartingSurfaceDrawer.mWindowRecords.addRecord(taskId,
+ new StartingSurfaceDrawer.StartingWindowRecord() {
+ @Override
+ public boolean removeIfPossible(StartingWindowRemovalInfo info,
+ boolean immediately) {
+ return true;
+ }
+ });
+ mStartingSurfaceDrawer.mWindowlessRecords.addRecord(taskId,
+ new StartingSurfaceDrawer.StartingWindowRecord() {
+ @Override
+ public boolean removeIfPossible(StartingWindowRemovalInfo info,
+ boolean immediately) {
+ return true;
+ }
+ });
mStartingSurfaceDrawer.clearAllWindows();
waitHandlerIdle(mTestHandler);
- verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(true));
- assertEquals(mStartingSurfaceDrawer.mStartingWindowRecords.size(), 0);
+ verify(mStartingSurfaceDrawer.mWindowRecords).removeWindow(any(), eq(true));
+ assertEquals(mStartingSurfaceDrawer.mWindowRecords.recordSize(), 0);
+ verify(mStartingSurfaceDrawer.mWindowlessRecords).removeWindow(any(), eq(true));
+ assertEquals(mStartingSurfaceDrawer.mWindowlessRecords.recordSize(), 0);
}
@Test
@@ -351,7 +336,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
longAppDuration, longAppDuration));
}
- private StartingWindowInfo createWindowInfo(int taskId, int themeResId) {
+ private StartingWindowInfo createWindowInfo(int taskId, int themeResId, IBinder appToken) {
StartingWindowInfo windowInfo = new StartingWindowInfo();
final ActivityInfo info = new ActivityInfo();
info.applicationInfo = new ApplicationInfo();
@@ -360,6 +345,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.topActivityInfo = info;
taskInfo.taskId = taskId;
+ windowInfo.appToken = appToken;
windowInfo.targetActivityInfo = info;
windowInfo.taskInfo = taskInfo;
windowInfo.topOpaqueWindowInsetsState = new InsetsState();
@@ -378,6 +364,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
1, HardwareBuffer.USAGE_CPU_READ_RARELY);
return new TaskSnapshot(
System.currentTimeMillis(),
+ 0 /* captureTime */,
new ComponentName("", ""), buffer,
ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
index 10dec9ef12f9..a9082a6e2585 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
@@ -81,7 +81,7 @@ public class StartingWindowControllerTests extends ShellTestCase {
doReturn(mock(Display.class)).when(mDisplayManager).getDisplay(anyInt());
doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
mShellInit = spy(new ShellInit(mMainExecutor));
- mShellController = spy(new ShellController(mShellInit, mShellCommandHandler,
+ mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
mMainExecutor));
mController = new StartingWindowController(mContext, mShellInit, mShellController,
mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
deleted file mode 100644
index 3de50bb60470..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.wm.shell.startingsurface;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-
-import android.app.ActivityManager.TaskDescription;
-import android.content.ComponentName;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorSpace;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
-import android.view.InsetsState;
-import android.view.Surface;
-import android.view.SurfaceControl;
-import android.window.TaskSnapshot;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.TestShellExecutor;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test class for {@link TaskSnapshotWindow}.
- *
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TaskSnapshotWindowTest extends ShellTestCase {
-
- private TaskSnapshotWindow mWindow;
-
- private void setupSurface(int width, int height) {
- setupSurface(width, height, new Rect(), 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
- new Rect(0, 0, width, height));
- }
-
- private void setupSurface(int width, int height, Rect contentInsets, int sysuiVis,
- int windowFlags, Rect taskBounds) {
- // Previously when constructing TaskSnapshots for this test, scale was 1.0f, so to mimic
- // this behavior set the taskSize to be the same as the taskBounds width and height. The
- // taskBounds passed here are assumed to be the same task bounds as when the snapshot was
- // taken. We assume there is no aspect ratio mismatch between the screenshot and the
- // taskBounds
- assertEquals(width, taskBounds.width());
- assertEquals(height, taskBounds.height());
- Point taskSize = new Point(taskBounds.width(), taskBounds.height());
-
- final TaskSnapshot snapshot = createTaskSnapshot(width, height, taskSize, contentInsets);
- mWindow = new TaskSnapshotWindow(new SurfaceControl(), snapshot, "Test",
- createTaskDescription(Color.WHITE, Color.RED, Color.BLUE),
- 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */,
- taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD,
- new InsetsState(), null /* clearWindow */, new TestShellExecutor());
- }
-
- private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize,
- Rect contentInsets) {
- final HardwareBuffer buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888,
- 1, HardwareBuffer.USAGE_CPU_READ_RARELY);
- return new TaskSnapshot(
- System.currentTimeMillis(),
- new ComponentName("", ""), buffer,
- ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
- Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */,
- false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN,
- 0 /* systemUiVisibility */, false /* isTranslucent */, false /* hasImeSurface */);
- }
-
- private static TaskDescription createTaskDescription(int background, int statusBar,
- int navigationBar) {
- final TaskDescription td = new TaskDescription();
- td.setBackgroundColor(background);
- td.setStatusBarColor(statusBar);
- td.setNavigationBarColor(navigationBar);
- return td;
- }
-
- @Test
- public void fillEmptyBackground_fillHorizontally() {
- setupSurface(200, 100);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(200);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 200));
- verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any());
- }
-
- @Test
- public void fillEmptyBackground_fillVertically() {
- setupSurface(100, 200);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(200);
- mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 100));
- verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(100.0f), eq(200.0f), any());
- }
-
- @Test
- public void fillEmptyBackground_fillBoth() {
- setupSurface(200, 200);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(200);
- when(mockCanvas.getHeight()).thenReturn(200);
- mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100));
- verify(mockCanvas).drawRect(eq(100.0f), eq(0.0f), eq(200.0f), eq(100.0f), any());
- verify(mockCanvas).drawRect(eq(0.0f), eq(100.0f), eq(200.0f), eq(200.0f), any());
- }
-
- @Test
- public void fillEmptyBackground_dontFill_sameSize() {
- setupSurface(100, 100);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 100, 100));
- verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
- }
-
- @Test
- public void fillEmptyBackground_dontFill_bitmapLarger() {
- setupSurface(100, 100);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawBackgroundAndBars(mockCanvas, new Rect(0, 0, 200, 200));
- verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
- }
-
- @Test
- public void testCalculateSnapshotCrop() {
- setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 0, 100, 100));
- assertEquals(new Rect(0, 0, 100, 90), mWindow.calculateSnapshotCrop());
- }
-
- @Test
- public void testCalculateSnapshotCrop_taskNotOnTop() {
- setupSurface(100, 100, new Rect(0, 10, 0, 10), 0, 0, new Rect(0, 50, 100, 150));
- assertEquals(new Rect(0, 10, 100, 90), mWindow.calculateSnapshotCrop());
- }
-
- @Test
- public void testCalculateSnapshotCrop_navBarLeft() {
- setupSurface(100, 100, new Rect(10, 10, 0, 0), 0, 0, new Rect(0, 0, 100, 100));
- assertEquals(new Rect(10, 0, 100, 100), mWindow.calculateSnapshotCrop());
- }
-
- @Test
- public void testCalculateSnapshotCrop_navBarRight() {
- setupSurface(100, 100, new Rect(0, 10, 10, 0), 0, 0, new Rect(0, 0, 100, 100));
- assertEquals(new Rect(0, 0, 90, 100), mWindow.calculateSnapshotCrop());
- }
-
- @Test
- public void testCalculateSnapshotCrop_waterfall() {
- setupSurface(100, 100, new Rect(5, 10, 5, 10), 0, 0, new Rect(0, 0, 100, 100));
- assertEquals(new Rect(5, 0, 95, 90), mWindow.calculateSnapshotCrop());
- }
-
- @Test
- public void testCalculateSnapshotFrame() {
- setupSurface(100, 100);
- final Rect insets = new Rect(0, 10, 0, 10);
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- assertEquals(new Rect(0, 0, 100, 80),
- mWindow.calculateSnapshotFrame(new Rect(0, 10, 100, 90)));
- }
-
- @Test
- public void testCalculateSnapshotFrame_navBarLeft() {
- setupSurface(100, 100);
- final Rect insets = new Rect(10, 10, 0, 0);
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- assertEquals(new Rect(10, 0, 100, 90),
- mWindow.calculateSnapshotFrame(new Rect(10, 10, 100, 100)));
- }
-
- @Test
- public void testCalculateSnapshotFrame_waterfall() {
- setupSurface(100, 100, new Rect(5, 10, 5, 10), 0, 0, new Rect(0, 0, 100, 100));
- final Rect insets = new Rect(0, 10, 0, 10);
- mWindow.setFrames(new Rect(5, 0, 95, 100), insets);
- assertEquals(new Rect(0, 0, 90, 90),
- mWindow.calculateSnapshotFrame(new Rect(5, 0, 95, 90)));
- }
-
- @Test
- public void testDrawStatusBarBackground() {
- setupSurface(100, 100);
- final Rect insets = new Rect(0, 10, 10, 0);
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawStatusBarBackground(mockCanvas, new Rect(0, 0, 50, 100));
- verify(mockCanvas).drawRect(eq(50.0f), eq(0.0f), eq(90.0f), eq(10.0f), any());
- }
-
- @Test
- public void testDrawStatusBarBackground_nullFrame() {
- setupSurface(100, 100);
- final Rect insets = new Rect(0, 10, 10, 0);
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawStatusBarBackground(mockCanvas, null);
- verify(mockCanvas).drawRect(eq(0.0f), eq(0.0f), eq(90.0f), eq(10.0f), any());
- }
-
- @Test
- public void testDrawStatusBarBackground_nope() {
- setupSurface(100, 100);
- final Rect insets = new Rect(0, 10, 10, 0);
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawStatusBarBackground(mockCanvas, new Rect(0, 0, 100, 100));
- verify(mockCanvas, never()).drawRect(anyInt(), anyInt(), anyInt(), anyInt(), any());
- }
-
- @Test
- public void testDrawNavigationBarBackground() {
- final Rect insets = new Rect(0, 10, 0, 10);
- setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
- new Rect(0, 0, 100, 100));
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawNavigationBarBackground(mockCanvas);
- verify(mockCanvas).drawRect(eq(new Rect(0, 90, 100, 100)), any());
- }
-
- @Test
- public void testDrawNavigationBarBackground_left() {
- final Rect insets = new Rect(10, 10, 0, 0);
- setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
- new Rect(0, 0, 100, 100));
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawNavigationBarBackground(mockCanvas);
- verify(mockCanvas).drawRect(eq(new Rect(0, 0, 10, 100)), any());
- }
-
- @Test
- public void testDrawNavigationBarBackground_right() {
- final Rect insets = new Rect(0, 10, 10, 0);
- setupSurface(100, 100, insets, 0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
- new Rect(0, 0, 100, 100));
- mWindow.setFrames(new Rect(0, 0, 100, 100), insets);
- final Canvas mockCanvas = mock(Canvas.class);
- when(mockCanvas.getWidth()).thenReturn(100);
- when(mockCanvas.getHeight()).thenReturn(100);
- mWindow.drawNavigationBarBackground(mockCanvas);
- verify(mockCanvas).drawRect(eq(new Rect(90, 0, 100, 100)), any());
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index 8d92d0864338..7c520c34b29d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -78,7 +78,7 @@ public class ShellControllerTest extends ShellTestCase {
mConfigChangeListener = new TestConfigurationChangeListener();
mUserChangeListener = new TestUserChangeListener();
mExecutor = new TestShellExecutor();
- mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor);
+ mController = new ShellController(mContext, mShellInit, mShellCommandHandler, mExecutor);
mController.onConfigurationChanged(getConfigurationCopy());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
index d5bb901259bd..1b389565c066 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell;
+package com.android.wm.shell.taskview;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
@@ -53,6 +53,8 @@ import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.HandlerExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable;
@@ -92,6 +94,7 @@ public class TaskViewTest extends ShellTestCase {
Context mContext;
TaskView mTaskView;
TaskViewTransitions mTaskViewTransitions;
+ TaskViewTaskController mTaskViewTaskController;
@Before
public void setUp() {
@@ -125,7 +128,9 @@ public class TaskViewTest extends ShellTestCase {
doReturn(true).when(mTransitions).isRegistered();
}
mTaskViewTransitions = spy(new TaskViewTransitions(mTransitions));
- mTaskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue);
+ mTaskViewTaskController = spy(new TaskViewTaskController(mContext, mOrganizer,
+ mTaskViewTransitions, mSyncQueue));
+ mTaskView = new TaskView(mContext, mTaskViewTaskController);
mTaskView.setListener(mExecutor, mViewListener);
}
@@ -138,7 +143,8 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testSetPendingListener_throwsException() {
- TaskView taskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue);
+ TaskView taskView = new TaskView(mContext,
+ new TaskViewTaskController(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue));
taskView.setListener(mExecutor, mViewListener);
try {
taskView.setListener(mExecutor, mViewListener);
@@ -152,16 +158,17 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testStartActivity() {
ActivityOptions options = ActivityOptions.makeBasic();
- mTaskView.startActivity(mock(PendingIntent.class), null, options, new Rect(0, 0, 100, 100));
+ mTaskView.startActivity(mock(PendingIntent.class), null, options,
+ new Rect(0, 0, 100, 100));
- verify(mOrganizer).setPendingLaunchCookieListener(any(), eq(mTaskView));
+ verify(mOrganizer).setPendingLaunchCookieListener(any(), eq(mTaskViewTaskController));
assertThat(options.getLaunchWindowingMode()).isEqualTo(WINDOWING_MODE_MULTI_WINDOW);
}
@Test
public void testOnTaskAppeared_noSurface_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
verify(mViewListener, never()).onInitialized();
@@ -173,7 +180,7 @@ public class TaskViewTest extends ShellTestCase {
public void testOnTaskAppeared_withSurface_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
assertThat(mTaskView.isInitialized()).isTrue();
@@ -194,7 +201,7 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testSurfaceCreated_withTask_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
verify(mViewListener).onInitialized();
@@ -216,7 +223,7 @@ public class TaskViewTest extends ShellTestCase {
public void testSurfaceDestroyed_withTask_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
SurfaceHolder sh = mock(SurfaceHolder.class);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
mTaskView.surfaceCreated(sh);
reset(mViewListener);
mTaskView.surfaceDestroyed(sh);
@@ -227,11 +234,11 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testOnReleased_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
mTaskView.release();
- verify(mOrganizer).removeListener(eq(mTaskView));
+ verify(mOrganizer).removeListener(eq(mTaskViewTaskController));
verify(mViewListener).onReleased();
assertThat(mTaskView.isInitialized()).isFalse();
}
@@ -239,9 +246,9 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testOnTaskVanished_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
- mTaskView.onTaskVanished(mTaskInfo);
+ mTaskViewTaskController.onTaskVanished(mTaskInfo);
verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId));
}
@@ -249,8 +256,8 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testOnBackPressedOnTaskRoot_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
- mTaskView.onBackPressedOnTaskRoot(mTaskInfo);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onBackPressedOnTaskRoot(mTaskInfo);
verify(mViewListener).onBackPressedOnTaskRoot(eq(mTaskInfo.taskId));
}
@@ -258,17 +265,17 @@ public class TaskViewTest extends ShellTestCase {
@Test
public void testSetOnBackPressedOnTaskRoot_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true));
}
@Test
public void testUnsetOnBackPressedOnTaskRoot_legacyTransitions() {
assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS);
- mTaskView.onTaskAppeared(mTaskInfo, mLeash);
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true));
- mTaskView.onTaskVanished(mTaskInfo);
+ mTaskViewTaskController.onTaskVanished(mTaskInfo);
verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false));
}
@@ -276,8 +283,9 @@ public class TaskViewTest extends ShellTestCase {
public void testOnNewTask_noSurface() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
verify(mViewListener, never()).onInitialized();
@@ -303,8 +311,9 @@ public class TaskViewTest extends ShellTestCase {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
@@ -314,15 +323,17 @@ public class TaskViewTest extends ShellTestCase {
public void testSurfaceCreated_withTask() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
verify(mViewListener).onInitialized();
- verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskView), eq(true));
+ verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskViewTaskController), eq(true));
- mTaskView.prepareOpenAnimation(false /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(false /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true));
}
@@ -342,15 +353,16 @@ public class TaskViewTest extends ShellTestCase {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
SurfaceHolder sh = mock(SurfaceHolder.class);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
mTaskView.surfaceCreated(sh);
reset(mViewListener);
mTaskView.surfaceDestroyed(sh);
- verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskView), eq(false));
+ verify(mTaskViewTransitions).setTaskViewVisible(eq(mTaskViewTaskController), eq(false));
- mTaskView.prepareHideAnimation(new SurfaceControl.Transaction());
+ mTaskViewTaskController.prepareHideAnimation(new SurfaceControl.Transaction());
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false));
}
@@ -359,25 +371,27 @@ public class TaskViewTest extends ShellTestCase {
public void testOnReleased() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
mTaskView.release();
- verify(mOrganizer).removeListener(eq(mTaskView));
+ verify(mOrganizer).removeListener(eq(mTaskViewTaskController));
verify(mViewListener).onReleased();
assertThat(mTaskView.isInitialized()).isFalse();
- verify(mTaskViewTransitions).removeTaskView(eq(mTaskView));
+ verify(mTaskViewTransitions).removeTaskView(eq(mTaskViewTaskController));
}
@Test
public void testOnTaskVanished() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
- mTaskView.prepareCloseAnimation();
+ mTaskViewTaskController.prepareCloseAnimation();
verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId));
}
@@ -386,9 +400,10 @@ public class TaskViewTest extends ShellTestCase {
public void testOnBackPressedOnTaskRoot() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
- mTaskView.onBackPressedOnTaskRoot(mTaskInfo);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
+ mTaskViewTaskController.onBackPressedOnTaskRoot(mTaskInfo);
verify(mViewListener).onBackPressedOnTaskRoot(eq(mTaskInfo.taskId));
}
@@ -397,8 +412,9 @@ public class TaskViewTest extends ShellTestCase {
public void testSetOnBackPressedOnTaskRoot() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true));
}
@@ -406,11 +422,12 @@ public class TaskViewTest extends ShellTestCase {
public void testUnsetOnBackPressedOnTaskRoot() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
WindowContainerTransaction wct = new WindowContainerTransaction();
- mTaskView.prepareOpenAnimation(true /* newTask */, new SurfaceControl.Transaction(),
- new SurfaceControl.Transaction(), mTaskInfo, mLeash, wct);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true));
- mTaskView.prepareCloseAnimation();
+ mTaskViewTaskController.prepareCloseAnimation();
verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false));
}
@@ -454,4 +471,96 @@ public class TaskViewTest extends ShellTestCase {
assertThat(insetsInfo.touchableRegion.contains(20, 20)).isFalse();
assertThat(insetsInfo.touchableRegion.contains(30, 30)).isFalse();
}
+
+ @Test
+ public void testTaskViewPrepareOpenAnimationSetsBoundsAndVisibility() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ TaskViewBase taskViewBase = mock(TaskViewBase.class);
+ Rect bounds = new Rect(0, 0, 100, 100);
+ when(taskViewBase.getCurrentBoundsOnScreen()).thenReturn(bounds);
+ mTaskViewTaskController.setTaskViewBase(taskViewBase);
+
+ // Surface created, but task not available so bounds / visibility isn't set
+ mTaskView.surfaceCreated(mock(SurfaceHolder.class));
+ verify(mTaskViewTransitions, never()).updateVisibilityState(
+ eq(mTaskViewTaskController), eq(true));
+
+ // Make the task available / start prepareOpen
+ WindowContainerTransaction wct = mock(WindowContainerTransaction.class);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
+
+ // Bounds got set
+ verify(wct).setBounds(any(WindowContainerToken.class), eq(bounds));
+ // Visibility & bounds state got set
+ verify(mTaskViewTransitions).updateVisibilityState(eq(mTaskViewTaskController), eq(true));
+ verify(mTaskViewTransitions).updateBoundsState(eq(mTaskViewTaskController), eq(bounds));
+ }
+
+ @Test
+ public void testTaskViewPrepareOpenAnimationSetsVisibilityFalse() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ TaskViewBase taskViewBase = mock(TaskViewBase.class);
+ Rect bounds = new Rect(0, 0, 100, 100);
+ when(taskViewBase.getCurrentBoundsOnScreen()).thenReturn(bounds);
+ mTaskViewTaskController.setTaskViewBase(taskViewBase);
+
+ // Task is available, but the surface was never created
+ WindowContainerTransaction wct = mock(WindowContainerTransaction.class);
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
+
+ // Bounds do not get set as there is no surface
+ verify(wct, never()).setBounds(any(WindowContainerToken.class), any());
+ // Visibility is set to false, bounds aren't set
+ verify(mTaskViewTransitions).updateVisibilityState(eq(mTaskViewTaskController), eq(false));
+ verify(mTaskViewTransitions, never()).updateBoundsState(eq(mTaskViewTaskController), any());
+ }
+
+ @Test
+ public void testRemoveTaskView_noTask() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskView.removeTask();
+ verify(mTaskViewTransitions, never()).closeTaskView(any(), any());
+ }
+
+ @Test
+ public void testRemoveTaskView() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskView.surfaceCreated(mock(SurfaceHolder.class));
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+ new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+ mLeash, wct);
+
+ verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
+
+ mTaskView.removeTask();
+ verify(mTaskViewTransitions).closeTaskView(any(), eq(mTaskViewTaskController));
+ }
+
+ @Test
+ public void testOnTaskAppearedWithTaskNotFound() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTaskController.setTaskNotFound();
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
+
+ verify(mTaskViewTaskController).cleanUpPendingTask();
+ verify(mTaskViewTransitions).closeTaskView(any(), eq(mTaskViewTaskController));
+ }
+
+ @Test
+ public void testOnTaskAppeared_withoutTaskNotFound() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
+ verify(mTaskViewTaskController, never()).cleanUpPendingTask();
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java
new file mode 100644
index 000000000000..03ed18c86608
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java
@@ -0,0 +1,333 @@
+/*
+ * 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.taskview;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class TaskViewTransitionsTest extends ShellTestCase {
+
+ @Mock
+ Transitions mTransitions;
+ @Mock
+ TaskViewTaskController mTaskViewTaskController;
+ @Mock
+ ActivityManager.RunningTaskInfo mTaskInfo;
+ @Mock
+ WindowContainerToken mToken;
+ @Mock
+ TaskViewTaskController mTaskViewTaskController2;
+ @Mock
+ ActivityManager.RunningTaskInfo mTaskInfo2;
+ @Mock
+ WindowContainerToken mToken2;
+
+ TaskViewTransitions mTaskViewTransitions;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ doReturn(true).when(mTransitions).isRegistered();
+ }
+
+ mTaskInfo = new ActivityManager.RunningTaskInfo();
+ mTaskInfo.token = mToken;
+ mTaskInfo.taskId = 314;
+ mTaskInfo.taskDescription = mock(ActivityManager.TaskDescription.class);
+ when(mTaskViewTaskController.getTaskInfo()).thenReturn(mTaskInfo);
+
+ mTaskInfo2 = new ActivityManager.RunningTaskInfo();
+ mTaskInfo2.token = mToken2;
+ mTaskInfo2.taskId = 315;
+ mTaskInfo2.taskDescription = mock(ActivityManager.TaskDescription.class);
+ when(mTaskViewTaskController2.getTaskInfo()).thenReturn(mTaskInfo2);
+
+ mTaskViewTransitions = spy(new TaskViewTransitions(mTransitions));
+ mTaskViewTransitions.addTaskView(mTaskViewTaskController);
+ }
+
+ @Test
+ public void testSetTaskBounds_taskNotVisible_noTransaction() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, false);
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+
+ assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE))
+ .isNull();
+ }
+
+ @Test
+ public void testSetTaskBounds_taskVisible_boundsChangeTransaction() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true);
+
+ // Consume the pending transaction from visibility change
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending).isNotNull();
+ mTaskViewTransitions.startAnimation(pending.mClaimed,
+ mock(TransitionInfo.class),
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+ // Verify it was consumed
+ TaskViewTransitions.PendingTransition pending2 =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending2).isNull();
+
+ // Test that set bounds creates a new transaction
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE))
+ .isNotNull();
+ }
+
+ @Test
+ public void testSetTaskBounds_taskVisibleWithPendingOpen_noTransaction() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true);
+
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending).isNotNull();
+
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE))
+ .isNull();
+ }
+
+ @Test
+ public void testSetTaskBounds_taskVisibleWithPendingChange_transition() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true);
+
+ // Consume the pending transition from visibility change
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending).isNotNull();
+ mTaskViewTransitions.startAnimation(pending.mClaimed,
+ mock(TransitionInfo.class),
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+ // Verify it was consumed
+ TaskViewTransitions.PendingTransition checkPending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(checkPending).isNull();
+
+ // Test that set bounds creates a new transition
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE))
+ .isNotNull();
+
+ // Test that set bounds again (with different bounds) creates another transition
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 300, 200));
+ List<TaskViewTransitions.PendingTransition> pendingList =
+ mTaskViewTransitions.findAllPending(mTaskViewTaskController)
+ .stream()
+ .filter(pendingTransition -> pendingTransition.mType == TRANSIT_CHANGE)
+ .toList();
+ assertThat(pendingList.size()).isEqualTo(2);
+ }
+
+ @Test
+ public void testSetTaskBounds_sameBounds_noTransaction() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true);
+
+ // Consume the pending transaction from visibility change
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending).isNotNull();
+ mTaskViewTransitions.startAnimation(pending.mClaimed,
+ mock(TransitionInfo.class),
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+ // Verify it was consumed
+ TaskViewTransitions.PendingTransition pending2 =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending2).isNull();
+
+ // Test that set bounds creates a new transaction
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ TaskViewTransitions.PendingTransition pendingBounds =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE);
+ assertThat(pendingBounds).isNotNull();
+
+ // Test that setting same bounds with in-flight transition doesn't cause another one
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ List<TaskViewTransitions.PendingTransition> pendingList =
+ mTaskViewTransitions.findAllPending(mTaskViewTaskController)
+ .stream()
+ .filter(pendingTransition -> pendingTransition.mType == TRANSIT_CHANGE)
+ .toList();
+ assertThat(pendingList.size()).isEqualTo(1);
+
+ // Consume the pending bounds transaction
+ mTaskViewTransitions.startAnimation(pendingBounds.mClaimed,
+ mock(TransitionInfo.class),
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+ // Verify it was consumed
+ TaskViewTransitions.PendingTransition pendingBounds1 =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE);
+ assertThat(pendingBounds1).isNull();
+
+ // Test that setting the same bounds doesn't creates a new transaction
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ TaskViewTransitions.PendingTransition pendingBounds2 =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE);
+ assertThat(pendingBounds2).isNull();
+ }
+
+
+ @Test
+ public void testSetTaskBounds_taskVisibleWithDifferentTaskViewPendingChange_transition() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.addTaskView(mTaskViewTaskController2);
+
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true);
+
+ // Consume the pending transition from visibility change
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending).isNotNull();
+ mTaskViewTransitions.startAnimation(pending.mClaimed,
+ mock(TransitionInfo.class),
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+ // Verify it was consumed
+ TaskViewTransitions.PendingTransition checkPending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(checkPending).isNull();
+
+ // Set the second taskview as visible & check that it has a pending transition
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController2, true);
+ TaskViewTransitions.PendingTransition pending2 =
+ mTaskViewTransitions.findPending(mTaskViewTaskController2, TRANSIT_TO_FRONT);
+ assertThat(pending2).isNotNull();
+
+ // Test that set bounds on the first taskview will create a new transition
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE))
+ .isNotNull();
+ }
+
+ @Test
+ public void testSetTaskVisibility_taskRemoved_noNPE() {
+ mTaskViewTransitions.removeTaskView(mTaskViewTaskController);
+
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, false);
+ }
+
+ @Test
+ public void testSetTaskBounds_taskRemoved_noNPE() {
+ mTaskViewTransitions.removeTaskView(mTaskViewTaskController);
+
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.setTaskBounds(mTaskViewTaskController,
+ new Rect(0, 0, 100, 100));
+ }
+
+ @Test
+ public void test_startAnimation_setsTaskNotFound() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ when(change.getTaskInfo()).thenReturn(mTaskInfo);
+ when(change.getMode()).thenReturn(TRANSIT_OPEN);
+
+ List<TransitionInfo.Change> changes = new ArrayList<>();
+ changes.add(change);
+
+ TransitionInfo info = mock(TransitionInfo.class);
+ when(info.getChanges()).thenReturn(changes);
+
+ mTaskViewTransitions.startTaskView(new WindowContainerTransaction(),
+ mTaskViewTaskController,
+ mock(IBinder.class));
+
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPendingOpeningTransition(mTaskViewTaskController);
+
+ mTaskViewTransitions.startAnimation(pending.mClaimed,
+ info,
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+
+ verify(mTaskViewTaskController).setTaskNotFound();
+ }
+}
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 595c3b4880df..ff380e92322d 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
@@ -17,6 +17,7 @@
package com.android.wm.shell.transition;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
@@ -30,10 +31,12 @@ import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
+import static android.window.TransitionInfo.FLAG_SYNC;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -45,6 +48,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.clearInvocations;
@@ -55,12 +59,19 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager.RunningTaskInfo;
+import android.app.IApplicationThread;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.view.IRecentsAnimationRunner;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -83,20 +94,25 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.TransitionInfoBuilder;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.sysui.ShellSharedConstants;
+import com.android.wm.shell.util.StubTransaction;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Answers;
import org.mockito.InOrder;
import java.util.ArrayList;
+import java.util.function.Function;
/**
* Tests for the shell transitions.
@@ -119,8 +135,8 @@ public class ShellTransitionTests extends ShellTestCase {
@Before
public void setUp() {
- doAnswer(invocation -> invocation.getArguments()[1])
- .when(mOrganizer).startTransition(any(), any());
+ doAnswer(invocation -> new Binder())
+ .when(mOrganizer).startNewTransition(anyInt(), any());
}
@Test
@@ -155,8 +171,8 @@ public class ShellTransitionTests extends ShellTestCase {
verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken, info, new StubTransaction(),
+ new StubTransaction());
assertEquals(1, mDefaultHandler.activeCount());
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
@@ -205,8 +221,8 @@ public class ShellTransitionTests extends ShellTestCase {
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
verify(mOrganizer, times(1)).startTransition(eq(transitToken), isNull());
- transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken, open, new StubTransaction(),
+ new StubTransaction());
assertEquals(1, mDefaultHandler.activeCount());
assertEquals(0, testHandler.activeCount());
mDefaultHandler.finishAll();
@@ -221,8 +237,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */));
verify(mOrganizer, times(1)).startTransition(
eq(transitToken), eq(handlerWCT));
- transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken, open, new StubTransaction(),
+ new StubTransaction());
assertEquals(1, mDefaultHandler.activeCount());
assertEquals(0, testHandler.activeCount());
mDefaultHandler.finishAll();
@@ -239,8 +255,8 @@ public class ShellTransitionTests extends ShellTestCase {
eq(transitToken), eq(handlerWCT));
TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(TRANSIT_CHANGE).build();
- transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken, change, new StubTransaction(),
+ new StubTransaction());
assertEquals(0, mDefaultHandler.activeCount());
assertEquals(1, testHandler.activeCount());
assertEquals(0, topHandler.activeCount());
@@ -273,12 +289,12 @@ public class ShellTransitionTests extends ShellTestCase {
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */,
- new RemoteTransition(testRemote)));
+ new RemoteTransition(testRemote, "Test")));
verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken, info, new StubTransaction(),
+ new StubTransaction());
assertEquals(0, mDefaultHandler.activeCount());
assertTrue(remoteCalled[0]);
mDefaultHandler.finishAll();
@@ -418,7 +434,7 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
- transitions.registerRemote(filter, new RemoteTransition(testRemote));
+ transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test"));
mMainExecutor.flushAll();
IBinder transitToken = new Binder();
@@ -427,8 +443,8 @@ public class ShellTransitionTests extends ShellTestCase {
verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken, info, new StubTransaction(),
+ new StubTransaction());
assertEquals(0, mDefaultHandler.activeCount());
assertTrue(remoteCalled[0]);
mDefaultHandler.finishAll();
@@ -462,11 +478,12 @@ public class ShellTransitionTests extends ShellTestCase {
final int transitType = TRANSIT_FIRST_CUSTOM + 1;
OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor,
- new RemoteTransition(testRemote));
+ new RemoteTransition(testRemote, "Test"));
// Verify that it responds to the remote but not other things.
IBinder transitToken = new Binder();
assertNotNull(oneShot.handleRequest(transitToken,
- new TransitionRequestInfo(transitType, null, new RemoteTransition(testRemote))));
+ new TransitionRequestInfo(transitType, null,
+ new RemoteTransition(testRemote, "Test"))));
assertNull(oneShot.handleRequest(transitToken,
new TransitionRequestInfo(transitType, null, null)));
@@ -476,10 +493,10 @@ public class ShellTransitionTests extends ShellTestCase {
oneShot.setTransition(transitToken);
IBinder anotherToken = new Binder();
assertFalse(oneShot.startAnimation(anotherToken, new TransitionInfo(transitType, 0),
- mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
+ new StubTransaction(), new StubTransaction(),
testFinish));
assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0),
- mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
+ new StubTransaction(), new StubTransaction(),
testFinish));
}
@@ -493,8 +510,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken1, info1, new StubTransaction(),
+ new StubTransaction());
assertEquals(1, mDefaultHandler.activeCount());
IBinder transitToken2 = new Binder();
@@ -502,8 +519,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */));
TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken2, info2, new StubTransaction(),
+ new StubTransaction());
// default handler doesn't merge by default, so it shouldn't increment active count.
assertEquals(1, mDefaultHandler.activeCount());
assertEquals(0, mDefaultHandler.mergeCount());
@@ -534,8 +551,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken1, info1, new StubTransaction(),
+ new StubTransaction());
assertEquals(1, mDefaultHandler.activeCount());
IBinder transitToken2 = new Binder();
@@ -543,8 +560,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */));
TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken2, info2, new StubTransaction(),
+ new StubTransaction());
// it should still only have 1 active, but then show 1 merged
assertEquals(1, mDefaultHandler.activeCount());
assertEquals(1, mDefaultHandler.mergeCount());
@@ -561,6 +578,122 @@ public class ShellTransitionTests extends ShellTestCase {
assertEquals(0, mDefaultHandler.activeCount());
}
+
+ @Test
+ public void testTransitionMergingOnFinish() {
+ final Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ // The current transition.
+ final IBinder transitToken1 = new Binder();
+ requestStartTransition(transitions, transitToken1);
+ onTransitionReady(transitions, transitToken1);
+
+ // The next ready transition.
+ final IBinder transitToken2 = new Binder();
+ requestStartTransition(transitions, transitToken2);
+ onTransitionReady(transitions, transitToken2);
+
+ // The non-ready merge candidate.
+ final IBinder transitTokenNotReady = new Binder();
+ requestStartTransition(transitions, transitTokenNotReady);
+
+ mDefaultHandler.setSimulateMerge(true);
+ mDefaultHandler.mFinishes.get(0).second.onTransitionFinished(
+ null /* wct */, null /* wctCB */);
+
+ // Make sure that the non-ready transition is not merged.
+ assertEquals(0, mDefaultHandler.mergeCount());
+ }
+
+ @Test
+ public void testInterleavedMerging() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ Function<Boolean, IBinder> startATransition = (doMerge) -> {
+ IBinder token = new Binder();
+ if (doMerge) {
+ mDefaultHandler.setShouldMerge(token);
+ }
+ transitions.requestStartTransition(token,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ transitions.onTransitionReady(token, info, new StubTransaction(),
+ new StubTransaction());
+ return token;
+ };
+
+ IBinder transitToken1 = startATransition.apply(false);
+ // merge first one
+ IBinder transitToken2 = startATransition.apply(true);
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, mDefaultHandler.mergeCount());
+
+ // don't merge next one
+ IBinder transitToken3 = startATransition.apply(false);
+ // make sure nothing happened (since it wasn't merged)
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, mDefaultHandler.mergeCount());
+
+ // make a mergable
+ IBinder transitToken4 = startATransition.apply(true);
+ // make sure nothing happened since there is a non-mergable pending.
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, mDefaultHandler.mergeCount());
+
+ // Queue up another mergable
+ IBinder transitToken5 = startATransition.apply(true);
+
+ // Queue up a non-mergable
+ IBinder transitToken6 = startATransition.apply(false);
+
+ // Our active now looks like: [playing, merged]
+ // and ready queue: [non-mergable, mergable, mergable, non-mergable]
+ // finish the playing one
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+ // Now we should have the non-mergable playing now with 2 merged:
+ // active: [playing, merged, merged] queue: [non-mergable]
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(2, mDefaultHandler.mergeCount());
+
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(0, mDefaultHandler.mergeCount());
+
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+ }
+
+ @Test
+ public void testTransitionOrderMatchesCore() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ IBinder transitToken = new Binder();
+ IBinder shellInit = transitions.startTransition(TRANSIT_CLOSE,
+ new WindowContainerTransaction(), null /* handler */);
+ // make sure we are testing the "New" API.
+ verify(mOrganizer, times(1)).startNewTransition(eq(TRANSIT_CLOSE), any());
+ // WMCore may not receive the new transition before requesting its own.
+ transitions.requestStartTransition(transitToken,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
+
+ // At this point, WM is working on its transition (the shell-initialized one is still
+ // queued), so continue the transition lifecycle for that.
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ transitions.onTransitionReady(transitToken, info, new StubTransaction(),
+ new StubTransaction());
+ // At this point, if things are not working, we'd get an NPE due to attempting to merge
+ // into the shellInit transition which hasn't started yet.
+ assertEquals(1, mDefaultHandler.activeCount());
+ }
+
@Test
public void testShouldRotateSeamlessly() throws Exception {
final RunningTaskInfo taskInfo =
@@ -569,8 +702,8 @@ public class ShellTransitionTests extends ShellTestCase {
createTaskInfo(1, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
final DisplayController displays = createTestDisplayController();
- final @Surface.Rotation int upsideDown = displays
- .getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation();
+ final DisplayLayout displayLayout = displays.getDisplayLayout(DEFAULT_DISPLAY);
+ final @Surface.Rotation int upsideDown = displayLayout.getUpsideDownRotation();
TransitionInfo.Change displayChange = new ChangeBuilder(TRANSIT_CHANGE)
.setFlags(FLAG_IS_DISPLAY).setRotate().build();
@@ -610,7 +743,8 @@ public class ShellTransitionTests extends ShellTestCase {
assertEquals(ROTATION_ANIMATION_ROTATE, DefaultTransitionHandler.getRotationAnimationHint(
displayChange, noTask, displays));
- // Not seamless if one of rotations is upside-down
+ // Not seamless if the nav bar cares rotation and one of rotations is upside-down.
+ doReturn(false).when(displayLayout).allowSeamlessRotationDespiteNavBarMoving();
displayChange = new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
.setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build();
final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE)
@@ -667,8 +801,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken1, info1, new StubTransaction(),
+ new StubTransaction());
assertEquals(1, mDefaultHandler.activeCount());
transitions.runOnIdle(runnable2);
@@ -682,8 +816,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */));
TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class),
- mock(SurfaceControl.Transaction.class));
+ transitions.onTransitionReady(transitToken2, info2, new StubTransaction(),
+ new StubTransaction());
assertEquals(1, mDefaultHandler.activeCount());
mDefaultHandler.finishAll();
@@ -734,8 +868,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT = new StubTransaction();
+ SurfaceControl.Transaction finishT = new StubTransaction();
transitions.onTransitionReady(transitToken, info, startT, finishT);
InOrder observerOrder = inOrder(observer);
@@ -759,8 +893,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT1 = new StubTransaction();
+ SurfaceControl.Transaction finishT1 = new StubTransaction();
transitions.onTransitionReady(transitToken1, info1, startT1, finishT1);
verify(observer).onTransitionReady(transitToken1, info1, startT1, finishT1);
@@ -769,8 +903,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */));
TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT2 = new StubTransaction();
+ SurfaceControl.Transaction finishT2 = new StubTransaction();
transitions.onTransitionReady(transitToken2, info2, startT2, finishT2);
verify(observer, times(1)).onTransitionReady(transitToken2, info2, startT2, finishT2);
verify(observer, times(0)).onTransitionStarting(transitToken2);
@@ -803,8 +937,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT1 = new StubTransaction();
+ SurfaceControl.Transaction finishT1 = new StubTransaction();
transitions.onTransitionReady(transitToken1, info1, startT1, finishT1);
IBinder transitToken2 = new Binder();
@@ -812,8 +946,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */));
TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
- SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT2 = new StubTransaction();
+ SurfaceControl.Transaction finishT2 = new StubTransaction();
transitions.onTransitionReady(transitToken2, info2, startT2, finishT2);
InOrder observerOrder = inOrder(observer);
@@ -875,8 +1009,8 @@ public class ShellTransitionTests extends ShellTestCase {
new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */));
TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(TRANSIT_CHANGE).build();
- SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT1 = new StubTransaction();
+ SurfaceControl.Transaction finishT1 = new StubTransaction();
transitions.onTransitionReady(transitToken1, change, startT1, finishT1);
// Request the second transition that should be handled by the default handler
@@ -885,8 +1019,8 @@ public class ShellTransitionTests extends ShellTestCase {
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
transitions.requestStartTransition(transitToken2,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
- SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT2 = new StubTransaction();
+ SurfaceControl.Transaction finishT2 = new StubTransaction();
transitions.onTransitionReady(transitToken2, open, startT2, finishT2);
verify(observer).onTransitionReady(transitToken2, open, startT2, finishT2);
verify(observer, times(0)).onTransitionStarting(transitToken2);
@@ -895,8 +1029,8 @@ public class ShellTransitionTests extends ShellTestCase {
IBinder transitToken3 = new Binder();
transitions.requestStartTransition(transitToken3,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
- SurfaceControl.Transaction startT3 = mock(SurfaceControl.Transaction.class);
- SurfaceControl.Transaction finishT3 = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction startT3 = new StubTransaction();
+ SurfaceControl.Transaction finishT3 = new StubTransaction();
transitions.onTransitionReady(transitToken3, open, startT3, finishT3);
verify(observer, times(0)).onTransitionStarting(transitToken2);
verify(observer).onTransitionReady(transitToken3, open, startT3, finishT3);
@@ -920,41 +1054,338 @@ public class ShellTransitionTests extends ShellTestCase {
verify(observer, times(0)).onTransitionFinished(eq(transitToken3), anyBoolean());
}
- class TransitionInfoBuilder {
- final TransitionInfo mInfo;
+ @Test
+ public void testTransitSleep_squashesRecents() {
+ ShellInit shellInit = new ShellInit(mMainExecutor);
+ final Transitions transitions =
+ new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer,
+ mTransactionPool, createTestDisplayController(), mMainExecutor,
+ mMainHandler, mAnimExecutor);
+ final RecentsTransitionHandler recentsHandler =
+ new RecentsTransitionHandler(shellInit, transitions, null);
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+ shellInit.init();
- TransitionInfoBuilder(@WindowManager.TransitionType int type) {
- this(type, 0 /* flags */);
- }
+ Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class);
+ transitions.registerObserver(observer);
- TransitionInfoBuilder(@WindowManager.TransitionType int type,
- @WindowManager.TransitionFlags int flags) {
- mInfo = new TransitionInfo(type, flags);
- mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0);
- }
+ RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_RECENTS);
+ RunningTaskInfo task2 = createTaskInfo(2);
+
+ // Start an open transition for the purpose of occupying the ready queue
+ final IBinder transitOpen1 = new Binder("transitOpen1");
+ final TransitionInfo infoOpen1 =
+ new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, task1)
+ .build();
+ mMainExecutor.execute(() -> {
+ transitions.requestStartTransition(transitOpen1, new TransitionRequestInfo(
+ TRANSIT_OPEN, task1 /* trigger */, null /* remote */));
+ onTransitionReady(transitions, transitOpen1, infoOpen1);
+ });
+
+ // First transition on the queue should start immediately.
+ mMainExecutor.flushAll();
+ verify(observer).onTransitionReady(eq(transitOpen1), any(), any(), any());
+ verify(observer).onTransitionStarting(eq(transitOpen1));
+
+ // Start recents
+ final IRecentsAnimationRunner recentsListener =
+ mock(IRecentsAnimationRunner.class, Answers.RETURNS_DEEP_STUBS);
+ final IBinder transitRecents = recentsHandler.startRecentsTransition(
+ mock(PendingIntent.class) /* intent */,
+ mock(Intent.class) /* fillIn */,
+ new Bundle() /* options */,
+ mock(IApplicationThread.class) /* appThread */,
+ recentsListener);
+ final TransitionInfo infoRecents =
+ new TransitionInfoBuilder(TRANSIT_TO_FRONT)
+ .addChange(TRANSIT_TO_FRONT, task1)
+ .addChange(TRANSIT_CLOSE, task2)
+ .build();
+ onTransitionReady(transitions, transitRecents, infoRecents);
+
+ // Start another open transition during recents
+ final IBinder transitOpen2 = new Binder("transitOpen2");
+ final TransitionInfo infoOpen2 =
+ new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, task2)
+ .addChange(TRANSIT_TO_BACK, task1)
+ .build();
+ mMainExecutor.execute(() -> {
+ transitions.requestStartTransition(transitOpen2, new TransitionRequestInfo(
+ TRANSIT_OPEN, task2 /* trigger */, null /* remote */));
+ onTransitionReady(transitions, transitOpen2, infoOpen2);
+ });
+
+ // Finish testOpen1 to start processing the other transitions
+ mMainExecutor.execute(() -> {
+ mDefaultHandler.finishOne();
+ });
+ mMainExecutor.flushAll();
- TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
- RunningTaskInfo taskInfo) {
- final TransitionInfo.Change change =
- new TransitionInfo.Change(null /* token */, createMockSurface(true));
- change.setMode(mode);
- change.setTaskInfo(taskInfo);
- mInfo.addChange(change);
- return this;
- }
+ // Recents transition SHOULD start, and merge the open transition, which should NOT start.
+ verify(observer).onTransitionFinished(eq(transitOpen1), eq(false) /* aborted */);
+ verify(observer).onTransitionReady(eq(transitRecents), any(), any(), any());
+ verify(observer).onTransitionStarting(eq(transitRecents));
+ verify(observer).onTransitionReady(eq(transitOpen2), any(), any(), any());
+ verify(observer).onTransitionMerged(eq(transitOpen2), eq(transitRecents));
+ // verify(observer).onTransitionFinished(eq(transitOpen2), eq(true) /* aborted */);
+
+ // Go to sleep
+ final IBinder transitSleep = new Binder("transitSleep");
+ final TransitionInfo infoSleep = new TransitionInfoBuilder(TRANSIT_SLEEP).build();
+ mMainExecutor.execute(() -> {
+ transitions.requestStartTransition(transitSleep, new TransitionRequestInfo(
+ TRANSIT_SLEEP, null /* trigger */, null /* remote */));
+ onTransitionReady(transitions, transitSleep, infoSleep);
+ });
+ mMainExecutor.flushAll();
- TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) {
- return addChange(mode, null /* taskInfo */);
- }
+ // Recents transition should finish itself when it sees the sleep transition coming.
+ verify(observer).onTransitionFinished(eq(transitRecents), eq(false));
+ verify(observer).onTransitionFinished(eq(transitSleep), eq(false));
+ }
- TransitionInfoBuilder addChange(TransitionInfo.Change change) {
- mInfo.addChange(change);
- return this;
- }
+ private void onTransitionReady(Transitions transitions, IBinder token, TransitionInfo info) {
+ transitions.onTransitionReady(token, info, new StubTransaction(),
+ new StubTransaction());
+ }
- TransitionInfo build() {
- return mInfo;
- }
+ @Test
+ public void testEmptyTransitionStillReportsKeyguardGoingAway() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ IBinder transitToken = new Binder();
+ transitions.requestStartTransition(transitToken,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+
+ // Make a no-op transition
+ TransitionInfo info = new TransitionInfoBuilder(
+ TRANSIT_OPEN, TRANSIT_FLAG_KEYGUARD_GOING_AWAY, true /* noOp */).build();
+ transitions.onTransitionReady(transitToken, info, new StubTransaction(),
+ new StubTransaction());
+
+ // If keyguard-going-away flag set, then it shouldn't be aborted.
+ assertEquals(1, mDefaultHandler.activeCount());
+ }
+
+ @Test
+ public void testMultipleTracks() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+ TestTransitionHandler alwaysMergeHandler = new TestTransitionHandler();
+ alwaysMergeHandler.setSimulateMerge(true);
+
+ final boolean[] becameIdle = new boolean[]{false};
+
+ final WindowContainerTransaction emptyWCT = new WindowContainerTransaction();
+ final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class);
+
+ // Make this always merge so we can ensure that it does NOT get a merge-attempt for a
+ // different track.
+ IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, alwaysMergeHandler);
+ // start tracking idle
+ transitions.runOnIdle(() -> becameIdle[0] = true);
+
+ IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+ IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler);
+
+ TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoA.setTrack(0);
+ TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoB.setTrack(1);
+ TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ infoC.setTrack(1);
+
+ transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT);
+ assertEquals(1, alwaysMergeHandler.activeCount());
+ transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT);
+ // should now be running in parallel
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, alwaysMergeHandler.activeCount());
+ // make sure we didn't try to merge into a different track.
+ assertEquals(0, alwaysMergeHandler.mergeCount());
+
+ // This should be queued-up since it is on track 1 (same as B)
+ transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT);
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, alwaysMergeHandler.activeCount());
+
+ // Now finish B and make sure C starts
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+
+ // Now C and A running in parallel
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, alwaysMergeHandler.activeCount());
+ assertEquals(0, alwaysMergeHandler.mergeCount());
+
+ // Finish A
+ alwaysMergeHandler.finishOne();
+ mMainExecutor.flushAll();
+
+ // C still running
+ assertEquals(0, alwaysMergeHandler.activeCount());
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertFalse(becameIdle[0]);
+
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+
+ assertEquals(0, mDefaultHandler.activeCount());
+ assertTrue(becameIdle[0]);
+ }
+
+ @Test
+ public void testSyncMultipleTracks() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+ TestTransitionHandler secondHandler = new TestTransitionHandler();
+
+ // Disable the forced early-sync-finish so that we can test the ordering mechanics.
+ transitions.setDisableForceSyncForTest(true);
+ mDefaultHandler.mFinishOnSync = false;
+ secondHandler.mFinishOnSync = false;
+
+ final WindowContainerTransaction emptyWCT = new WindowContainerTransaction();
+ final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class);
+
+ // Make this always merge so we can ensure that it does NOT get a merge-attempt for a
+ // different track.
+ IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+ IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler);
+ IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, secondHandler);
+ IBinder transitSync = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler);
+ IBinder transitD = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler);
+ IBinder transitE = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+
+ TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoA.setTrack(0);
+ TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoB.setTrack(1);
+ TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ infoC.setTrack(1);
+ TransitionInfo infoSync = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ infoSync.setTrack(0);
+ infoSync.setFlags(FLAG_SYNC);
+ TransitionInfo infoD = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoD.setTrack(1);
+ TransitionInfo infoE = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoE.setTrack(0);
+
+ // Start A B and C where A is track 0, B and C are track 1 (C should be queued)
+ transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT);
+ transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT);
+ transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT);
+ // should now be running in parallel (with one queued)
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, secondHandler.activeCount());
+
+ // Make the sync ready and the following (D, E) ready.
+ transitions.onTransitionReady(transitSync, infoSync, mockSCT, mockSCT);
+ transitions.onTransitionReady(transitD, infoD, mockSCT, mockSCT);
+ transitions.onTransitionReady(transitE, infoE, mockSCT, mockSCT);
+
+ // nothing should have happened yet since the sync is queued and blocking everything.
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, secondHandler.activeCount());
+
+ // Finish A (which is track 0 like the sync).
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+
+ // Even though the sync is on track 0 and track 0 became idle, it should NOT be started yet
+ // because it must wait for everything. Additionally, D/E shouldn't start yet either.
+ assertEquals(0, mDefaultHandler.activeCount());
+ assertEquals(1, secondHandler.activeCount());
+
+ // Now finish B and C -- this should then allow the sync to start and D to run (in parallel)
+ secondHandler.finishOne();
+ secondHandler.finishOne();
+ mMainExecutor.flushAll();
+
+ // Now the sync and D (on track 1) should be running
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, secondHandler.activeCount());
+
+ // finish the sync. track 0 still has E
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+ assertEquals(1, mDefaultHandler.activeCount());
+
+ mDefaultHandler.finishOne();
+ secondHandler.finishOne();
+ mMainExecutor.flushAll();
+
+ assertEquals(0, mDefaultHandler.activeCount());
+ assertEquals(0, secondHandler.activeCount());
+ }
+
+ @Test
+ public void testForceSyncTracks() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+ TestTransitionHandler secondHandler = new TestTransitionHandler();
+
+ final WindowContainerTransaction emptyWCT = new WindowContainerTransaction();
+ final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class);
+
+ // Make this always merge so we can ensure that it does NOT get a merge-attempt for a
+ // different track.
+ IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+ IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+ IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, secondHandler);
+ IBinder transitD = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler);
+ IBinder transitSync = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler);
+
+ TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoA.setTrack(0);
+ TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoB.setTrack(0);
+ TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ infoC.setTrack(1);
+ TransitionInfo infoD = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).build();
+ infoD.setTrack(1);
+ TransitionInfo infoSync = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ infoSync.setTrack(0);
+ infoSync.setFlags(FLAG_SYNC);
+
+ transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT);
+ transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT);
+ transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT);
+ transitions.onTransitionReady(transitD, infoD, mockSCT, mockSCT);
+ // should now be running in parallel (with one queued in each)
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(1, secondHandler.activeCount());
+
+ // Make the sync ready.
+ transitions.onTransitionReady(transitSync, infoSync, mockSCT, mockSCT);
+ mMainExecutor.flushAll();
+
+ // Everything should be forced-finish now except the sync
+ assertEquals(1, mDefaultHandler.activeCount());
+ assertEquals(0, secondHandler.activeCount());
+
+ mDefaultHandler.finishOne();
+ mMainExecutor.flushAll();
+
+ assertEquals(0, mDefaultHandler.activeCount());
}
class ChangeBuilder {
@@ -995,16 +1426,19 @@ public class ShellTransitionTests extends ShellTestCase {
}
class TestTransitionHandler implements Transitions.TransitionHandler {
- ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>();
+ ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes =
+ new ArrayList<>();
final ArrayList<IBinder> mMerged = new ArrayList<>();
boolean mSimulateMerge = false;
+ boolean mFinishOnSync = true;
+ final ArraySet<IBinder> mShouldMerge = new ArraySet<>();
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- mFinishes.add(finishCallback);
+ mFinishes.add(new Pair<>(transition, finishCallback));
return true;
}
@@ -1012,7 +1446,14 @@ public class ShellTransitionTests extends ShellTestCase {
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (!mSimulateMerge) return;
+ if (mFinishOnSync && info.getType() == TRANSIT_SLEEP) {
+ for (int i = 0; i < mFinishes.size(); ++i) {
+ if (mFinishes.get(i).first != mergeTarget) continue;
+ mFinishes.remove(i).second.onTransitionFinished(null, null);
+ return;
+ }
+ }
+ if (!(mSimulateMerge || mShouldMerge.contains(transition))) return;
mMerged.add(transition);
finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
}
@@ -1028,12 +1469,24 @@ public class ShellTransitionTests extends ShellTestCase {
mSimulateMerge = sim;
}
+ void setShouldMerge(IBinder toMerge) {
+ mShouldMerge.add(toMerge);
+ }
+
void finishAll() {
- final ArrayList<Transitions.TransitionFinishCallback> finishes = mFinishes;
+ final ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> finishes =
+ mFinishes;
mFinishes = new ArrayList<>();
for (int i = finishes.size() - 1; i >= 0; --i) {
- finishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */);
+ finishes.get(i).second.onTransitionFinished(null /* wct */, null /* wctCB */);
}
+ mShouldMerge.clear();
+ }
+
+ void finishOne() {
+ Pair<IBinder, Transitions.TransitionFinishCallback> fin = mFinishes.remove(0);
+ mMerged.clear();
+ fin.second.onTransitionFinished(null /* wct */, null /* wctCB */);
}
int activeCount() {
@@ -1045,6 +1498,21 @@ public class ShellTransitionTests extends ShellTestCase {
}
}
+ private static void requestStartTransition(Transitions transitions, IBinder token) {
+ transitions.requestStartTransition(token,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ }
+
+ private static void onTransitionReady(Transitions transitions, IBinder token) {
+ transitions.onTransitionReady(token, createTransitionInfo(),
+ new StubTransaction(), new StubTransaction());
+ }
+
+ private static TransitionInfo createTransitionInfo() {
+ return new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ }
+
private static SurfaceControl createMockSurface(boolean valid) {
SurfaceControl sc = mock(SurfaceControl.class);
doReturn(valid).when(sc).isValid();
@@ -1054,15 +1522,15 @@ public class ShellTransitionTests extends ShellTestCase {
private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, int activityType) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
+ taskInfo.topActivityType = activityType;
taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
taskInfo.configuration.windowConfiguration.setActivityType(activityType);
+ taskInfo.token = mock(WindowContainerToken.class);
return taskInfo;
}
private static RunningTaskInfo createTaskInfo(int taskId) {
- RunningTaskInfo taskInfo = new RunningTaskInfo();
- taskInfo.taskId = taskId;
- return taskInfo;
+ return createTaskInfo(taskId, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
}
private DisplayController createTestDisplayController() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/StubTransaction.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/StubTransaction.java
new file mode 100644
index 000000000000..855f5416fd0f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/StubTransaction.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.ColorSpace;
+import android.graphics.GraphicBuffer;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.hardware.HardwareBuffer;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.view.InputWindowHandle;
+import android.view.Surface;
+import android.view.SurfaceControl;
+
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+/**
+ * Stubbed {@link SurfaceControl.Transaction} class that can be used when unit
+ * testing to avoid calls to native code.
+ *
+ * Note: This is a copy of com.android.server.testutils.StubTransaction
+ */
+public class StubTransaction extends SurfaceControl.Transaction {
+
+ private HashSet<Runnable> mWindowInfosReportedListeners = new HashSet<>();
+
+ @Override
+ public void apply() {
+ for (Runnable listener : mWindowInfosReportedListeners) {
+ listener.run();
+ }
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void apply(boolean sync) {
+ apply();
+ }
+
+ @Override
+ public SurfaceControl.Transaction setVisibility(SurfaceControl sc, boolean visible) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction show(SurfaceControl sc) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction hide(SurfaceControl sc) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setPosition(SurfaceControl sc, float x, float y) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setBufferSize(SurfaceControl sc,
+ int w, int h) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setLayer(SurfaceControl sc, int z) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setRelativeLayer(SurfaceControl sc, SurfaceControl relativeTo,
+ int z) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setTransparentRegionHint(SurfaceControl sc,
+ Region transparentRegion) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setAlpha(SurfaceControl sc, float alpha) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setInputWindowInfo(SurfaceControl sc,
+ InputWindowHandle handle) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setGeometry(SurfaceControl sc, Rect sourceCrop,
+ Rect destFrame, @Surface.Rotation int orientation) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setMatrix(SurfaceControl sc,
+ float dsdx, float dtdx, float dtdy, float dsdy) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setMatrix(SurfaceControl sc, Matrix matrix, float[] float9) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setColorTransform(SurfaceControl sc, float[] matrix,
+ float[] translation) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setWindowCrop(SurfaceControl sc, Rect crop) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setWindowCrop(SurfaceControl sc, int width, int height) {
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SurfaceControl.Transaction setCrop(@NonNull SurfaceControl sc, @Nullable Rect crop) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setCornerRadius(SurfaceControl sc, float cornerRadius) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setBackgroundBlurRadius(SurfaceControl sc, int radius) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setLayerStack(SurfaceControl sc, int layerStack) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction reparent(SurfaceControl sc, SurfaceControl newParent) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setColor(SurfaceControl sc, float[] color) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setSecure(SurfaceControl sc, boolean isSecure) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setOpaque(SurfaceControl sc, boolean isOpaque) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setDisplaySurface(IBinder displayToken, Surface surface) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setDisplayLayerStack(IBinder displayToken, int layerStack) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setDisplayFlags(IBinder displayToken, int flags) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setDisplayProjection(IBinder displayToken,
+ int orientation, Rect layerStackRect, Rect displayRect) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setDisplaySize(IBinder displayToken, int width, int height) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setAnimationTransaction() {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setMetadata(SurfaceControl sc, int key, int data) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setMetadata(SurfaceControl sc, int key, Parcel data) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction merge(SurfaceControl.Transaction other) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction remove(SurfaceControl sc) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction addTransactionCommittedListener(Executor executor,
+ SurfaceControl.TransactionCommittedListener listener) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setColorSpaceAgnostic(SurfaceControl sc, boolean agnostic) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setFrameRateSelectionPriority(SurfaceControl sc,
+ int priority) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setFrameRate(SurfaceControl sc, float frameRate,
+ int compatibility, int changeFrameRateStrategy) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction unsetColor(SurfaceControl sc) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setShadowRadius(SurfaceControl sc, float shadowRadius) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setFixedTransformHint(SurfaceControl sc,
+ @Surface.Rotation int transformHint) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction unsetFixedTransformHint(@NonNull SurfaceControl sc) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setBuffer(SurfaceControl sc, GraphicBuffer buffer) {
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SurfaceControl.Transaction setBuffer(@NonNull SurfaceControl sc,
+ @Nullable HardwareBuffer buffer) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setColorSpace(SurfaceControl sc, ColorSpace colorSpace) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction setTrustedOverlay(SurfaceControl sc,
+ boolean isTrustedOverlay) {
+ return this;
+ }
+
+ @Override
+ public SurfaceControl.Transaction addWindowInfosReportedListener(@NonNull Runnable listener) {
+ mWindowInfosReportedListeners.add(listener);
+ return this;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
index 1d1aa795173c..41bab95b7dd4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -28,6 +29,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.hardware.input.InputManager;
@@ -46,10 +49,12 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopModeController;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
import org.junit.Test;
@@ -60,12 +65,14 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
/** Tests of {@link DesktopModeWindowDecorViewModel} */
@SmallTest
public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
private static final String TAG = "DesktopModeWindowDecorViewModelTests";
+ private static final Rect STABLE_INSETS = new Rect(0, 100, 0, 0);
@Mock private DesktopModeWindowDecoration mDesktopModeWindowDecoration;
@Mock private DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
@@ -74,14 +81,17 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
@Mock private Choreographer mMainChoreographer;
@Mock private ShellTaskOrganizer mTaskOrganizer;
@Mock private DisplayController mDisplayController;
+ @Mock private DisplayLayout mDisplayLayout;
@Mock private SplitScreenController mSplitScreenController;
@Mock private SyncTransactionQueue mSyncQueue;
@Mock private DesktopModeController mDesktopModeController;
@Mock private DesktopTasksController mDesktopTasksController;
@Mock private InputMonitor mInputMonitor;
@Mock private InputManager mInputManager;
-
+ @Mock private Transitions mTransitions;
@Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory;
+ @Mock private Supplier<SurfaceControl.Transaction> mTransactionFactory;
+ @Mock private SurfaceControl.Transaction mTransaction;
private final List<InputManager> mMockInputManagers = new ArrayList<>();
private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel;
@@ -98,16 +108,21 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
mTaskOrganizer,
mDisplayController,
mSyncQueue,
+ mTransitions,
Optional.of(mDesktopModeController),
Optional.of(mDesktopTasksController),
Optional.of(mSplitScreenController),
mDesktopModeWindowDecorFactory,
- mMockInputMonitorFactory
+ mMockInputMonitorFactory,
+ mTransactionFactory
);
doReturn(mDesktopModeWindowDecoration)
.when(mDesktopModeWindowDecorFactory)
.create(any(), any(), any(), any(), any(), any(), any(), any());
+ doReturn(mTransaction).when(mTransactionFactory).get();
+ doReturn(mDisplayLayout).when(mDisplayController).getDisplayLayout(anyInt());
+ doReturn(STABLE_INSETS).when(mDisplayLayout).stableInsets();
when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor);
// InputChannel cannot be mocked because it passes to InputEventReceiver.
@@ -250,7 +265,7 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
}
private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId,
- int displayId, int windowingMode) {
+ int displayId, @WindowConfiguration.WindowingMode int windowingMode) {
ActivityManager.RunningTaskInfo taskInfo =
new TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
new file mode 100644
index 000000000000..de46b31879ed
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -0,0 +1,232 @@
+/*
+ * 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.windowdecor
+
+import android.app.ActivityManager
+import android.graphics.PointF
+import android.graphics.Rect
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.window.WindowContainerToken
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [DragPositioningCallbackUtility].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DragPositioningCallbackUtilityTest
+ */
+@RunWith(AndroidTestingRunner::class)
+class DragPositioningCallbackUtilityTest {
+ @Mock
+ private lateinit var mockWindowDecoration: WindowDecoration<*>
+ @Mock
+ private lateinit var taskToken: WindowContainerToken
+ @Mock
+ private lateinit var taskBinder: IBinder
+ @Mock
+ private lateinit var mockDisplayController: DisplayController
+ @Mock
+ private lateinit var mockDisplayLayout: DisplayLayout
+ @Mock
+ private lateinit var mockDisplay: Display
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(taskToken.asBinder()).thenReturn(taskBinder)
+ whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
+ whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
+ whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+ }
+
+ mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+ taskId = TASK_ID
+ token = taskToken
+ minWidth = MIN_WIDTH
+ minHeight = MIN_HEIGHT
+ defaultMinSize = DEFAULT_MIN
+ displayId = DISPLAY_ID
+ configuration.windowConfiguration.bounds = STARTING_BOUNDS
+ }
+ mockWindowDecoration.mDisplay = mockDisplay
+ whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
+ }
+
+ @Test
+ fun testChangeBoundsDoesNotChangeHeightWhenLessThanMin() {
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+
+ // Resize to width of 95px and height of 5px with min width of 10px
+ val newX = STARTING_BOUNDS.right.toFloat() - 5
+ val newY = STARTING_BOUNDS.top.toFloat() + 95
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 5)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ fun testChangeBoundsDoesNotChangeWidthWhenLessThanMin() {
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+
+ // Resize to height of 95px and width of 5px with min width of 10px
+ val newX = STARTING_BOUNDS.right.toFloat() - 95
+ val newY = STARTING_BOUNDS.top.toFloat() + 5
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 5)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ fun testChangeBoundsDoesNotChangeHeightWhenNegative() {
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+
+ // Resize to width of 95px and width of -5px with minimum of 10px
+ val newX = STARTING_BOUNDS.right.toFloat() - 5
+ val newY = STARTING_BOUNDS.top.toFloat() + 105
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 5)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ fun testChangeBoundsRunsWhenResizeBoundsValid() {
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+
+ // Shrink to height 20px and width 20px with both min height/width equal to 10px
+ val newX = STARTING_BOUNDS.right.toFloat() - 80
+ val newY = STARTING_BOUNDS.top.toFloat() + 80
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 80)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ fun testChangeBoundsDoesNotRunWithNegativeHeightAndWidth() {
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+ // Shrink to height -5px and width -5px with both min height/width equal to 10px
+ val newX = STARTING_BOUNDS.right.toFloat() - 105
+ val newY = STARTING_BOUNDS.top.toFloat() + 105
+
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ fun testChangeBounds_toDisallowedBounds_freezesAtLimit() {
+ var hasMoved = false
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(),
+ STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+ // Initial resize to width and height 110px.
+ var newX = STARTING_BOUNDS.right.toFloat() + 10
+ var newY = STARTING_BOUNDS.bottom.toFloat() + 10
+ var delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+ assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration))
+ hasMoved = true
+ // Resize width to 120px, height to disallowed area which should not result in a change.
+ newX += 10
+ newY = DISALLOWED_RESIZE_AREA.top.toFloat()
+ delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+ assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration))
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right + 20)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom + 10)
+ }
+
+ companion object {
+ private const val TASK_ID = 5
+ private const val MIN_WIDTH = 10
+ private const val MIN_HEIGHT = 10
+ private const val DENSITY_DPI = 20
+ private const val DEFAULT_MIN = 40
+ private const val DISPLAY_ID = 1
+ private const val NAVBAR_HEIGHT = 50
+ private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
+ private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+ private val DISALLOWED_RESIZE_AREA = Rect(
+ DISPLAY_BOUNDS.left,
+ DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
+ DISPLAY_BOUNDS.right,
+ DISPLAY_BOUNDS.bottom)
+ private val STABLE_BOUNDS = Rect(
+ DISPLAY_BOUNDS.left,
+ DISPLAY_BOUNDS.top,
+ DISPLAY_BOUNDS.right,
+ DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
+ )
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 94c064bda763..69604ddf0af1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -6,45 +6,48 @@ import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
import android.view.Display
+import android.view.SurfaceControl
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING
import androidx.test.filters.SmallTest
-import com.android.wm.shell.common.DisplayController
-import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_BOTTOM
-import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT
-import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP
-import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.argThat
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
+import java.util.function.Supplier
+import org.mockito.Mockito.`when` as whenever
/**
- * Tests for [TaskPositioner].
+ * Tests for [FluidResizeTaskPositioner].
*
* Build/Install/Run:
- * atest WMShellUnitTests:TaskPositionerTest
+ * atest WMShellUnitTests:FluidResizeTaskPositionerTest
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
-class TaskPositionerTest : ShellTestCase() {
+class FluidResizeTaskPositionerTest : ShellTestCase() {
@Mock
private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
@Mock
private lateinit var mockWindowDecoration: WindowDecoration<*>
@Mock
- private lateinit var mockDragStartListener: TaskPositioner.DragStartListener
+ private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
@Mock
private lateinit var taskToken: WindowContainerToken
@@ -57,26 +60,35 @@ class TaskPositionerTest : ShellTestCase() {
private lateinit var mockDisplayLayout: DisplayLayout
@Mock
private lateinit var mockDisplay: Display
+ @Mock
+ private lateinit var mockTransactionFactory: Supplier<SurfaceControl.Transaction>
+ @Mock
+ private lateinit var mockTransaction: SurfaceControl.Transaction
- private lateinit var taskPositioner: TaskPositioner
+ private lateinit var taskPositioner: FluidResizeTaskPositioner
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- taskPositioner = TaskPositioner(
+ taskPositioner =
+ FluidResizeTaskPositioner(
mockShellTaskOrganizer,
mockWindowDecoration,
mockDisplayController,
- mockDragStartListener
+ DISALLOWED_AREA_FOR_END_BOUNDS,
+ mockDragStartListener,
+ mockTransactionFactory
)
- `when`(taskToken.asBinder()).thenReturn(taskBinder)
- `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
- `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
- `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+ whenever(taskToken.asBinder()).thenReturn(taskBinder)
+ whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
+ whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
+ whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
(i.arguments.first() as Rect).set(STABLE_BOUNDS)
}
+ `when`(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS)
+ `when`(mockTransactionFactory.get()).thenReturn(mockTransaction)
mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
taskId = TASK_ID
@@ -88,7 +100,7 @@ class TaskPositionerTest : ShellTestCase() {
configuration.windowConfiguration.bounds = STARTING_BOUNDS
}
mockWindowDecoration.mDisplay = mockDisplay
- `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
+ whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
}
@Test
@@ -523,11 +535,85 @@ class TaskPositionerTest : ShellTestCase() {
})
}
+ @Test
+ fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_UNDEFINED, // drag
+ STARTING_BOUNDS.right.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ val newX = STARTING_BOUNDS.right.toFloat() + 5
+ val newY = STARTING_BOUNDS.top.toFloat() + 5
+ taskPositioner.onDragPositioningMove(
+ newX,
+ newY
+ )
+
+ taskPositioner.onDragPositioningEnd(newX, newY)
+
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+ }
+ })
+ }
+
private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean {
return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) &&
bounds == configuration.windowConfiguration.bounds
}
+ @Test
+ fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() {
+ mockWindowDecoration.mTaskInfo.isFocused = false
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_RIGHT, // Resize right
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // Verify task is reordered to top
+ verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ return@argThat wct.hierarchyOps.any { hierarchyOps ->
+ hierarchyOps.container == taskBinder && hierarchyOps.toTop }
+ })
+ }
+
+ @Test
+ fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() {
+ mockWindowDecoration.mTaskInfo.isFocused = true
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_RIGHT, // Resize right
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // Verify task is not reordered to top
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.hierarchyOps.any { hierarchyOps ->
+ hierarchyOps.container == taskBinder && hierarchyOps.toTop }
+ })
+ }
+
+ @Test
+ fun testDragResize_drag_draggedTaskNotReorderedToTop() {
+ mockWindowDecoration.mTaskInfo.isFocused = false
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_UNDEFINED, // drag
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // Verify task is not reordered to top since task is already brought to top before dragging
+ // begins
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.hierarchyOps.any { hierarchyOps ->
+ hierarchyOps.container == taskBinder && hierarchyOps.toTop }
+ })
+ }
+
companion object {
private const val TASK_ID = 5
private const val MIN_WIDTH = 10
@@ -538,6 +624,8 @@ class TaskPositionerTest : ShellTestCase() {
private const val NAVBAR_HEIGHT = 50
private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+ private val STABLE_INSETS = Rect(0, 50, 0, 0)
+ private val DISALLOWED_AREA_FOR_END_BOUNDS = Rect(0, 0, 300, 300)
private val DISALLOWED_RESIZE_AREA = Rect(
DISPLAY_BOUNDS.left,
DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
new file mode 100644
index 000000000000..4147dd8e6152
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -0,0 +1,355 @@
+/*
+ * 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.
+ */
+package com.android.wm.shell.windowdecor
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.graphics.Rect
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import java.util.function.Supplier
+import org.mockito.Mockito.`when` as whenever
+
+/**
+ * Tests for [VeiledResizeTaskPositioner].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:VeiledResizeTaskPositionerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class VeiledResizeTaskPositionerTest : ShellTestCase() {
+
+ @Mock
+ private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
+ @Mock
+ private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration
+ @Mock
+ private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
+
+ @Mock
+ private lateinit var taskToken: WindowContainerToken
+ @Mock
+ private lateinit var taskBinder: IBinder
+
+ @Mock
+ private lateinit var mockDisplayController: DisplayController
+ @Mock
+ private lateinit var mockDisplayLayout: DisplayLayout
+ @Mock
+ private lateinit var mockDisplay: Display
+ @Mock
+ private lateinit var mockTransactionFactory: Supplier<SurfaceControl.Transaction>
+ @Mock
+ private lateinit var mockTransaction: SurfaceControl.Transaction
+ @Mock
+ private lateinit var mockTransitions: Transitions
+
+ private lateinit var taskPositioner: VeiledResizeTaskPositioner
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ taskPositioner =
+ VeiledResizeTaskPositioner(
+ mockShellTaskOrganizer,
+ mockDesktopWindowDecoration,
+ mockDisplayController,
+ DISALLOWED_AREA_FOR_END_BOUNDS,
+ mockDragStartListener,
+ mockTransactionFactory,
+ mockTransitions
+ )
+
+ whenever(taskToken.asBinder()).thenReturn(taskBinder)
+ whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
+ whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
+ whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+ }
+ `when`(mockTransactionFactory.get()).thenReturn(mockTransaction)
+
+ mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+ taskId = TASK_ID
+ token = taskToken
+ minWidth = MIN_WIDTH
+ minHeight = MIN_HEIGHT
+ defaultMinSize = DEFAULT_MIN
+ displayId = DISPLAY_ID
+ configuration.windowConfiguration.bounds = STARTING_BOUNDS
+ }
+ mockDesktopWindowDecoration.mDisplay = mockDisplay
+ whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
+ }
+
+ @Test
+ fun testDragResize_noMove_showsResizeVeil() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+ verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS)
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+ verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == STARTING_BOUNDS}},
+ eq(taskPositioner))
+ verify(mockDesktopWindowDecoration).hideResizeVeil()
+ }
+
+ @Test
+ fun testDragResize_movesTask_doesNotShowResizeVeil() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_UNDEFINED,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningMove(
+ STARTING_BOUNDS.left.toFloat() + 60,
+ STARTING_BOUNDS.top.toFloat() + 100
+ )
+ val rectAfterMove = Rect(STARTING_BOUNDS)
+ rectAfterMove.left += 60
+ rectAfterMove.right += 60
+ rectAfterMove.top += 100
+ rectAfterMove.bottom += 100
+ verify(mockTransaction).setPosition(any(), eq(rectAfterMove.left.toFloat()),
+ eq(rectAfterMove.top.toFloat()))
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat() + 70,
+ STARTING_BOUNDS.top.toFloat() + 20
+ )
+ val rectAfterEnd = Rect(STARTING_BOUNDS)
+ rectAfterEnd.left += 70
+ rectAfterEnd.right += 70
+ rectAfterEnd.top += 20
+ rectAfterEnd.bottom += 20
+
+ verify(mockDesktopWindowDecoration, never()).createResizeVeil()
+ verify(mockDesktopWindowDecoration, never()).hideResizeVeil()
+ verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterEnd
+ }
+ })
+ }
+
+ @Test
+ fun testDragResize_resize_boundsUpdateOnEnd() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ STARTING_BOUNDS.right.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+ verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS)
+
+ taskPositioner.onDragPositioningMove(
+ STARTING_BOUNDS.right.toFloat() + 10,
+ STARTING_BOUNDS.top.toFloat() + 10
+ )
+
+ val rectAfterMove = Rect(STARTING_BOUNDS)
+ rectAfterMove.right += 10
+ rectAfterMove.top += 10
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterMove
+ }
+ })
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.right.toFloat() + 20,
+ STARTING_BOUNDS.top.toFloat() + 20
+ )
+ val rectAfterEnd = Rect(rectAfterMove)
+ rectAfterEnd.right += 10
+ rectAfterEnd.top += 10
+ verify(mockDesktopWindowDecoration, times(2)).updateResizeVeil(any())
+ verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterEnd}},
+ eq(taskPositioner))
+ }
+
+ @Test
+ fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+ verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS)
+
+ taskPositioner.onDragPositioningMove(
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat() + 10,
+ STARTING_BOUNDS.top.toFloat() + 10
+ )
+
+ verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == STARTING_BOUNDS}},
+ eq(taskPositioner))
+
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+ }
+ })
+ }
+
+
+ @Test
+ fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_UNDEFINED, // drag
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ val newX = STARTING_BOUNDS.left.toFloat() + 5
+ val newY = STARTING_BOUNDS.top.toFloat() + 5
+ taskPositioner.onDragPositioningMove(
+ newX,
+ newY
+ )
+
+ taskPositioner.onDragPositioningEnd(newX, newY)
+
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+ }
+ })
+ }
+
+ @Test
+ fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() {
+ mockDesktopWindowDecoration.mTaskInfo.isFocused = false
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_RIGHT, // Resize right
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // Verify task is reordered to top
+ verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ return@argThat wct.hierarchyOps.any { hierarchyOps ->
+ hierarchyOps.container == taskBinder && hierarchyOps.toTop }
+ })
+ }
+
+ @Test
+ fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() {
+ mockDesktopWindowDecoration.mTaskInfo.isFocused = true
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_RIGHT, // Resize right
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // Verify task is not reordered to top
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.hierarchyOps.any { hierarchyOps ->
+ hierarchyOps.container == taskBinder && hierarchyOps.toTop }
+ })
+ }
+
+ @Test
+ fun testDragResize_drag_draggedTaskNotReorderedToTop() {
+ mockDesktopWindowDecoration.mTaskInfo.isFocused = false
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_UNDEFINED, // drag
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ // Verify task is not reordered to top since task is already brought to top before dragging
+ // begins
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.hierarchyOps.any { hierarchyOps ->
+ hierarchyOps.container == taskBinder && hierarchyOps.toTop }
+ })
+ }
+
+ companion object {
+ private const val TASK_ID = 5
+ private const val MIN_WIDTH = 10
+ private const val MIN_HEIGHT = 10
+ private const val DENSITY_DPI = 20
+ private const val DEFAULT_MIN = 40
+ private const val DISPLAY_ID = 1
+ private const val NAVBAR_HEIGHT = 50
+ private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
+ private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+ private val DISALLOWED_AREA_FOR_END_BOUNDS = Rect(0, 0, 50, 50)
+ private val STABLE_BOUNDS = Rect(
+ DISPLAY_BOUNDS.left,
+ DISPLAY_BOUNDS.top,
+ DISPLAY_BOUNDS.right,
+ DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 7e39b5b8f2ce..5a2326b9c393 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.windowdecor;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+
import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlBuilder;
import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction;
@@ -23,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.doReturn;
@@ -32,6 +35,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.content.Context;
@@ -41,14 +45,15 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
+import android.view.AttachedSurfaceControl;
import android.view.Display;
-import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.ViewRootImpl;
+import android.view.WindowInsets;
import android.view.WindowManager.LayoutParams;
-import android.window.TaskConstants;
+import android.window.SurfaceSyncGroup;
import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
@@ -83,6 +88,7 @@ import java.util.function.Supplier;
public class WindowDecorationTests extends ShellTestCase {
private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400);
private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60);
+ private static final int CORNER_RADIUS = 20;
private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult =
new WindowDecoration.RelayoutResult<>();
@@ -96,9 +102,13 @@ public class WindowDecorationTests extends ShellTestCase {
@Mock
private SurfaceControlViewHost mMockSurfaceControlViewHost;
@Mock
+ private AttachedSurfaceControl mMockRootSurfaceControl;
+ @Mock
private TestView mMockView;
@Mock
private WindowContainerTransaction mMockWindowContainerTransaction;
+ @Mock
+ private SurfaceSyncGroup mMockSurfaceSyncGroup;
private final List<SurfaceControl.Transaction> mMockSurfaceControlTransactions =
new ArrayList<>();
@@ -123,9 +133,12 @@ public class WindowDecorationTests extends ShellTestCase {
mCaptionMenuShadowRadiusId = R.dimen.test_caption_menu_shadow_radius;
mCaptionMenuCornerRadiusId = R.dimen.test_caption_menu_corner_radius;
mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
+ mRelayoutParams.mCornerRadius = CORNER_RADIUS;
doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
.create(any(), any(), any());
+ when(mMockSurfaceControlViewHost.getRootSurfaceControl())
+ .thenReturn(mMockRootSurfaceControl);
}
@Test
@@ -158,14 +171,8 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(false)
.build();
taskInfo.isFocused = false;
- // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
- // 64px.
+ // Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mRelayoutParams.setOutsets(
- R.dimen.test_window_decor_left_outset,
- R.dimen.test_window_decor_top_outset,
- R.dimen.test_window_decor_right_outset,
- R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -192,10 +199,6 @@ public class WindowDecorationTests extends ShellTestCase {
final SurfaceControl.Builder decorContainerSurfaceBuilder =
createMockSurfaceControlBuilder(decorContainerSurface);
mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
- final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
- final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
- createMockSurfaceControlBuilder(taskBackgroundSurface);
- mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
final SurfaceControl.Builder captionContainerSurfaceBuilder =
createMockSurfaceControlBuilder(captionContainerSurface);
@@ -210,16 +213,11 @@ public class WindowDecorationTests extends ShellTestCase {
.setBounds(TASK_BOUNDS)
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
taskInfo.isFocused = true;
- // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
- // 64px.
+ // Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mRelayoutParams.setOutsets(
- R.dimen.test_window_decor_left_outset,
- R.dimen.test_window_decor_top_outset,
- R.dimen.test_window_decor_right_outset,
- R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -228,22 +226,10 @@ public class WindowDecorationTests extends ShellTestCase {
verify(decorContainerSurfaceBuilder).setParent(taskSurface);
verify(decorContainerSurfaceBuilder).setContainerLayer();
verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true);
- verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40);
- verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220);
-
- verify(taskBackgroundSurfaceBuilder).setParent(taskSurface);
- verify(taskBackgroundSurfaceBuilder).setEffectLayer();
- verify(mMockSurfaceControlStartT).setWindowCrop(taskBackgroundSurface, 300, 100);
- verify(mMockSurfaceControlStartT)
- .setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f});
- verify(mMockSurfaceControlStartT).setShadowRadius(taskBackgroundSurface, 10);
- verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface,
- TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND);
- verify(mMockSurfaceControlStartT).show(taskBackgroundSurface);
+ verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 300, 100);
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
verify(captionContainerSurfaceBuilder).setContainerLayer();
- verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40);
verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
verify(mMockSurfaceControlStartT).show(captionContainerSurface);
@@ -256,21 +242,28 @@ public class WindowDecorationTests extends ShellTestCase {
&& (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
if (ViewRootImpl.CAPTION_ON_SHELL) {
verify(mMockView).setTaskFocusState(true);
- verify(mMockWindowContainerTransaction)
- .addRectInsetsProvider(taskInfo.token,
- new Rect(100, 300, 400, 364),
- new int[] { InsetsState.ITYPE_CAPTION_BAR });
+ verify(mMockWindowContainerTransaction).addInsetsSource(
+ eq(taskInfo.token),
+ any(),
+ eq(0 /* index */),
+ eq(WindowInsets.Type.captionBar()),
+ eq(new Rect(100, 300, 400, 364)));
}
verify(mMockSurfaceControlFinishT)
.setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y);
verify(mMockSurfaceControlFinishT)
- .setCrop(taskSurface, new Rect(-20, -40, 360, 180));
+ .setWindowCrop(taskSurface, 300, 100);
+ verify(mMockSurfaceControlStartT).setCornerRadius(taskSurface, CORNER_RADIUS);
+ verify(mMockSurfaceControlFinishT).setCornerRadius(taskSurface, CORNER_RADIUS);
verify(mMockSurfaceControlStartT)
.show(taskSurface);
+ verify(mMockSurfaceControlStartT)
+ .setColor(taskSurface, new float[] {1.f, 1.f, 0.f});
+ verify(mMockSurfaceControlStartT).setShadowRadius(taskSurface, 10);
- assertEquals(380, mRelayoutResult.mWidth);
- assertEquals(220, mRelayoutResult.mHeight);
+ assertEquals(300, mRelayoutResult.mWidth);
+ assertEquals(100, mRelayoutResult.mHeight);
}
@Test
@@ -283,10 +276,6 @@ public class WindowDecorationTests extends ShellTestCase {
final SurfaceControl.Builder decorContainerSurfaceBuilder =
createMockSurfaceControlBuilder(decorContainerSurface);
mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
- final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
- final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
- createMockSurfaceControlBuilder(taskBackgroundSurface);
- mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
final SurfaceControl.Builder captionContainerSurfaceBuilder =
createMockSurfaceControlBuilder(captionContainerSurface);
@@ -306,14 +295,8 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(true)
.build();
taskInfo.isFocused = true;
- // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
- // 64px.
+ // Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mRelayoutParams.setOutsets(
- R.dimen.test_window_decor_left_outset,
- R.dimen.test_window_decor_top_outset,
- R.dimen.test_window_decor_right_outset,
- R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -323,7 +306,7 @@ public class WindowDecorationTests extends ShellTestCase {
verify(mMockSurfaceControlViewHost, never()).release();
verify(t, never()).apply();
verify(mMockWindowContainerTransaction, never())
- .removeInsetsProvider(eq(taskInfo.token), any());
+ .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt());
taskInfo.isVisible = false;
windowDecor.relayout(taskInfo);
@@ -332,9 +315,9 @@ public class WindowDecorationTests extends ShellTestCase {
releaseOrder.verify(mMockSurfaceControlViewHost).release();
releaseOrder.verify(t).remove(captionContainerSurface);
releaseOrder.verify(t).remove(decorContainerSurface);
- releaseOrder.verify(t).remove(taskBackgroundSurface);
releaseOrder.verify(t).apply();
- verify(mMockWindowContainerTransaction).removeInsetsProvider(eq(taskInfo.token), any());
+ verify(mMockWindowContainerTransaction)
+ .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt());
}
@Test
@@ -392,10 +375,6 @@ public class WindowDecorationTests extends ShellTestCase {
final SurfaceControl.Builder decorContainerSurfaceBuilder =
createMockSurfaceControlBuilder(decorContainerSurface);
mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
- final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
- final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
- createMockSurfaceControlBuilder(taskBackgroundSurface);
- mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
final SurfaceControl.Builder captionContainerSurfaceBuilder =
createMockSurfaceControlBuilder(captionContainerSurface);
@@ -415,11 +394,6 @@ public class WindowDecorationTests extends ShellTestCase {
.build();
taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mRelayoutParams.setOutsets(
- R.dimen.test_window_decor_left_outset,
- R.dimen.test_window_decor_top_outset,
- R.dimen.test_window_decor_right_outset,
- R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
windowDecor.relayout(taskInfo);
@@ -434,7 +408,7 @@ public class WindowDecorationTests extends ShellTestCase {
verify(additionalWindowSurfaceBuilder).setContainerLayer();
verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface);
verify(additionalWindowSurfaceBuilder).build();
- verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40);
+ verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 0, 0);
final int width = WindowDecoration.loadDimensionPixelSize(
mContext.getResources(), mCaptionMenuWidthId);
final int height = WindowDecoration.loadDimensionPixelSize(
@@ -469,10 +443,6 @@ public class WindowDecorationTests extends ShellTestCase {
final SurfaceControl.Builder decorContainerSurfaceBuilder =
createMockSurfaceControlBuilder(decorContainerSurface);
mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
- final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
- final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
- createMockSurfaceControlBuilder(taskBackgroundSurface);
- mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
final SurfaceControl.Builder captionContainerSurfaceBuilder =
createMockSurfaceControlBuilder(captionContainerSurface);
@@ -492,11 +462,6 @@ public class WindowDecorationTests extends ShellTestCase {
.build();
taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mRelayoutParams.setOutsets(
- R.dimen.test_window_decor_left_outset,
- R.dimen.test_window_decor_top_outset,
- R.dimen.test_window_decor_right_outset,
- R.dimen.test_window_decor_bottom_outset);
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -504,12 +469,48 @@ public class WindowDecorationTests extends ShellTestCase {
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
verify(captionContainerSurfaceBuilder).setContainerLayer();
- verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40);
// Width of the captionContainerSurface should match the width of TASK_BOUNDS
verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
verify(mMockSurfaceControlStartT).show(captionContainerSurface);
}
+ @Test
+ public void testRelayout_applyTransactionInSyncWithDraw() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder decorContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(decorContainerSurface);
+ mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+ final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder captionContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(captionContainerSurface);
+ mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder);
+
+ final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+ mMockSurfaceControlTransactions.add(t);
+ final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+ new ActivityManager.TaskDescription.Builder()
+ .setBackgroundColor(Color.YELLOW);
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(true)
+ .build();
+ taskInfo.isFocused = true;
+ taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+ final SurfaceControl taskSurface = mock(SurfaceControl.class);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+
+ windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */);
+
+ verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT);
+ }
+
private TestWindowDecoration createWindowDecoration(
ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(),
@@ -565,6 +566,12 @@ public class WindowDecorationTests extends ShellTestCase {
@Override
void relayout(ActivityManager.RunningTaskInfo taskInfo) {
+ relayout(taskInfo, false /* applyStartTransactionOnDraw */);
+ }
+
+ void relayout(ActivityManager.RunningTaskInfo taskInfo,
+ boolean applyStartTransactionOnDraw) {
+ mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
mMockWindowContainerTransaction, mMockView, mRelayoutResult);
}
@@ -579,10 +586,8 @@ public class WindowDecorationTests extends ShellTestCase {
int cornerRadius = loadDimensionPixelSize(resources, mCaptionMenuCornerRadiusId);
String name = "Test Window";
WindowDecoration.AdditionalWindow additionalWindow =
- addWindow(R.layout.desktop_mode_decor_handle_menu, name,
- mMockSurfaceControlAddWindowT,
- x - mRelayoutResult.mDecorContainerOffsetX,
- y - mRelayoutResult.mDecorContainerOffsetY,
+ addWindow(R.layout.desktop_mode_window_decor_handle_menu_app_info_pill, name,
+ mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, x, y,
width, height, shadowRadius, cornerRadius);
return additionalWindow;
}
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index c80fb188e70f..28bda72bccdd 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -33,6 +33,7 @@ license {
cc_defaults {
name: "libandroidfw_defaults",
+ cpp_std: "gnu++2b",
cflags: [
"-Werror",
"-Wunreachable-code",
@@ -54,12 +55,14 @@ cc_library {
host_supported: true,
srcs: [
"ApkAssets.cpp",
+ "ApkParsing.cpp",
"Asset.cpp",
"AssetDir.cpp",
"AssetManager.cpp",
"AssetManager2.cpp",
"AssetsProvider.cpp",
"AttributeResolution.cpp",
+ "BigBuffer.cpp",
"ChunkIterator.cpp",
"ConfigDescription.cpp",
"Idmap.cpp",
@@ -69,9 +72,11 @@ cc_library {
"misc.cpp",
"ObbFile.cpp",
"PosixUtils.cpp",
+ "ResourceTimer.cpp",
"ResourceTypes.cpp",
"ResourceUtils.cpp",
"StreamingZipInflater.cpp",
+ "StringPool.cpp",
"TypeWrappers.cpp",
"Util.cpp",
"ZipFileRO.cpp",
@@ -156,11 +161,13 @@ cc_test {
// Actual tests.
"tests/ApkAssets_test.cpp",
+ "tests/ApkParsing_test.cpp",
"tests/AppAsLib_test.cpp",
"tests/Asset_test.cpp",
"tests/AssetManager2_test.cpp",
"tests/AttributeFinder_test.cpp",
"tests/AttributeResolution_test.cpp",
+ "tests/BigBuffer_test.cpp",
"tests/ByteBucketArray_test.cpp",
"tests/Config_test.cpp",
"tests/ConfigDescription_test.cpp",
@@ -169,10 +176,12 @@ cc_test {
"tests/Idmap_test.cpp",
"tests/LoadedArsc_test.cpp",
"tests/Locale_test.cpp",
+ "tests/ResourceTimer_test.cpp",
"tests/ResourceUtils_test.cpp",
"tests/ResTable_test.cpp",
"tests/Split_test.cpp",
"tests/StringPiece_test.cpp",
+ "tests/StringPool_test.cpp",
"tests/Theme_test.cpp",
"tests/TypeWrappers_test.cpp",
"tests/ZipUtils_test.cpp",
@@ -204,6 +213,8 @@ cc_test {
"tests/data/**/*.apk",
"tests/data/**/*.arsc",
"tests/data/**/*.idmap",
+ ":FrameworkResourcesSparseTestApp",
+ ":FrameworkResourcesNotSparseTestApp",
],
test_suites: ["device-tests"],
}
diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp
index 2beb33abe782..15aaae25f754 100755..100644
--- a/libs/androidfw/ApkAssets.cpp
+++ b/libs/androidfw/ApkAssets.cpp
@@ -18,6 +18,7 @@
#include "android-base/errors.h"
#include "android-base/logging.h"
+#include "android-base/utf8.h"
namespace android {
@@ -83,15 +84,16 @@ std::unique_ptr<ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path,
return {};
}
+ std::string overlay_path(loaded_idmap->OverlayApkPath());
+ auto fd = unique_fd(base::utf8::open(overlay_path.c_str(), O_RDONLY | O_CLOEXEC));
std::unique_ptr<AssetsProvider> overlay_assets;
- const std::string overlay_path(loaded_idmap->OverlayApkPath());
- if (IsFabricatedOverlay(overlay_path)) {
+ if (IsFabricatedOverlay(fd)) {
// Fabricated overlays do not contain resource definitions. All of the overlay resource values
// are defined inline in the idmap.
- overlay_assets = EmptyAssetsProvider::Create(overlay_path);
+ overlay_assets = EmptyAssetsProvider::Create(std::move(overlay_path));
} else {
// The overlay should be an APK.
- overlay_assets = ZipAssetsProvider::Create(overlay_path, flags);
+ overlay_assets = ZipAssetsProvider::Create(std::move(overlay_path), flags, std::move(fd));
}
if (overlay_assets == nullptr) {
return {};
@@ -141,6 +143,9 @@ std::unique_ptr<ApkAssets> ApkAssets::LoadImpl(std::unique_ptr<Asset> resources_
return {};
}
loaded_arsc = LoadedArsc::Load(data, length, loaded_idmap.get(), property_flags);
+ } else if (loaded_idmap != nullptr &&
+ IsFabricatedOverlay(std::string(loaded_idmap->OverlayApkPath()))) {
+ loaded_arsc = LoadedArsc::Load(loaded_idmap.get());
} else {
loaded_arsc = LoadedArsc::CreateEmpty();
}
diff --git a/libs/androidfw/ApkParsing.cpp b/libs/androidfw/ApkParsing.cpp
new file mode 100644
index 000000000000..32d2c5b05acb
--- /dev/null
+++ b/libs/androidfw/ApkParsing.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+#include "androidfw/ApkParsing.h"
+#include <algorithm>
+#include <array>
+#include <stdlib.h>
+#include <string_view>
+#include <sys/types.h>
+
+const std::string_view APK_LIB = "lib/";
+const size_t APK_LIB_LEN = APK_LIB.size();
+
+const std::string_view LIB_PREFIX = "/lib";
+const size_t LIB_PREFIX_LEN = LIB_PREFIX.size();
+
+const std::string_view LIB_SUFFIX = ".so";
+const size_t LIB_SUFFIX_LEN = LIB_SUFFIX.size();
+
+static const std::array<std::string_view, 2> abis = {"arm64-v8a", "x86_64"};
+
+namespace android::util {
+const char* ValidLibraryPathLastSlash(const char* fileName, bool suppress64Bit, bool debuggable) {
+ // Make sure the filename is at least to the minimum library name size.
+ const size_t fileNameLen = strlen(fileName);
+ static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN;
+ if (fileNameLen < minLength) {
+ return nullptr;
+ }
+
+ const char* lastSlash = strrchr(fileName, '/');
+ if (!lastSlash) {
+ return nullptr;
+ }
+
+ // Skip directories.
+ if (*(lastSlash + 1) == 0) {
+ return nullptr;
+ }
+
+ // Make sure the filename is safe.
+ if (!isFilenameSafe(lastSlash + 1)) {
+ return nullptr;
+ }
+
+ // Make sure there aren't subdirectories by checking if the next / after lib/ is the last slash
+ if (memchr(fileName + APK_LIB_LEN, '/', fileNameLen - APK_LIB_LEN) != lastSlash) {
+ return nullptr;
+ }
+
+ if (!debuggable) {
+ // Make sure the filename starts with lib and ends with ".so".
+ if (strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX.data(), LIB_SUFFIX_LEN) != 0
+ || strncmp(lastSlash, LIB_PREFIX.data(), LIB_PREFIX_LEN) != 0) {
+ return nullptr;
+ }
+ }
+
+ // Don't include 64 bit versions if they are suppressed
+ if (suppress64Bit && std::find(abis.begin(), abis.end(), std::string_view(
+ fileName + APK_LIB_LEN, lastSlash - fileName - APK_LIB_LEN)) != abis.end()) {
+ return nullptr;
+ }
+
+ return lastSlash;
+}
+
+bool isFilenameSafe(const char* filename) {
+ off_t offset = 0;
+ for (;;) {
+ switch (*(filename + offset)) {
+ case 0:
+ // Null.
+ // If we've reached the end, all the other characters are good.
+ return true;
+
+ case 'A' ... 'Z':
+ case 'a' ... 'z':
+ case '0' ... '9':
+ case '+':
+ case ',':
+ case '-':
+ case '.':
+ case '/':
+ case '=':
+ case '_':
+ offset++;
+ break;
+
+ default:
+ // We found something that is not good.
+ return false;
+ }
+ }
+ // Should not reach here.
+}
+} \ No newline at end of file
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 400829e15364..68f5e4a88c7e 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -22,6 +22,7 @@
#include <iterator>
#include <map>
#include <set>
+#include <span>
#include "android-base/logging.h"
#include "android-base/stringprintf.h"
@@ -43,28 +44,19 @@ namespace {
using EntryValue = std::variant<Res_value, incfs::verified_map_ptr<ResTable_map_entry>>;
+/* NOTE: table_entry has been verified in LoadedPackage::GetEntryFromOffset(),
+ * and so access to ->value() and ->map_entry() are safe here
+ */
base::expected<EntryValue, IOError> GetEntryValue(
incfs::verified_map_ptr<ResTable_entry> table_entry) {
- const uint16_t entry_size = dtohs(table_entry->size);
+ const uint16_t entry_size = table_entry->size();
// Check if the entry represents a bag value.
- if (entry_size >= sizeof(ResTable_map_entry) &&
- (dtohs(table_entry->flags) & ResTable_entry::FLAG_COMPLEX)) {
- const auto map_entry = table_entry.convert<ResTable_map_entry>();
- if (!map_entry) {
- return base::unexpected(IOError::PAGES_MISSING);
- }
- return map_entry.verified();
+ if (entry_size >= sizeof(ResTable_map_entry) && table_entry->is_complex()) {
+ return table_entry.convert<ResTable_map_entry>().verified();
}
- // The entry represents a non-bag value.
- const auto entry_value = table_entry.offset(entry_size).convert<Res_value>();
- if (!entry_value) {
- return base::unexpected(IOError::PAGES_MISSING);
- }
- Res_value value;
- value.copyFrom_dtoh(entry_value.value());
- return value;
+ return table_entry->value();
}
} // namespace
@@ -120,7 +112,7 @@ void AssetManager2::BuildDynamicRefTable() {
// A mapping from path of apk assets that could be target packages of overlays to the runtime
// package id of its first loaded package. Overlays currently can only override resources in the
// first package in the target resource table.
- std::unordered_map<std::string, uint8_t> target_assets_package_ids;
+ std::unordered_map<std::string_view, uint8_t> target_assets_package_ids;
// Overlay resources are not directly referenced by an application so their resource ids
// can change throughout the application's lifetime. Assign overlay package ids last.
@@ -143,7 +135,7 @@ void AssetManager2::BuildDynamicRefTable() {
if (auto loaded_idmap = apk_assets->GetLoadedIdmap(); loaded_idmap != nullptr) {
// The target package must precede the overlay package in the apk assets paths in order
// to take effect.
- auto iter = target_assets_package_ids.find(std::string(loaded_idmap->TargetApkPath()));
+ auto iter = target_assets_package_ids.find(loaded_idmap->TargetApkPath());
if (iter == target_assets_package_ids.end()) {
LOG(INFO) << "failed to find target package for overlay "
<< loaded_idmap->OverlayApkPath();
@@ -188,7 +180,7 @@ void AssetManager2::BuildDynamicRefTable() {
if (overlay_ref_table != nullptr) {
// If this package is from an overlay, use a dynamic reference table that can rewrite
// overlay resource ids to their corresponding target resource ids.
- new_group.dynamic_ref_table = overlay_ref_table;
+ new_group.dynamic_ref_table = std::move(overlay_ref_table);
}
DynamicRefTable* ref_table = new_group.dynamic_ref_table.get();
@@ -196,9 +188,9 @@ void AssetManager2::BuildDynamicRefTable() {
ref_table->mAppAsLib = package->IsDynamic() && package->GetPackageId() == 0x7f;
}
- // Add the package and to the set of packages with the same ID.
+ // Add the package to the set of packages with the same ID.
PackageGroup* package_group = &package_groups_[idx];
- package_group->packages_.push_back(ConfiguredPackage{package.get(), {}});
+ package_group->packages_.emplace_back().loaded_package_ = package.get();
package_group->cookies_.push_back(apk_assets_cookies[apk_assets]);
// Add the package name -> build time ID mappings.
@@ -210,30 +202,39 @@ void AssetManager2::BuildDynamicRefTable() {
if (auto apk_assets_path = apk_assets->GetPath()) {
// Overlay target ApkAssets must have been created using path based load apis.
- target_assets_package_ids.insert(std::make_pair(std::string(*apk_assets_path), package_id));
+ target_assets_package_ids.emplace(*apk_assets_path, package_id);
}
}
}
// Now assign the runtime IDs so that we have a build-time to runtime ID map.
- const auto package_groups_end = package_groups_.end();
- for (auto iter = package_groups_.begin(); iter != package_groups_end; ++iter) {
- const std::string& package_name = iter->packages_[0].loaded_package_->GetPackageName();
- for (auto iter2 = package_groups_.begin(); iter2 != package_groups_end; ++iter2) {
- iter2->dynamic_ref_table->addMapping(String16(package_name.c_str(), package_name.size()),
- iter->dynamic_ref_table->mAssignedPackageId);
-
- // Add the alias resources to the dynamic reference table of every package group. Since
- // staging aliases can only be defined by the framework package (which is not a shared
- // library), the compile-time package id of the framework is the same across all packages
- // that compile against the framework.
- for (const auto& package : iter->packages_) {
- for (const auto& entry : package.loaded_package_->GetAliasResourceIdMap()) {
- iter2->dynamic_ref_table->addAlias(entry.first, entry.second);
- }
- }
+ DynamicRefTable::AliasMap aliases;
+ for (const auto& group : package_groups_) {
+ const std::string& package_name = group.packages_[0].loaded_package_->GetPackageName();
+ const auto name_16 = String16(package_name.c_str(), package_name.size());
+ for (auto&& inner_group : package_groups_) {
+ inner_group.dynamic_ref_table->addMapping(name_16,
+ group.dynamic_ref_table->mAssignedPackageId);
+ }
+
+ for (const auto& package : group.packages_) {
+ const auto& package_aliases = package.loaded_package_->GetAliasResourceIdMap();
+ aliases.insert(aliases.end(), package_aliases.begin(), package_aliases.end());
}
}
+
+ if (!aliases.empty()) {
+ std::sort(aliases.begin(), aliases.end(), [](auto&& l, auto&& r) { return l.first < r.first; });
+
+ // Add the alias resources to the dynamic reference table of every package group. Since
+ // staging aliases can only be defined by the framework package (which is not a shared
+ // library), the compile-time package id of the framework is the same across all packages
+ // that compile against the framework.
+ for (auto& group : std::span(package_groups_.data(), package_groups_.size() - 1)) {
+ group.dynamic_ref_table->setAliases(aliases);
+ }
+ package_groups_.back().dynamic_ref_table->setAliases(std::move(aliases));
+ }
}
void AssetManager2::DumpToLog() const {
@@ -326,7 +327,7 @@ const std::unordered_map<std::string, std::string>*
return &loaded_package->GetOverlayableMap();
}
-bool AssetManager2::GetOverlayablesToString(const android::StringPiece& package_name,
+bool AssetManager2::GetOverlayablesToString(android::StringPiece package_name,
std::string* out) const {
uint8_t package_id = 0U;
for (const auto& apk_assets : apk_assets_) {
@@ -373,7 +374,7 @@ bool AssetManager2::GetOverlayablesToString(const android::StringPiece& package_
const std::string name = ToFormattedResourceString(*res_name);
output.append(base::StringPrintf(
"resource='%s' overlayable='%s' actor='%s' policy='0x%08x'\n",
- name.c_str(), info->name.c_str(), info->actor.c_str(), info->policy_flags));
+ name.c_str(), info->name.data(), info->actor.data(), info->policy_flags));
}
}
}
@@ -501,7 +502,7 @@ std::unique_ptr<AssetDir> AssetManager2::OpenDir(const std::string& dirname) con
continue;
}
- auto func = [&](const StringPiece& name, FileType type) {
+ auto func = [&](StringPiece name, FileType type) {
AssetDir::FileInfo info;
info.setFileName(String8(name.data(), name.size()));
info.setFileType(type);
@@ -611,7 +612,21 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
}
if (overlay_entry.IsInlineValue()) {
// The target resource is overlaid by an inline value not represented by a resource.
- result->entry = overlay_entry.GetInlineValue();
+ ConfigDescription best_frro_config;
+ Res_value best_frro_value;
+ bool frro_found = false;
+ for( const auto& [config, value] : overlay_entry.GetInlineValue()) {
+ if ((!frro_found || config.isBetterThan(best_frro_config, desired_config))
+ && config.match(*desired_config)) {
+ frro_found = true;
+ best_frro_config = config;
+ best_frro_value = value;
+ }
+ }
+ if (!frro_found) {
+ continue;
+ }
+ result->entry = best_frro_value;
result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
result->cookie = id_map.cookie;
@@ -800,17 +815,12 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal(
return base::unexpected(std::nullopt);
}
- auto best_entry_result = LoadedPackage::GetEntryFromOffset(best_type, best_offset);
- if (!best_entry_result.has_value()) {
- return base::unexpected(best_entry_result.error());
- }
-
- const incfs::map_ptr<ResTable_entry> best_entry = *best_entry_result;
- if (!best_entry) {
- return base::unexpected(IOError::PAGES_MISSING);
+ auto best_entry_verified = LoadedPackage::GetEntryFromOffset(best_type, best_offset);
+ if (!best_entry_verified.has_value()) {
+ return base::unexpected(best_entry_verified.error());
}
- const auto entry = GetEntryValue(best_entry.verified());
+ const auto entry = GetEntryValue(*best_entry_verified);
if (!entry.has_value()) {
return base::unexpected(entry.error());
}
@@ -823,7 +833,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal(
.package_name = &best_package->GetPackageName(),
.type_string_ref = StringPoolRef(best_package->GetTypeStringPool(), best_type->id - 1),
.entry_string_ref = StringPoolRef(best_package->GetKeyStringPool(),
- best_entry->key.index),
+ (*best_entry_verified)->key()),
.dynamic_ref_table = package_group.dynamic_ref_table.get(),
};
}
@@ -1054,7 +1064,7 @@ base::expected<const ResolvedBag*, NullOrIOError> AssetManager2::ResolveBag(
base::expected<const ResolvedBag*, NullOrIOError> AssetManager2::GetBag(uint32_t resid) const {
std::vector<uint32_t> found_resids;
const auto bag = GetBag(resid, found_resids);
- cached_bag_resid_stacks_.emplace(resid, found_resids);
+ cached_bag_resid_stacks_.emplace(resid, std::move(found_resids));
return bag;
}
@@ -1271,7 +1281,7 @@ base::expected<const ResolvedBag*, NullOrIOError> AssetManager2::GetBag(
return result;
}
-static bool Utf8ToUtf16(const StringPiece& str, std::u16string* out) {
+static bool Utf8ToUtf16(StringPiece str, std::u16string* out) {
ssize_t len =
utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(str.data()), str.size(), false);
if (len < 0) {
@@ -1346,22 +1356,22 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId(
void AssetManager2::RebuildFilterList() {
for (PackageGroup& group : package_groups_) {
- for (ConfiguredPackage& impl : group.packages_) {
- // Destroy it.
- impl.filtered_configs_.~ByteBucketArray();
-
- // Re-create it.
- new (&impl.filtered_configs_) ByteBucketArray<FilteredConfigGroup>();
-
+ for (ConfiguredPackage& package : group.packages_) {
+ package.filtered_configs_.forEachItem([](auto, auto& fcg) { fcg.type_entries.clear(); });
// Create the filters here.
- impl.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) {
- FilteredConfigGroup& group = impl.filtered_configs_.editItemAt(type_id - 1);
+ package.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) {
+ FilteredConfigGroup* group = nullptr;
for (const auto& type_entry : type_spec.type_entries) {
if (type_entry.config.match(configuration_)) {
- group.type_entries.push_back(&type_entry);
+ if (!group) {
+ group = &package.filtered_configs_.editItemAt(type_id - 1);
+ }
+ group->type_entries.push_back(&type_entry);
}
}
});
+ package.filtered_configs_.trimBuckets(
+ [](const auto& fcg) { return fcg.type_entries.empty(); });
}
}
}
@@ -1402,30 +1412,34 @@ uint8_t AssetManager2::GetAssignedPackageId(const LoadedPackage* package) const
std::unique_ptr<Theme> AssetManager2::NewTheme() {
constexpr size_t kInitialReserveSize = 32;
auto theme = std::unique_ptr<Theme>(new Theme(this));
+ theme->keys_.reserve(kInitialReserveSize);
theme->entries_.reserve(kInitialReserveSize);
return theme;
}
+void AssetManager2::ForEachPackage(base::function_ref<bool(const std::string&, uint8_t)> func,
+ package_property_t excluded_property_flags) const {
+ for (const PackageGroup& package_group : package_groups_) {
+ const auto loaded_package = package_group.packages_.front().loaded_package_;
+ if ((loaded_package->GetPropertyFlags() & excluded_property_flags) == 0U
+ && !func(loaded_package->GetPackageName(),
+ package_group.dynamic_ref_table->mAssignedPackageId)) {
+ return;
+ }
+ }
+}
+
Theme::Theme(AssetManager2* asset_manager) : asset_manager_(asset_manager) {
}
Theme::~Theme() = default;
struct Theme::Entry {
- uint32_t attr_res_id;
ApkAssetsCookie cookie;
uint32_t type_spec_flags;
Res_value value;
};
-namespace {
-struct ThemeEntryKeyComparer {
- bool operator() (const Theme::Entry& entry, uint32_t attr_res_id) const noexcept {
- return entry.attr_res_id < attr_res_id;
- }
-};
-} // namespace
-
base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid, bool force) {
ATRACE_NAME("Theme::ApplyStyle");
@@ -1454,19 +1468,20 @@ base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid,
continue;
}
- Theme::Entry new_entry{attr_res_id, it->cookie, (*bag)->type_spec_flags, it->value};
- auto entry_it = std::lower_bound(entries_.begin(), entries_.end(), attr_res_id,
- ThemeEntryKeyComparer{});
- if (entry_it != entries_.end() && entry_it->attr_res_id == attr_res_id) {
+ const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), attr_res_id);
+ const auto entry_it = entries_.begin() + (key_it - keys_.begin());
+ if (key_it != keys_.end() && *key_it == attr_res_id) {
if (is_undefined) {
// DATA_NULL_UNDEFINED clears the value of the attribute in the theme only when `force` is
- /// true.
+ // true.
+ keys_.erase(key_it);
entries_.erase(entry_it);
} else if (force) {
- *entry_it = new_entry;
+ *entry_it = Entry{it->cookie, (*bag)->type_spec_flags, it->value};
}
} else {
- entries_.insert(entry_it, new_entry);
+ keys_.insert(key_it, attr_res_id);
+ entries_.insert(entry_it, Entry{it->cookie, (*bag)->type_spec_flags, it->value});
}
}
return {};
@@ -1477,6 +1492,7 @@ void Theme::Rebase(AssetManager2* am, const uint32_t* style_ids, const uint8_t*
ATRACE_NAME("Theme::Rebase");
// Reset the entries without changing the vector capacity to prevent reallocations during
// ApplyStyle.
+ keys_.clear();
entries_.clear();
asset_manager_ = am;
for (size_t i = 0; i < style_count; i++) {
@@ -1485,16 +1501,14 @@ void Theme::Rebase(AssetManager2* am, const uint32_t* style_ids, const uint8_t*
}
std::optional<AssetManager2::SelectedValue> Theme::GetAttribute(uint32_t resid) const {
-
constexpr const uint32_t kMaxIterations = 20;
uint32_t type_spec_flags = 0u;
for (uint32_t i = 0; i <= kMaxIterations; i++) {
- auto entry_it = std::lower_bound(entries_.begin(), entries_.end(), resid,
- ThemeEntryKeyComparer{});
- if (entry_it == entries_.end() || entry_it->attr_res_id != resid) {
+ const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), resid);
+ if (key_it == keys_.end() || *key_it != resid) {
return std::nullopt;
}
-
+ const auto entry_it = entries_.begin() + (key_it - keys_.begin());
type_spec_flags |= entry_it->type_spec_flags;
if (entry_it->value.dataType == Res_value::TYPE_ATTRIBUTE) {
resid = entry_it->value.data;
@@ -1528,6 +1542,7 @@ base::expected<std::monostate, NullOrIOError> Theme::ResolveAttributeReference(
}
void Theme::Clear() {
+ keys_.clear();
entries_.clear();
}
@@ -1539,18 +1554,19 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) {
type_spec_flags_ = source.type_spec_flags_;
if (asset_manager_ == source.asset_manager_) {
+ keys_ = source.keys_;
entries_ = source.entries_;
} else {
- std::map<ApkAssetsCookie, ApkAssetsCookie> src_to_dest_asset_cookies;
- typedef std::map<int, int> SourceToDestinationRuntimePackageMap;
- std::map<ApkAssetsCookie, SourceToDestinationRuntimePackageMap> src_asset_cookie_id_map;
+ std::unordered_map<ApkAssetsCookie, ApkAssetsCookie> src_to_dest_asset_cookies;
+ using SourceToDestinationRuntimePackageMap = std::unordered_map<int, int>;
+ std::unordered_map<ApkAssetsCookie, SourceToDestinationRuntimePackageMap> src_asset_cookie_id_map;
// Determine which ApkAssets are loaded in both theme AssetManagers.
- const auto src_assets = source.asset_manager_->GetApkAssets();
+ const auto& src_assets = source.asset_manager_->GetApkAssets();
for (size_t i = 0; i < src_assets.size(); i++) {
const ApkAssets* src_asset = src_assets[i];
- const auto dest_assets = asset_manager_->GetApkAssets();
+ const auto& dest_assets = asset_manager_->GetApkAssets();
for (size_t j = 0; j < dest_assets.size(); j++) {
const ApkAssets* dest_asset = dest_assets[j];
if (src_asset != dest_asset) {
@@ -1571,15 +1587,17 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) {
}
src_to_dest_asset_cookies.insert(std::make_pair(i, j));
- src_asset_cookie_id_map.insert(std::make_pair(i, package_map));
+ src_asset_cookie_id_map.insert(std::make_pair(i, std::move(package_map)));
break;
}
}
// Reset the data in the destination theme.
+ keys_.clear();
entries_.clear();
- for (const auto& entry : source.entries_) {
+ for (size_t i = 0, size = source.entries_.size(); i != size; ++i) {
+ const auto& entry = source.entries_[i];
bool is_reference = (entry.value.dataType == Res_value::TYPE_ATTRIBUTE
|| entry.value.dataType == Res_value::TYPE_REFERENCE
|| entry.value.dataType == Res_value::TYPE_DYNAMIC_ATTRIBUTE
@@ -1619,13 +1637,15 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) {
}
}
+ const auto source_res_id = source.keys_[i];
+
// The package id of the attribute needs to be rewritten to the package id of the
// attribute in the destination.
- int attribute_dest_package_id = get_package_id(entry.attr_res_id);
+ int attribute_dest_package_id = get_package_id(source_res_id);
if (attribute_dest_package_id != 0x01) {
// Find the cookie of the attribute resource id in the source AssetManager
base::expected<FindEntryResult, NullOrIOError> attribute_entry_result =
- source.asset_manager_->FindEntry(entry.attr_res_id, 0 /* density_override */ ,
+ source.asset_manager_->FindEntry(source_res_id, 0 /* density_override */ ,
true /* stop_at_first_match */,
true /* ignore_configuration */);
if (UNLIKELY(IsIOError(attribute_entry_result))) {
@@ -1649,16 +1669,15 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) {
attribute_dest_package_id = attribute_dest_package->second;
}
- auto dest_attr_id = make_resid(attribute_dest_package_id, get_type_id(entry.attr_res_id),
- get_entry_id(entry.attr_res_id));
- Theme::Entry new_entry{dest_attr_id, data_dest_cookie, entry.type_spec_flags,
- Res_value{.dataType = entry.value.dataType,
- .data = attribute_data}};
-
+ auto dest_attr_id = make_resid(attribute_dest_package_id, get_type_id(source_res_id),
+ get_entry_id(source_res_id));
+ const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), dest_attr_id);
+ const auto entry_it = entries_.begin() + (key_it - keys_.begin());
// Since the entries were cleared, the attribute resource id has yet been mapped to any value.
- auto entry_it = std::lower_bound(entries_.begin(), entries_.end(), dest_attr_id,
- ThemeEntryKeyComparer{});
- entries_.insert(entry_it, new_entry);
+ keys_.insert(key_it, dest_attr_id);
+ entries_.insert(entry_it, Entry{data_dest_cookie, entry.type_spec_flags,
+ Res_value{.dataType = entry.value.dataType,
+ .data = attribute_data}});
}
}
return {};
@@ -1666,9 +1685,11 @@ base::expected<std::monostate, IOError> Theme::SetTo(const Theme& source) {
void Theme::Dump() const {
LOG(INFO) << base::StringPrintf("Theme(this=%p, AssetManager2=%p)", this, asset_manager_);
- for (auto& entry : entries_) {
+ for (size_t i = 0, size = keys_.size(); i != size; ++i) {
+ auto res_id = keys_[i];
+ const auto& entry = entries_[i];
LOG(INFO) << base::StringPrintf(" entry(0x%08x)=(0x%08x) type=(0x%02x), cookie(%d)",
- entry.attr_res_id, entry.value.data, entry.value.dataType,
+ res_id, entry.value.data, entry.value.dataType,
entry.cookie);
}
}
diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp
index bce34d37c90b..2d3c06506a1f 100644
--- a/libs/androidfw/AssetsProvider.cpp
+++ b/libs/androidfw/AssetsProvider.cpp
@@ -73,9 +73,6 @@ std::unique_ptr<Asset> AssetsProvider::CreateAssetFromFd(base::unique_fd fd,
(path != nullptr) ? base::unique_fd(-1) : std::move(fd));
}
-ZipAssetsProvider::PathOrDebugName::PathOrDebugName(std::string&& value, bool is_path)
- : value_(std::forward<std::string>(value)), is_path_(is_path) {}
-
const std::string* ZipAssetsProvider::PathOrDebugName::GetPath() const {
return is_path_ ? &value_ : nullptr;
}
@@ -84,34 +81,42 @@ const std::string& ZipAssetsProvider::PathOrDebugName::GetDebugName() const {
return value_;
}
+void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const {
+ ::CloseArchive(a);
+}
+
ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path,
package_property_t flags, time_t last_mod_time)
- : zip_handle_(handle, ::CloseArchive),
- name_(std::forward<PathOrDebugName>(path)),
+ : zip_handle_(handle),
+ name_(std::move(path)),
flags_(flags),
last_mod_time_(last_mod_time) {}
std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path,
- package_property_t flags) {
+ package_property_t flags,
+ base::unique_fd fd) {
+ const auto released_fd = fd.ok() ? fd.release() : -1;
ZipArchiveHandle handle;
- if (int32_t result = OpenArchive(path.c_str(), &handle); result != 0) {
+ if (int32_t result = released_fd < 0 ? OpenArchive(path.c_str(), &handle)
+ : OpenArchiveFd(released_fd, path.c_str(), &handle)) {
LOG(ERROR) << "Failed to open APK '" << path << "': " << ::ErrorCodeString(result);
CloseArchive(handle);
return {};
}
struct stat sb{.st_mtime = -1};
- if (stat(path.c_str(), &sb) < 0) {
- // Stat requires execute permissions on all directories path to the file. If the process does
- // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
- // always have to return true.
- LOG(WARNING) << "Failed to stat file '" << path << "': "
- << base::SystemErrorCodeToString(errno);
+ // Skip all up-to-date checks if the file won't ever change.
+ if (!isReadonlyFilesystem(path.c_str())) {
+ if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) {
+ // Stat requires execute permissions on all directories path to the file. If the process does
+ // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
+ // always have to return true.
+ PLOG(WARNING) << "Failed to stat file '" << path << "'";
+ }
}
return std::unique_ptr<ZipAssetsProvider>(
- new ZipAssetsProvider(handle, PathOrDebugName{std::move(path),
- true /* is_path */}, flags, sb.st_mtime));
+ new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime));
}
std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
@@ -133,17 +138,19 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
}
struct stat sb{.st_mtime = -1};
- if (fstat(released_fd, &sb) < 0) {
- // Stat requires execute permissions on all directories path to the file. If the process does
- // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
- // always have to return true.
- LOG(WARNING) << "Failed to fstat file '" << friendly_name << "': "
- << base::SystemErrorCodeToString(errno);
+ // Skip all up-to-date checks if the file won't ever change.
+ if (!isReadonlyFilesystem(released_fd)) {
+ if (fstat(released_fd, &sb) < 0) {
+ // Stat requires execute permissions on all directories path to the file. If the process does
+ // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
+ // always have to return true.
+ LOG(WARNING) << "Failed to fstat file '" << friendly_name
+ << "': " << base::SystemErrorCodeToString(errno);
+ }
}
- return std::unique_ptr<ZipAssetsProvider>(
- new ZipAssetsProvider(handle, PathOrDebugName{std::move(friendly_name),
- false /* is_path */}, flags, sb.st_mtime));
+ return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider(
+ handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime));
}
std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path,
@@ -210,9 +217,9 @@ std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path,
return asset;
}
-bool ZipAssetsProvider::ForEachFile(const std::string& root_path,
- const std::function<void(const StringPiece&, FileType)>& f)
- const {
+bool ZipAssetsProvider::ForEachFile(
+ const std::string& root_path,
+ base::function_ref<void(StringPiece, FileType)> f) const {
std::string root_path_full = root_path;
if (root_path_full.back() != '/') {
root_path_full += '/';
@@ -238,8 +245,7 @@ bool ZipAssetsProvider::ForEachFile(const std::string& root_path,
if (!leaf_file_path.empty()) {
auto iter = std::find(leaf_file_path.begin(), leaf_file_path.end(), '/');
if (iter != leaf_file_path.end()) {
- std::string dir =
- leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)).to_string();
+ std::string dir(leaf_file_path.substr(0, std::distance(leaf_file_path.begin(), iter)));
dirs.insert(std::move(dir));
} else {
f(leaf_file_path, kFileTypeRegular);
@@ -277,6 +283,9 @@ const std::string& ZipAssetsProvider::GetDebugName() const {
}
bool ZipAssetsProvider::IsUpToDate() const {
+ if (last_mod_time_ == -1) {
+ return true;
+ }
struct stat sb{};
if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) {
// If fstat fails on the zip archive, return true so the zip archive the resource system does
@@ -287,10 +296,10 @@ bool ZipAssetsProvider::IsUpToDate() const {
}
DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time)
- : dir_(std::forward<std::string>(path)), last_mod_time_(last_mod_time) {}
+ : dir_(std::move(path)), last_mod_time_(last_mod_time) {}
std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) {
- struct stat sb{};
+ struct stat sb;
const int result = stat(path.c_str(), &sb);
if (result == -1) {
LOG(ERROR) << "Failed to find directory '" << path << "'.";
@@ -302,12 +311,13 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st
return nullptr;
}
- if (path[path.size() - 1] != OS_PATH_SEPARATOR) {
+ if (path.back() != OS_PATH_SEPARATOR) {
path += OS_PATH_SEPARATOR;
}
- return std::unique_ptr<DirectoryAssetsProvider>(new DirectoryAssetsProvider(std::move(path),
- sb.st_mtime));
+ const bool isReadonly = isReadonlyFilesystem(path.c_str());
+ return std::unique_ptr<DirectoryAssetsProvider>(
+ new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime));
}
std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path,
@@ -324,8 +334,7 @@ std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string&
bool DirectoryAssetsProvider::ForEachFile(
const std::string& /* root_path */,
- const std::function<void(const StringPiece&, FileType)>& /* f */)
- const {
+ base::function_ref<void(StringPiece, FileType)> /* f */) const {
return true;
}
@@ -338,7 +347,10 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const {
}
bool DirectoryAssetsProvider::IsUpToDate() const {
- struct stat sb{};
+ if (last_mod_time_ == -1) {
+ return true;
+ }
+ struct stat sb;
if (stat(dir_.c_str(), &sb) < 0) {
// If stat fails on the zip archive, return true so the zip archive the resource system does
// attempt to refresh the ApkAsset.
@@ -349,8 +361,7 @@ bool DirectoryAssetsProvider::IsUpToDate() const {
MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary,
std::unique_ptr<AssetsProvider>&& secondary)
- : primary_(std::forward<std::unique_ptr<AssetsProvider>>(primary)),
- secondary_(std::forward<std::unique_ptr<AssetsProvider>>(secondary)) {
+ : primary_(std::move(primary)), secondary_(std::move(secondary)) {
debug_name_ = primary_->GetDebugName() + " and " + secondary_->GetDebugName();
path_ = (primary_->GetDebugName() != kEmptyDebugString) ? primary_->GetPath()
: secondary_->GetPath();
@@ -372,9 +383,9 @@ std::unique_ptr<Asset> MultiAssetsProvider::OpenInternal(const std::string& path
return (asset) ? std::move(asset) : secondary_->Open(path, mode, file_exists);
}
-bool MultiAssetsProvider::ForEachFile(const std::string& root_path,
- const std::function<void(const StringPiece&, FileType)>& f)
- const {
+bool MultiAssetsProvider::ForEachFile(
+ const std::string& root_path,
+ base::function_ref<void(StringPiece, FileType)> f) const {
return primary_->ForEachFile(root_path, f) && secondary_->ForEachFile(root_path, f);
}
@@ -397,8 +408,8 @@ std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create() {
return std::unique_ptr<EmptyAssetsProvider>(new EmptyAssetsProvider({}));
}
-std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create(const std::string& path) {
- return std::unique_ptr<EmptyAssetsProvider>(new EmptyAssetsProvider(path));
+std::unique_ptr<AssetsProvider> EmptyAssetsProvider::Create(std::string path) {
+ return std::unique_ptr<EmptyAssetsProvider>(new EmptyAssetsProvider(std::move(path)));
}
std::unique_ptr<Asset> EmptyAssetsProvider::OpenInternal(const std::string& /* path */,
@@ -412,7 +423,7 @@ std::unique_ptr<Asset> EmptyAssetsProvider::OpenInternal(const std::string& /* p
bool EmptyAssetsProvider::ForEachFile(
const std::string& /* root_path */,
- const std::function<void(const StringPiece&, FileType)>& /* f */) const {
+ base::function_ref<void(StringPiece, FileType)> /* f */) const {
return true;
}
@@ -435,4 +446,4 @@ bool EmptyAssetsProvider::IsUpToDate() const {
return true;
}
-} // namespace android \ No newline at end of file
+} // namespace android
diff --git a/libs/androidfw/BigBuffer.cpp b/libs/androidfw/BigBuffer.cpp
new file mode 100644
index 000000000000..bedfc49a1b0d
--- /dev/null
+++ b/libs/androidfw/BigBuffer.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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/BigBuffer.h>
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+#include "android-base/logging.h"
+
+namespace android {
+
+void* BigBuffer::NextBlockImpl(size_t size) {
+ if (!blocks_.empty()) {
+ Block& block = blocks_.back();
+ if (block.block_size_ - block.size >= size) {
+ void* out_buffer = block.buffer.get() + block.size;
+ block.size += size;
+ size_ += size;
+ return out_buffer;
+ }
+ }
+
+ const size_t actual_size = std::max(block_size_, size);
+
+ Block block = {};
+
+ // Zero-allocate the block's buffer.
+ block.buffer = std::unique_ptr<uint8_t[]>(new uint8_t[actual_size]());
+ CHECK(block.buffer);
+
+ block.size = size;
+ block.block_size_ = actual_size;
+
+ blocks_.push_back(std::move(block));
+ size_ += size;
+ return blocks_.back().buffer.get();
+}
+
+void* BigBuffer::NextBlock(size_t* out_size) {
+ if (!blocks_.empty()) {
+ Block& block = blocks_.back();
+ if (block.size != block.block_size_) {
+ void* out_buffer = block.buffer.get() + block.size;
+ size_t size = block.block_size_ - block.size;
+ block.size = block.block_size_;
+ size_ += size;
+ *out_size = size;
+ return out_buffer;
+ }
+ }
+
+ // Zero-allocate the block's buffer.
+ Block block = {};
+ block.buffer = std::unique_ptr<uint8_t[]>(new uint8_t[block_size_]());
+ CHECK(block.buffer);
+ block.size = block_size_;
+ block.block_size_ = block_size_;
+ blocks_.push_back(std::move(block));
+ size_ += block_size_;
+ *out_size = block_size_;
+ return blocks_.back().buffer.get();
+}
+
+std::string BigBuffer::to_string() const {
+ std::string result;
+ for (const Block& block : blocks_) {
+ result.append(block.buffer.get(), block.buffer.get() + block.size);
+ }
+ return result;
+}
+
+} // namespace android
diff --git a/libs/androidfw/ConfigDescription.cpp b/libs/androidfw/ConfigDescription.cpp
index 952101b1952c..e08030c4cca5 100644
--- a/libs/androidfw/ConfigDescription.cpp
+++ b/libs/androidfw/ConfigDescription.cpp
@@ -21,6 +21,7 @@
#include "androidfw/Util.h"
#include <string>
+#include <string_view>
#include <vector>
namespace android {
@@ -38,11 +39,11 @@ static bool parseMcc(const char* name, ResTable_config* out) {
return true;
}
const char* c = name;
- if (tolower(*c) != 'm') return false;
+ if (*c != 'm') return false;
c++;
- if (tolower(*c) != 'c') return false;
+ if (*c != 'c') return false;
c++;
- if (tolower(*c) != 'c') return false;
+ if (*c != 'c') return false;
c++;
const char* val = c;
@@ -68,11 +69,11 @@ static bool parseMnc(const char* name, ResTable_config* out) {
return true;
}
const char* c = name;
- if (tolower(*c) != 'm') return false;
+ if (*c != 'm') return false;
c++;
- if (tolower(*c) != 'n') return false;
+ if (*c != 'n') return false;
c++;
- if (tolower(*c) != 'c') return false;
+ if (*c != 'c') return false;
c++;
const char* val = c;
@@ -93,6 +94,23 @@ static bool parseMnc(const char* name, ResTable_config* out) {
return true;
}
+static bool parseGrammaticalInflection(const std::string& name, ResTable_config* out) {
+ using namespace std::literals;
+ if (name == "feminine"sv) {
+ if (out) out->grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_FEMININE;
+ return true;
+ }
+ if (name == "masculine"sv) {
+ if (out) out->grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_MASCULINE;
+ return true;
+ }
+ if (name == "neuter"sv) {
+ if (out) out->grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_NEUTER;
+ return true;
+ }
+ return false;
+}
+
static bool parseLayoutDirection(const char* name, ResTable_config* out) {
if (strcmp(name, kWildcardName) == 0) {
if (out)
@@ -637,7 +655,7 @@ static bool parseVersion(const char* name, ResTable_config* out) {
return true;
}
-bool ConfigDescription::Parse(const StringPiece& str, ConfigDescription* out) {
+bool ConfigDescription::Parse(StringPiece str, ConfigDescription* out) {
std::vector<std::string> parts = util::SplitAndLowercase(str, '-');
ConfigDescription config;
@@ -678,6 +696,13 @@ bool ConfigDescription::Parse(const StringPiece& str, ConfigDescription* out) {
}
}
+ if (parseGrammaticalInflection(*part_iter, &config)) {
+ ++part_iter;
+ if (part_iter == parts_end) {
+ goto success;
+ }
+ }
+
if (parseLayoutDirection(part_iter->c_str(), &config)) {
++part_iter;
if (part_iter == parts_end) {
@@ -832,11 +857,13 @@ success:
void ConfigDescription::ApplyVersionForCompatibility(
ConfigDescription* config) {
uint16_t min_sdk = 0;
- if ((config->uiMode & ResTable_config::MASK_UI_MODE_TYPE)
+ if (config->grammaticalInflection != 0) {
+ min_sdk = SDK_U;
+ } else if ((config->uiMode & ResTable_config::MASK_UI_MODE_TYPE)
== ResTable_config::UI_MODE_TYPE_VR_HEADSET ||
config->colorMode & ResTable_config::MASK_WIDE_COLOR_GAMUT ||
config->colorMode & ResTable_config::MASK_HDR) {
- min_sdk = SDK_O;
+ min_sdk = SDK_O;
} else if (config->screenLayout2 & ResTable_config::MASK_SCREENROUND) {
min_sdk = SDK_MARSHMALLOW;
} else if (config->density == ResTable_config::DENSITY_ANY) {
@@ -913,6 +940,7 @@ bool ConfigDescription::HasHigherPrecedenceThan(
if (country[0] || o.country[0]) return (!o.country[0]);
// Script and variant require either a language or country, both of which
// have higher precedence.
+ if (grammaticalInflection || o.grammaticalInflection) return !o.grammaticalInflection;
if ((screenLayout | o.screenLayout) & MASK_LAYOUTDIR) {
return !(o.screenLayout & MASK_LAYOUTDIR);
}
@@ -971,6 +999,7 @@ bool ConfigDescription::ConflictsWith(const ConfigDescription& o) const {
// The values here can be found in ResTable_config#match. Density and range
// values can't lead to conflicts, and are ignored.
return !pred(mcc, o.mcc) || !pred(mnc, o.mnc) || !pred(locale, o.locale) ||
+ !pred(grammaticalInflection, o.grammaticalInflection) ||
!pred(screenLayout & MASK_LAYOUTDIR,
o.screenLayout & MASK_LAYOUTDIR) ||
!pred(screenLayout & MASK_SCREENLONG,
diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp
index 5aa8e7006abb..5e645cceea2d 100644
--- a/libs/androidfw/CursorWindow.cpp
+++ b/libs/androidfw/CursorWindow.cpp
@@ -108,7 +108,7 @@ status_t CursorWindow::maybeInflate() {
{
// Migrate existing contents into new ashmem region
- uint32_t slotsSize = mSize - mSlotsOffset;
+ uint32_t slotsSize = sizeOfSlots();
uint32_t newSlotsOffset = mInflatedSize - slotsSize;
memcpy(static_cast<uint8_t*>(newData),
static_cast<uint8_t*>(mData), mAllocOffset);
@@ -216,11 +216,9 @@ status_t CursorWindow::writeToParcel(Parcel* parcel) {
if (parcel->writeDupFileDescriptor(mAshmemFd)) goto fail;
} else {
// Since we know we're going to be read-only on the remote side,
- // we can compact ourselves on the wire, with just enough padding
- // to ensure our slots stay aligned
- size_t slotsSize = mSize - mSlotsOffset;
- size_t compactedSize = mAllocOffset + slotsSize;
- compactedSize = (compactedSize + 3) & ~3;
+ // we can compact ourselves on the wire.
+ size_t slotsSize = sizeOfSlots();
+ size_t compactedSize = sizeInUse();
if (parcel->writeUint32(compactedSize)) goto fail;
if (parcel->writeBool(false)) goto fail;
void* dest = parcel->writeInplace(compactedSize);
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index efd1f6a25786..89835742c8ff 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -56,6 +56,8 @@ struct Idmap_header {
struct Idmap_data_header {
uint32_t target_entry_count;
uint32_t target_inline_entry_count;
+ uint32_t target_inline_entry_value_count;
+ uint32_t configuration_count;
uint32_t overlay_entry_count;
uint32_t string_pool_index_offset;
@@ -68,6 +70,12 @@ struct Idmap_target_entry {
struct Idmap_target_entry_inline {
uint32_t target_id;
+ uint32_t start_value_index;
+ uint32_t value_count;
+};
+
+struct Idmap_target_entry_inline_value {
+ uint32_t config_index;
Res_value value;
};
@@ -138,11 +146,15 @@ status_t OverlayDynamicRefTable::lookupResourceIdNoRewrite(uint32_t* resId) cons
IdmapResMap::IdmapResMap(const Idmap_data_header* data_header,
const Idmap_target_entry* entries,
const Idmap_target_entry_inline* inline_entries,
+ const Idmap_target_entry_inline_value* inline_entry_values,
+ const ConfigDescription* configs,
uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table)
: data_header_(data_header),
entries_(entries),
inline_entries_(inline_entries),
+ inline_entry_values_(inline_entry_values),
+ configurations_(configs),
target_assigned_package_id_(target_assigned_package_id),
overlay_ref_table_(overlay_ref_table) { }
@@ -183,7 +195,13 @@ IdmapResMap::Result IdmapResMap::Lookup(uint32_t target_res_id) const {
if (inline_entry != end_inline_entry &&
(0x00FFFFFFU & dtohl(inline_entry->target_id)) == target_res_id) {
- return Result(inline_entry->value);
+ std::map<ConfigDescription, Res_value> values_map;
+ for (int i = 0; i < inline_entry->value_count; i++) {
+ const auto& value = inline_entry_values_[inline_entry->start_value_index + i];
+ const auto& config = configurations_[value.config_index];
+ values_map[config] = value.value;
+ }
+ return Result(std::move(values_map));
}
return {};
}
@@ -232,28 +250,29 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size
}
} // namespace
-LoadedIdmap::LoadedIdmap(std::string&& idmap_path,
- const Idmap_header* header,
+LoadedIdmap::LoadedIdmap(std::string&& idmap_path, const Idmap_header* header,
const Idmap_data_header* data_header,
const Idmap_target_entry* target_entries,
const Idmap_target_entry_inline* target_inline_entries,
+ const Idmap_target_entry_inline_value* inline_entry_values,
+ const ConfigDescription* configs,
const Idmap_overlay_entry* overlay_entries,
std::unique_ptr<ResStringPool>&& string_pool,
- std::string_view overlay_apk_path,
- std::string_view target_apk_path)
- : header_(header),
- data_header_(data_header),
- target_entries_(target_entries),
- target_inline_entries_(target_inline_entries),
- overlay_entries_(overlay_entries),
- string_pool_(std::move(string_pool)),
- idmap_path_(std::move(idmap_path)),
- overlay_apk_path_(overlay_apk_path),
- target_apk_path_(target_apk_path),
- idmap_last_mod_time_(getFileModDate(idmap_path_.data())) {}
-
-std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path,
- const StringPiece& idmap_data) {
+ std::string_view overlay_apk_path, std::string_view target_apk_path)
+ : header_(header),
+ data_header_(data_header),
+ target_entries_(target_entries),
+ target_inline_entries_(target_inline_entries),
+ inline_entry_values_(inline_entry_values),
+ configurations_(configs),
+ overlay_entries_(overlay_entries),
+ string_pool_(std::move(string_pool)),
+ idmap_path_(std::move(idmap_path)),
+ overlay_apk_path_(overlay_apk_path),
+ target_apk_path_(target_apk_path),
+ idmap_last_mod_time_(getFileModDate(idmap_path_.data())) {}
+
+std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) {
ATRACE_CALL();
size_t data_size = idmap_data.size();
auto data_ptr = reinterpret_cast<const uint8_t*>(idmap_data.data());
@@ -303,6 +322,21 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path,
if (target_inline_entries == nullptr) {
return {};
}
+
+ auto target_inline_entry_values = ReadType<Idmap_target_entry_inline_value>(
+ &data_ptr, &data_size, "target inline values",
+ dtohl(data_header->target_inline_entry_value_count));
+ if (target_inline_entry_values == nullptr) {
+ return {};
+ }
+
+ auto configurations = ReadType<ConfigDescription>(
+ &data_ptr, &data_size, "configurations",
+ dtohl(data_header->configuration_count));
+ if (configurations == nullptr) {
+ return {};
+ }
+
auto overlay_entries = ReadType<Idmap_overlay_entry>(&data_ptr, &data_size, "target inline",
dtohl(data_header->overlay_entry_count));
if (overlay_entries == nullptr) {
@@ -328,9 +362,9 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(const StringPiece& idmap_path,
// Can't use make_unique because LoadedIdmap constructor is private.
return std::unique_ptr<LoadedIdmap>(
- new LoadedIdmap(idmap_path.to_string(), header, data_header, target_entries,
- target_inline_entries, overlay_entries, std::move(idmap_string_pool),
- *target_path, *overlay_path));
+ new LoadedIdmap(std::string(idmap_path), header, data_header, target_entries,
+ target_inline_entries, target_inline_entry_values, configurations,
+ overlay_entries, std::move(idmap_string_pool), *target_path, *overlay_path));
}
bool LoadedIdmap::IsUpToDate() const {
diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp
index 35b6170fae5b..c0fdfe25da21 100644
--- a/libs/androidfw/LoadedArsc.cpp
+++ b/libs/androidfw/LoadedArsc.cpp
@@ -88,7 +88,9 @@ static bool VerifyResTableType(incfs::map_ptr<ResTable_type> header) {
// Make sure that there is enough room for the entry offsets.
const size_t offsets_offset = dtohs(header->header.headerSize);
const size_t entries_offset = dtohl(header->entriesStart);
- const size_t offsets_length = sizeof(uint32_t) * entry_count;
+ const size_t offsets_length = header->flags & ResTable_type::FLAG_OFFSET16
+ ? sizeof(uint16_t) * entry_count
+ : sizeof(uint32_t) * entry_count;
if (offsets_offset > entries_offset || entries_offset - offsets_offset < offsets_length) {
LOG(ERROR) << "RES_TABLE_TYPE_TYPE entry offsets overlap actual entry data.";
@@ -107,8 +109,8 @@ static bool VerifyResTableType(incfs::map_ptr<ResTable_type> header) {
return true;
}
-static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry(
- incfs::verified_map_ptr<ResTable_type> type, uint32_t entry_offset) {
+static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError>
+VerifyResTableEntry(incfs::verified_map_ptr<ResTable_type> type, uint32_t entry_offset) {
// Check that the offset is aligned.
if (UNLIKELY(entry_offset & 0x03U)) {
LOG(ERROR) << "Entry at offset " << entry_offset << " is not 4-byte aligned.";
@@ -136,7 +138,7 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry(
return base::unexpected(IOError::PAGES_MISSING);
}
- const size_t entry_size = dtohs(entry->size);
+ const size_t entry_size = entry->size();
if (UNLIKELY(entry_size < sizeof(entry.value()))) {
LOG(ERROR) << "ResTable_entry size " << entry_size << " at offset " << entry_offset
<< " is too small.";
@@ -149,6 +151,11 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry(
return base::unexpected(std::nullopt);
}
+ // If entry is compact, value is already encoded, and a compact entry
+ // cannot be a map_entry, we are done verifying
+ if (entry->is_compact())
+ return entry.verified();
+
if (entry_size < sizeof(ResTable_map_entry)) {
// There needs to be room for one Res_value struct.
if (UNLIKELY(entry_offset + entry_size > chunk_size - sizeof(Res_value))) {
@@ -192,7 +199,7 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry(
return base::unexpected(std::nullopt);
}
}
- return {};
+ return entry.verified();
}
LoadedPackage::iterator::iterator(const LoadedPackage* lp, size_t ti, size_t ei)
@@ -228,7 +235,7 @@ uint32_t LoadedPackage::iterator::operator*() const {
entryIndex_);
}
-base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntry(
+base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntry(
incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index) {
base::expected<uint32_t, NullOrIOError> entry_offset = GetEntryOffset(type_chunk, entry_index);
if (UNLIKELY(!entry_offset.has_value())) {
@@ -242,14 +249,13 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::GetEntryOffset(
// The configuration matches and is better than the previous selection.
// Find the entry value if it exists for this configuration.
const size_t entry_count = dtohl(type_chunk->entryCount);
- const size_t offsets_offset = dtohs(type_chunk->header.headerSize);
+ const auto offsets = type_chunk.offset(dtohs(type_chunk->header.headerSize));
// Check if there is the desired entry in this type.
if (type_chunk->flags & ResTable_type::FLAG_SPARSE) {
// This is encoded as a sparse map, so perform a binary search.
bool error = false;
- auto sparse_indices = type_chunk.offset(offsets_offset)
- .convert<ResTable_sparseTypeEntry>().iterator();
+ auto sparse_indices = offsets.convert<ResTable_sparseTypeEntry>().iterator();
auto sparse_indices_end = sparse_indices + entry_count;
auto result = std::lower_bound(sparse_indices, sparse_indices_end, entry_index,
[&error](const incfs::map_ptr<ResTable_sparseTypeEntry>& entry,
@@ -284,26 +290,36 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::GetEntryOffset(
return base::unexpected(std::nullopt);
}
- const auto entry_offset_ptr = type_chunk.offset(offsets_offset).convert<uint32_t>() + entry_index;
- if (UNLIKELY(!entry_offset_ptr)) {
- return base::unexpected(IOError::PAGES_MISSING);
+ uint32_t result;
+
+ if (type_chunk->flags & ResTable_type::FLAG_OFFSET16) {
+ const auto entry_offset_ptr = offsets.convert<uint16_t>() + entry_index;
+ if (UNLIKELY(!entry_offset_ptr)) {
+ return base::unexpected(IOError::PAGES_MISSING);
+ }
+ result = offset_from16(entry_offset_ptr.value());
+ } else {
+ const auto entry_offset_ptr = offsets.convert<uint32_t>() + entry_index;
+ if (UNLIKELY(!entry_offset_ptr)) {
+ return base::unexpected(IOError::PAGES_MISSING);
+ }
+ result = dtohl(entry_offset_ptr.value());
}
- const uint32_t value = dtohl(entry_offset_ptr.value());
- if (value == ResTable_type::NO_ENTRY) {
+ if (result == ResTable_type::NO_ENTRY) {
return base::unexpected(std::nullopt);
}
-
- return value;
+ return result;
}
-base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntryFromOffset(
- incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset) {
+base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError>
+LoadedPackage::GetEntryFromOffset(incfs::verified_map_ptr<ResTable_type> type_chunk,
+ uint32_t offset) {
auto valid = VerifyResTableEntry(type_chunk, offset);
if (UNLIKELY(!valid.has_value())) {
return base::unexpected(valid.error());
}
- return type_chunk.offset(offset + dtohl(type_chunk->entriesStart)).convert<ResTable_entry>();
+ return valid;
}
base::expected<std::monostate, IOError> LoadedPackage::CollectConfigurations(
@@ -376,31 +392,42 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::FindEntryByName(
for (const auto& type_entry : type_spec->type_entries) {
const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type;
- size_t entry_count = dtohl(type->entryCount);
- for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) {
- auto entry_offset_ptr = type.offset(dtohs(type->header.headerSize)).convert<uint32_t>() +
- entry_idx;
- if (!entry_offset_ptr) {
- return base::unexpected(IOError::PAGES_MISSING);
- }
+ const size_t entry_count = dtohl(type->entryCount);
+ const auto entry_offsets = type.offset(dtohs(type->header.headerSize));
+ for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) {
uint32_t offset;
uint16_t res_idx;
if (type->flags & ResTable_type::FLAG_SPARSE) {
- auto sparse_entry = entry_offset_ptr.convert<ResTable_sparseTypeEntry>();
+ auto sparse_entry = entry_offsets.convert<ResTable_sparseTypeEntry>() + entry_idx;
+ if (!sparse_entry) {
+ return base::unexpected(IOError::PAGES_MISSING);
+ }
offset = dtohs(sparse_entry->offset) * 4u;
res_idx = dtohs(sparse_entry->idx);
+ } else if (type->flags & ResTable_type::FLAG_OFFSET16) {
+ auto entry = entry_offsets.convert<uint16_t>() + entry_idx;
+ if (!entry) {
+ return base::unexpected(IOError::PAGES_MISSING);
+ }
+ offset = offset_from16(entry.value());
+ res_idx = entry_idx;
} else {
- offset = dtohl(entry_offset_ptr.value());
+ auto entry = entry_offsets.convert<uint32_t>() + entry_idx;
+ if (!entry) {
+ return base::unexpected(IOError::PAGES_MISSING);
+ }
+ offset = dtohl(entry.value());
res_idx = entry_idx;
}
+
if (offset != ResTable_type::NO_ENTRY) {
auto entry = type.offset(dtohl(type->entriesStart) + offset).convert<ResTable_entry>();
if (!entry) {
return base::unexpected(IOError::PAGES_MISSING);
}
- if (dtohl(entry->key.index) == static_cast<uint32_t>(*key_idx)) {
+ if (entry->key() == static_cast<uint32_t>(*key_idx)) {
// The package ID will be overridden by the caller (due to runtime assignment of package
// IDs for shared libraries).
return make_resid(0x00, *type_idx + type_id_offset_ + 1, res_idx);
@@ -618,16 +645,16 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk,
}
std::string name;
- util::ReadUtf16StringFromDevice(overlayable->name, arraysize(overlayable->name), &name);
+ util::ReadUtf16StringFromDevice(overlayable->name, std::size(overlayable->name), &name);
std::string actor;
- util::ReadUtf16StringFromDevice(overlayable->actor, arraysize(overlayable->actor), &actor);
-
- if (loaded_package->overlayable_map_.find(name) !=
- loaded_package->overlayable_map_.end()) {
- LOG(ERROR) << "Multiple <overlayable> blocks with the same name '" << name << "'.";
+ util::ReadUtf16StringFromDevice(overlayable->actor, std::size(overlayable->actor), &actor);
+ auto [name_to_actor_it, inserted] =
+ loaded_package->overlayable_map_.emplace(std::move(name), std::move(actor));
+ if (!inserted) {
+ LOG(ERROR) << "Multiple <overlayable> blocks with the same name '"
+ << name_to_actor_it->first << "'.";
return {};
}
- loaded_package->overlayable_map_.emplace(name, actor);
// Iterate over the overlayable policy chunks contained within the overlayable chunk data
ChunkIterator overlayable_iter(child_chunk.data_ptr(), child_chunk.data_size());
@@ -642,7 +669,6 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk,
LOG(ERROR) << "RES_TABLE_OVERLAYABLE_POLICY_TYPE too small.";
return {};
}
-
if ((overlayable_child_chunk.data_size() / sizeof(ResTable_ref))
< dtohl(policy_header->entry_count)) {
LOG(ERROR) << "RES_TABLE_OVERLAYABLE_POLICY_TYPE too small to hold entries.";
@@ -664,8 +690,8 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk,
// Add the pairing of overlayable properties and resource ids to the package
OverlayableInfo overlayable_info {
- .name = name,
- .actor = actor,
+ .name = name_to_actor_it->first,
+ .actor = name_to_actor_it->second,
.policy_flags = policy_header->policy_flags
};
loaded_package->overlayable_infos_.emplace_back(std::move(overlayable_info), std::move(ids));
@@ -709,6 +735,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk,
const auto entry_end = entry_begin + dtohl(lib_alias->count);
std::unordered_set<uint32_t> finalized_ids;
finalized_ids.reserve(entry_end - entry_begin);
+ loaded_package->alias_id_map_.reserve(entry_end - entry_begin);
for (auto entry_iter = entry_begin; entry_iter != entry_end; ++entry_iter) {
if (!entry_iter) {
LOG(ERROR) << "NULL ResTable_staged_alias_entry record??";
@@ -722,13 +749,20 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk,
}
auto staged_id = dtohl(entry_iter->stagedResId);
- auto [_, success] = loaded_package->alias_id_map_.emplace(staged_id, finalized_id);
- if (!success) {
+ loaded_package->alias_id_map_.emplace_back(staged_id, finalized_id);
+ }
+
+ std::sort(loaded_package->alias_id_map_.begin(), loaded_package->alias_id_map_.end(),
+ [](auto&& l, auto&& r) { return l.first < r.first; });
+ const auto duplicate_it =
+ std::adjacent_find(loaded_package->alias_id_map_.begin(),
+ loaded_package->alias_id_map_.end(),
+ [](auto&& l, auto&& r) { return l.first == r.first; });
+ if (duplicate_it != loaded_package->alias_id_map_.end()) {
LOG(ERROR) << StringPrintf("Repeated staged resource id '%08x' in staged aliases.",
- staged_id);
+ duplicate_it->first);
return {};
}
- }
} break;
default:
@@ -820,6 +854,13 @@ bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap,
return true;
}
+bool LoadedArsc::LoadStringPool(const LoadedIdmap* loaded_idmap) {
+ if (loaded_idmap != nullptr) {
+ global_string_pool_ = util::make_unique<OverlayStringPool>(loaded_idmap);
+ }
+ return true;
+}
+
std::unique_ptr<LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data,
const size_t length,
const LoadedIdmap* loaded_idmap,
@@ -855,6 +896,16 @@ std::unique_ptr<LoadedArsc> LoadedArsc::Load(incfs::map_ptr<void> data,
return loaded_arsc;
}
+std::unique_ptr<LoadedArsc> LoadedArsc::Load(const LoadedIdmap* loaded_idmap) {
+ ATRACE_NAME("LoadedArsc::Load");
+
+ // Not using make_unique because the constructor is private.
+ std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc());
+ loaded_arsc->LoadStringPool(loaded_idmap);
+ return loaded_arsc;
+}
+
+
std::unique_ptr<LoadedArsc> LoadedArsc::CreateEmpty() {
return std::unique_ptr<LoadedArsc>(new LoadedArsc());
}
diff --git a/libs/androidfw/Locale.cpp b/libs/androidfw/Locale.cpp
index d87a3ce72177..272a988ec55a 100644
--- a/libs/androidfw/Locale.cpp
+++ b/libs/androidfw/Locale.cpp
@@ -66,7 +66,7 @@ static inline bool is_number(const std::string& str) {
return std::all_of(std::begin(str), std::end(str), ::isdigit);
}
-bool LocaleValue::InitFromFilterString(const StringPiece& str) {
+bool LocaleValue::InitFromFilterString(StringPiece str) {
// A locale (as specified in the filter) is an underscore separated name such
// as "en_US", "en_Latn_US", or "en_US_POSIX".
std::vector<std::string> parts = util::SplitAndLowercase(str, '_');
@@ -132,11 +132,11 @@ bool LocaleValue::InitFromFilterString(const StringPiece& str) {
return true;
}
-bool LocaleValue::InitFromBcp47Tag(const StringPiece& bcp47tag) {
+bool LocaleValue::InitFromBcp47Tag(StringPiece bcp47tag) {
return InitFromBcp47TagImpl(bcp47tag, '-');
}
-bool LocaleValue::InitFromBcp47TagImpl(const StringPiece& bcp47tag, const char separator) {
+bool LocaleValue::InitFromBcp47TagImpl(StringPiece bcp47tag, const char separator) {
std::vector<std::string> subtags = util::SplitAndLowercase(bcp47tag, separator);
if (subtags.size() == 1) {
set_language(subtags[0].c_str());
diff --git a/libs/androidfw/PosixUtils.cpp b/libs/androidfw/PosixUtils.cpp
index 026912883a73..8ddc57240129 100644
--- a/libs/androidfw/PosixUtils.cpp
+++ b/libs/androidfw/PosixUtils.cpp
@@ -17,7 +17,7 @@
#ifdef _WIN32
// nothing to see here
#else
-#include <memory>
+#include <optional>
#include <string>
#include <vector>
@@ -29,45 +29,42 @@
#include "androidfw/PosixUtils.h"
-namespace {
-
-std::unique_ptr<std::string> ReadFile(int fd) {
- std::unique_ptr<std::string> str(new std::string());
+static std::optional<std::string> ReadFile(int fd) {
+ std::string str;
char buf[1024];
ssize_t r;
while ((r = read(fd, buf, sizeof(buf))) > 0) {
- str->append(buf, r);
+ str.append(buf, r);
}
if (r != 0) {
- return nullptr;
+ return std::nullopt;
}
- return str;
-}
-
+ return std::move(str);
}
namespace android {
namespace util {
-std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv) {
- int stdout[2]; // stdout[0] read, stdout[1] write
+ProcResult ExecuteBinary(const std::vector<std::string>& argv) {
+ int stdout[2]; // [0] read, [1] write
if (pipe(stdout) != 0) {
- PLOG(ERROR) << "pipe";
- return nullptr;
+ PLOG(ERROR) << "out pipe";
+ return ProcResult{-1};
}
- int stderr[2]; // stdout[0] read, stdout[1] write
+ int stderr[2]; // [0] read, [1] write
if (pipe(stderr) != 0) {
- PLOG(ERROR) << "pipe";
+ PLOG(ERROR) << "err pipe";
close(stdout[0]);
close(stdout[1]);
- return nullptr;
+ return ProcResult{-1};
}
auto gid = getgid();
auto uid = getuid();
- char const** argv0 = (char const**)malloc(sizeof(char*) * (argv.size() + 1));
+ // better keep no C++ objects going into the child here
+ auto argv0 = (char const**)malloc(sizeof(char*) * (argv.size() + 1));
for (size_t i = 0; i < argv.size(); i++) {
argv0[i] = argv[i].c_str();
}
@@ -76,8 +73,12 @@ std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv)
switch (pid) {
case -1: // error
free(argv0);
+ close(stdout[0]);
+ close(stdout[1]);
+ close(stderr[0]);
+ close(stderr[1]);
PLOG(ERROR) << "fork";
- return nullptr;
+ return ProcResult{-1};
case 0: // child
if (setgid(gid) != 0) {
PLOG(ERROR) << "setgid";
@@ -109,17 +110,16 @@ std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv)
if (!WIFEXITED(status)) {
close(stdout[0]);
close(stderr[0]);
- return nullptr;
+ return ProcResult{-1};
}
- std::unique_ptr<ProcResult> result(new ProcResult());
- result->status = status;
- const auto out = ReadFile(stdout[0]);
- result->stdout_str = out ? *out : "";
+ ProcResult result(status);
+ auto out = ReadFile(stdout[0]);
+ result.stdout_str = out ? std::move(*out) : "";
close(stdout[0]);
- const auto err = ReadFile(stderr[0]);
- result->stderr_str = err ? *err : "";
+ auto err = ReadFile(stderr[0]);
+ result.stderr_str = err ? std::move(*err) : "";
close(stderr[0]);
- return result;
+ return std::move(result);
}
}
diff --git a/libs/androidfw/ResourceTimer.cpp b/libs/androidfw/ResourceTimer.cpp
new file mode 100644
index 000000000000..44128d9e4e3d
--- /dev/null
+++ b/libs/androidfw/ResourceTimer.cpp
@@ -0,0 +1,271 @@
+/*
+ * 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.
+ */
+
+#include <unistd.h>
+#include <string.h>
+
+#include <map>
+#include <atomic>
+
+#include <utils/Log.h>
+#include <androidfw/ResourceTimer.h>
+
+// The following block allows compilation on windows, which does not have getuid().
+#ifdef _WIN32
+#ifdef ERROR
+#undef ERROR
+#endif
+#define getuid() (getUidWindows_)
+#endif
+
+namespace android {
+
+namespace {
+
+#ifdef _WIN32
+// A temporary to confuse lint into thinking that getuid() on windows might return something other
+// than zero.
+int getUidWindows_ = 0;
+#endif
+
+// The number of nanoseconds in a microsecond.
+static const unsigned int US = 1000;
+// The number of nanoseconds in a second.
+static const unsigned int S = 1000 * 1000 * 1000;
+
+// Return the difference between two timespec values. The difference is in nanoseconds. If the
+// return value would exceed 2s (2^31 nanoseconds) then UINT_MAX is returned.
+unsigned int diffInNs(timespec const &a, timespec const &b) {
+ timespec r = { 0, 0 };
+ r.tv_nsec = a.tv_nsec - b.tv_nsec;
+ if (r.tv_nsec < 0) {
+ r.tv_sec = -1;
+ r.tv_nsec += S;
+ }
+ r.tv_sec = r.tv_sec + (a.tv_sec - b.tv_sec);
+ if (r.tv_sec > 2) return UINT_MAX;
+ unsigned int result = (r.tv_sec * S) + r.tv_nsec;
+ if (result > 2 * S) return UINT_MAX;
+ return result;
+}
+
+}
+
+ResourceTimer::ResourceTimer(Counter api)
+ : active_(enabled_.load()),
+ api_(api) {
+ if (active_) {
+ clock_gettime(CLOCK_MONOTONIC, &start_);
+ }
+}
+
+ResourceTimer::~ResourceTimer() {
+ record();
+}
+
+void ResourceTimer::enable() {
+ if (!enabled_.load()) counter_ = new GuardedTimer[ResourceTimer::counterSize];
+ enabled_.store(true);
+}
+
+void ResourceTimer::cancel() {
+ active_ = false;
+}
+
+void ResourceTimer::record() {
+ if (!active_) return;
+
+ struct timespec end;
+ clock_gettime(CLOCK_MONOTONIC, &end);
+ // Get the difference in microseconds.
+ const unsigned int ticks = diffInNs(end, start_);
+ ScopedTimer t(counter_[toIndex(api_)]);
+ t->record(ticks);
+ active_ = false;
+}
+
+bool ResourceTimer::copy(int counter, Timer &dst, bool reset) {
+ ScopedTimer t(counter_[counter]);
+ if (t->count == 0) {
+ dst.reset();
+ if (reset) t->reset();
+ return false;
+ }
+ Timer::copy(dst, *t, reset);
+ return true;
+}
+
+void ResourceTimer::reset() {
+ for (int i = 0; i < counterSize; i++) {
+ ScopedTimer t(counter_[i]);
+ t->reset();
+ }
+}
+
+ResourceTimer::Timer::Timer() {
+ // Ensure newly-created objects are zeroed.
+ memset(buckets, 0, sizeof(buckets));
+ reset();
+}
+
+ResourceTimer::Timer::~Timer() {
+ for (int d = 0; d < MaxDimension; d++) {
+ delete[] buckets[d];
+ }
+}
+
+void ResourceTimer::Timer::freeBuckets() {
+ for (int d = 0; d < MaxDimension; d++) {
+ delete[] buckets[d];
+ buckets[d] = 0;
+ }
+}
+
+void ResourceTimer::Timer::reset() {
+ count = total = mintime = maxtime = 0;
+ memset(largest, 0, sizeof(largest));
+ memset(&pvalues, 0, sizeof(pvalues));
+ // Zero the histogram, keeping any allocated dimensions.
+ for (int d = 0; d < MaxDimension; d++) {
+ if (buckets[d] != 0) memset(buckets[d], 0, sizeof(int) * MaxBuckets);
+ }
+}
+
+void ResourceTimer::Timer::copy(Timer &dst, Timer &src, bool reset) {
+ dst.freeBuckets();
+ dst = src;
+ // Clean up the histograms.
+ if (reset) {
+ // Do NOT free the src buckets because they being used by dst.
+ memset(src.buckets, 0, sizeof(src.buckets));
+ src.reset();
+ } else {
+ for (int d = 0; d < MaxDimension; d++) {
+ if (src.buckets[d] != nullptr) {
+ dst.buckets[d] = new int[MaxBuckets];
+ memcpy(dst.buckets[d], src.buckets[d], sizeof(int) * MaxBuckets);
+ }
+ }
+ }
+}
+
+void ResourceTimer::Timer::record(int ticks) {
+ // Record that the event happened.
+ count++;
+
+ total += ticks;
+ if (mintime == 0 || ticks < mintime) mintime = ticks;
+ if (ticks > maxtime) maxtime = ticks;
+
+ // Do not add oversized events to the histogram.
+ if (ticks != UINT_MAX) {
+ for (int d = 0; d < MaxDimension; d++) {
+ if (ticks < range[d]) {
+ if (buckets[d] == 0) {
+ buckets[d] = new int[MaxBuckets];
+ memset(buckets[d], 0, sizeof(int) * MaxBuckets);
+ }
+ if (ticks < width[d]) {
+ // Special case: never write to bucket 0 because it complicates the percentile logic.
+ // However, this is always the smallest possible value to it is very unlikely to ever
+ // affect any of the percentile results.
+ buckets[d][1]++;
+ } else {
+ buckets[d][ticks / width[d]]++;
+ }
+ break;
+ }
+ }
+ }
+
+ // The list of largest times is sorted with the biggest value at index 0 and the smallest at
+ // index MaxLargest-1. The incoming tick count should be added to the array only if it is
+ // larger than the current value at MaxLargest-1.
+ if (ticks > largest[Timer::MaxLargest-1]) {
+ for (size_t i = 0; i < Timer::MaxLargest; i++) {
+ if (ticks > largest[i]) {
+ if (i < Timer::MaxLargest-1) {
+ for (size_t j = Timer::MaxLargest - 1; j > i; j--) {
+ largest[j] = largest[j-1];
+ }
+ }
+ largest[i] = ticks;
+ break;
+ }
+ }
+ }
+}
+
+void ResourceTimer::Timer::Percentile::compute(
+ int cumulative, int current, int count, int width, int time) {
+ nominal = time;
+ nominal_actual = (cumulative * 100) / count;
+ floor = nominal - width;
+ floor_actual = ((cumulative - current) * 100) / count;
+}
+
+void ResourceTimer::Timer::compute() {
+ memset(&pvalues, 0, sizeof(pvalues));
+
+ float l50 = count / 2.0;
+ float l90 = (count * 9.0) / 10.0;
+ float l95 = (count * 95.0) / 100.0;
+ float l99 = (count * 99.0) / 100.0;
+
+ int sum = 0;
+ for (int d = 0; d < MaxDimension; d++) {
+ if (buckets[d] == 0) continue;
+ for (int j = 0; j < MaxBuckets && sum < count; j++) {
+ // Empty buckets don't contribute to the answers. Skip them.
+ if (buckets[d][j] == 0) continue;
+ sum += buckets[d][j];
+ // A word on indexing. j is never zero in the following lines. buckets[0][0] corresponds
+ // to a delay of 0us, which cannot happen. buckets[n][0], for n > 0 overlaps a value in
+ // buckets[n-1], and the code would have stopped there.
+ if (sum >= l50 && pvalues.p50.nominal == 0) {
+ pvalues.p50.compute(sum, buckets[d][j], count, width[d], j * width[d]);
+ }
+ if (sum >= l90 && pvalues.p90.nominal == 0) {
+ pvalues.p90.compute(sum, buckets[d][j], count, width[d], j * width[d]);
+ }
+ if (sum >= l95 && pvalues.p95.nominal == 0) {
+ pvalues.p95.compute(sum, buckets[d][j], count, width[d], j * width[d]);
+ }
+ if (sum >= l99 && pvalues.p99.nominal == 0) {
+ pvalues.p99.compute(sum, buckets[d][j], count, width[d], j * width[d]);
+ }
+ }
+ }
+}
+
+char const *ResourceTimer::toString(ResourceTimer::Counter counter) {
+ switch (counter) {
+ case Counter::GetResourceValue:
+ return "GetResourceValue";
+ case Counter::RetrieveAttributes:
+ return "RetrieveAttributes";
+ };
+ return "Unknown";
+}
+
+std::atomic<bool> ResourceTimer::enabled_(false);
+std::atomic<ResourceTimer::GuardedTimer *> ResourceTimer::counter_(nullptr);
+
+const int ResourceTimer::Timer::range[] = { 100 * US, 1000 * US, 10*1000 * US, 100*1000 * US };
+const int ResourceTimer::Timer::width[] = { 1 * US, 10 * US, 100 * US, 1000 * US };
+
+
+} // namespace android
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 0e7d8412e615..c7a17af9f708 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -33,7 +33,9 @@
#include <type_traits>
#include <vector>
+#include <android-base/file.h>
#include <android-base/macros.h>
+#include <android-base/utf8.h>
#include <androidfw/ByteBucketArray.h>
#include <androidfw/ResourceTypes.h>
#include <androidfw/TypeWrappers.h>
@@ -236,12 +238,23 @@ void Res_png_9patch::serialize(const Res_png_9patch& patch, const int32_t* xDivs
}
bool IsFabricatedOverlay(const std::string& path) {
- std::ifstream fin(path);
+ return IsFabricatedOverlay(path.c_str());
+}
+
+bool IsFabricatedOverlay(const char* path) {
+ auto fd = base::unique_fd(base::utf8::open(path, O_RDONLY|O_CLOEXEC));
+ if (fd < 0) {
+ return false;
+ }
+ return IsFabricatedOverlay(fd);
+}
+
+bool IsFabricatedOverlay(base::borrowed_fd fd) {
uint32_t magic;
- if (fin.read(reinterpret_cast<char*>(&magic), sizeof(uint32_t))) {
- return magic == kFabricatedOverlayMagic;
+ if (!base::ReadFullyAtOffset(fd, &magic, sizeof(magic), 0)) {
+ return false;
}
- return false;
+ return magic == kFabricatedOverlayMagic;
}
static bool assertIdmapHeader(const void* idmap, size_t size) {
@@ -2092,6 +2105,9 @@ int ResTable_config::compare(const ResTable_config& o) const {
return 1;
}
+ if (grammaticalInflection != o.grammaticalInflection) {
+ return grammaticalInflection < o.grammaticalInflection ? -1 : 1;
+ }
if (screenType != o.screenType) {
return (screenType > o.screenType) ? 1 : -1;
}
@@ -2140,7 +2156,9 @@ int ResTable_config::compareLogical(const ResTable_config& o) const {
if (diff > 0) {
return 1;
}
-
+ if (grammaticalInflection != o.grammaticalInflection) {
+ return grammaticalInflection < o.grammaticalInflection ? -1 : 1;
+ }
if ((screenLayout & MASK_LAYOUTDIR) != (o.screenLayout & MASK_LAYOUTDIR)) {
return (screenLayout & MASK_LAYOUTDIR) < (o.screenLayout & MASK_LAYOUTDIR) ? -1 : 1;
}
@@ -2210,6 +2228,7 @@ int ResTable_config::diff(const ResTable_config& o) const {
if (uiMode != o.uiMode) diffs |= CONFIG_UI_MODE;
if (smallestScreenWidthDp != o.smallestScreenWidthDp) diffs |= CONFIG_SMALLEST_SCREEN_SIZE;
if (screenSizeDp != o.screenSizeDp) diffs |= CONFIG_SCREEN_SIZE;
+ if (grammaticalInflection != o.grammaticalInflection) diffs |= CONFIG_GRAMMATICAL_GENDER;
const int diff = compareLocales(*this, o);
if (diff) diffs |= CONFIG_LOCALE;
@@ -2276,6 +2295,13 @@ bool ResTable_config::isMoreSpecificThan(const ResTable_config& o) const {
}
}
+ if (grammaticalInflection || o.grammaticalInflection) {
+ if (grammaticalInflection != o.grammaticalInflection) {
+ if (!grammaticalInflection) return false;
+ if (!o.grammaticalInflection) return true;
+ }
+ }
+
if (screenLayout || o.screenLayout) {
if (((screenLayout^o.screenLayout) & MASK_LAYOUTDIR) != 0) {
if (!(screenLayout & MASK_LAYOUTDIR)) return false;
@@ -2542,6 +2568,13 @@ bool ResTable_config::isBetterThan(const ResTable_config& o,
return true;
}
+ if (grammaticalInflection || o.grammaticalInflection) {
+ if (grammaticalInflection != o.grammaticalInflection
+ && requested->grammaticalInflection) {
+ return !!grammaticalInflection;
+ }
+ }
+
if (screenLayout || o.screenLayout) {
if (((screenLayout^o.screenLayout) & MASK_LAYOUTDIR) != 0
&& (requested->screenLayout & MASK_LAYOUTDIR)) {
@@ -2841,6 +2874,10 @@ bool ResTable_config::match(const ResTable_config& settings) const {
}
}
+ if (grammaticalInflection && grammaticalInflection != settings.grammaticalInflection) {
+ return false;
+ }
+
if (screenConfig != 0) {
const int layoutDir = screenLayout&MASK_LAYOUTDIR;
const int setLayoutDir = settings.screenLayout&MASK_LAYOUTDIR;
@@ -3254,6 +3291,15 @@ String8 ResTable_config::toString() const {
appendDirLocale(res);
+ if ((grammaticalInflection & GRAMMATICAL_INFLECTION_GENDER_MASK) != 0) {
+ if (res.size() > 0) res.append("-");
+ switch (grammaticalInflection & GRAMMATICAL_INFLECTION_GENDER_MASK) {
+ case GRAMMATICAL_GENDER_NEUTER: res.append("neuter"); break;
+ case GRAMMATICAL_GENDER_FEMININE: res.append("feminine"); break;
+ case GRAMMATICAL_GENDER_MASCULINE: res.append("masculine"); break;
+ }
+ }
+
if ((screenLayout&MASK_LAYOUTDIR) != 0) {
if (res.size() > 0) res.append("-");
switch (screenLayout&ResTable_config::MASK_LAYOUTDIR) {
@@ -4487,20 +4533,14 @@ ssize_t ResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag
return err;
}
- if ((dtohs(entry.entry->flags) & ResTable_entry::FLAG_COMPLEX) != 0) {
+ if (entry.entry->map_entry()) {
if (!mayBeBag) {
ALOGW("Requesting resource 0x%08x failed because it is complex\n", resID);
}
return BAD_VALUE;
}
- const Res_value* value = reinterpret_cast<const Res_value*>(
- reinterpret_cast<const uint8_t*>(entry.entry) + entry.entry->size);
-
- outValue->size = dtohs(value->size);
- outValue->res0 = value->res0;
- outValue->dataType = value->dataType;
- outValue->data = dtohl(value->data);
+ *outValue = entry.entry->value();
// The reference may be pointing to a resource in a shared library. These
// references have build-time generated package IDs. These ids may not match
@@ -4691,11 +4731,10 @@ ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag,
return err;
}
- const uint16_t entrySize = dtohs(entry.entry->size);
- const uint32_t parent = entrySize >= sizeof(ResTable_map_entry)
- ? dtohl(((const ResTable_map_entry*)entry.entry)->parent.ident) : 0;
- const uint32_t count = entrySize >= sizeof(ResTable_map_entry)
- ? dtohl(((const ResTable_map_entry*)entry.entry)->count) : 0;
+ const uint16_t entrySize = entry.entry->size();
+ const ResTable_map_entry* map_entry = entry.entry->map_entry();
+ const uint32_t parent = map_entry ? dtohl(map_entry->parent.ident) : 0;
+ const uint32_t count = map_entry ? dtohl(map_entry->count) : 0;
size_t N = count;
@@ -4759,7 +4798,7 @@ ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag,
// Now merge in the new attributes...
size_t curOff = (reinterpret_cast<uintptr_t>(entry.entry) - reinterpret_cast<uintptr_t>(entry.type))
- + dtohs(entry.entry->size);
+ + entrySize;
const ResTable_map* map;
bag_entry* entries = (bag_entry*)(set+1);
size_t curEntry = 0;
@@ -5137,7 +5176,7 @@ uint32_t ResTable::findEntry(const PackageGroup* group, ssize_t typeIndex, const
continue;
}
- if (dtohl(entry->key.index) == (size_t) *ei) {
+ if (entry->key() == (size_t) *ei) {
uint32_t resId = Res_MAKEID(group->id - 1, typeIndex, iter.index());
if (outTypeSpecFlags) {
Entry result;
@@ -6600,8 +6639,12 @@ status_t ResTable::getEntry(
// Entry does not exist.
continue;
}
-
- thisOffset = dtohl(eindex[realEntryIndex]);
+ if (thisType->flags & ResTable_type::FLAG_OFFSET16) {
+ auto eindex16 = reinterpret_cast<const uint16_t*>(eindex);
+ thisOffset = offset_from16(eindex16[realEntryIndex]);
+ } else {
+ thisOffset = dtohl(eindex[realEntryIndex]);
+ }
}
if (thisOffset == ResTable_type::NO_ENTRY) {
@@ -6651,8 +6694,8 @@ status_t ResTable::getEntry(
const ResTable_entry* const entry = reinterpret_cast<const ResTable_entry*>(
reinterpret_cast<const uint8_t*>(bestType) + bestOffset);
- if (dtohs(entry->size) < sizeof(*entry)) {
- ALOGW("ResTable_entry size 0x%x is too small", dtohs(entry->size));
+ if (entry->size() < sizeof(*entry)) {
+ ALOGW("ResTable_entry size 0x%zx is too small", entry->size());
return BAD_TYPE;
}
@@ -6663,7 +6706,7 @@ status_t ResTable::getEntry(
outEntry->specFlags = specFlags;
outEntry->package = bestPackage;
outEntry->typeStr = StringPoolRef(&bestPackage->typeStrings, actualTypeIndex - bestPackage->typeIdOffset);
- outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, dtohl(entry->key.index));
+ outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, entry->key());
}
return NO_ERROR;
}
@@ -6880,7 +6923,8 @@ status_t ResTable::parsePackage(const ResTable_package* const pkg,
const uint32_t typeSize = dtohl(type->header.size);
const size_t newEntryCount = dtohl(type->entryCount);
-
+ const size_t entrySize = type->flags & ResTable_type::FLAG_OFFSET16 ?
+ sizeof(uint16_t) : sizeof(uint32_t);
if (kDebugLoadTableNoisy) {
printf("Type off %p: type=0x%x, headerSize=0x%x, size=%u\n",
(void*)(base-(const uint8_t*)chunk),
@@ -6888,9 +6932,9 @@ status_t ResTable::parsePackage(const ResTable_package* const pkg,
dtohs(type->header.headerSize),
typeSize);
}
- if (dtohs(type->header.headerSize)+(sizeof(uint32_t)*newEntryCount) > typeSize) {
+ if (dtohs(type->header.headerSize)+(entrySize*newEntryCount) > typeSize) {
ALOGW("ResTable_type entry index to %p extends beyond chunk end 0x%x.",
- (void*)(dtohs(type->header.headerSize) + (sizeof(uint32_t)*newEntryCount)),
+ (void*)(dtohs(type->header.headerSize) + (entrySize*newEntryCount)),
typeSize);
return (mError=BAD_TYPE);
}
@@ -6991,11 +7035,10 @@ status_t ResTable::parsePackage(const ResTable_package* const pkg,
DynamicRefTable::DynamicRefTable() : DynamicRefTable(0, false) {}
DynamicRefTable::DynamicRefTable(uint8_t packageId, bool appAsLib)
- : mAssignedPackageId(packageId)
+ : mLookupTable()
+ , mAssignedPackageId(packageId)
, mAppAsLib(appAsLib)
{
- memset(mLookupTable, 0, sizeof(mLookupTable));
-
// Reserved package ids
mLookupTable[APP_PACKAGE_ID] = APP_PACKAGE_ID;
mLookupTable[SYS_PACKAGE_ID] = SYS_PACKAGE_ID;
@@ -7076,10 +7119,6 @@ void DynamicRefTable::addMapping(uint8_t buildPackageId, uint8_t runtimePackageI
mLookupTable[buildPackageId] = runtimePackageId;
}
-void DynamicRefTable::addAlias(uint32_t stagedId, uint32_t finalizedId) {
- mAliasId[stagedId] = finalizedId;
-}
-
status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {
uint32_t res = *resId;
size_t packageId = Res_GETPACKAGE(res) + 1;
@@ -7089,11 +7128,12 @@ status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {
return NO_ERROR;
}
- auto alias_id = mAliasId.find(res);
- if (alias_id != mAliasId.end()) {
+ const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res,
+ [](const AliasMap::value_type& pair, uint32_t val) { return pair.first < val; });
+ if (alias_it != mAliasId.end() && alias_it->first == res) {
// Rewrite the resource id to its alias resource id. Since the alias resource id is a
// compile-time id, it still needs to be resolved further.
- res = alias_id->second;
+ res = alias_it->second;
}
if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) {
@@ -7653,6 +7693,9 @@ void ResTable::print(bool inclValues) const
if (type->flags & ResTable_type::FLAG_SPARSE) {
printf(" [sparse]");
}
+ if (type->flags & ResTable_type::FLAG_OFFSET16) {
+ printf(" [offset16]");
+ }
}
printf(":\n");
@@ -7684,7 +7727,13 @@ void ResTable::print(bool inclValues) const
thisOffset = static_cast<uint32_t>(dtohs(entry->offset)) * 4u;
} else {
entryId = entryIndex;
- thisOffset = dtohl(eindex[entryIndex]);
+ if (type->flags & ResTable_type::FLAG_OFFSET16) {
+ const auto eindex16 =
+ reinterpret_cast<const uint16_t*>(eindex);
+ thisOffset = offset_from16(eindex16[entryIndex]);
+ } else {
+ thisOffset = dtohl(eindex[entryIndex]);
+ }
if (thisOffset == ResTable_type::NO_ENTRY) {
continue;
}
@@ -7734,7 +7783,7 @@ void ResTable::print(bool inclValues) const
continue;
}
- uintptr_t esize = dtohs(ent->size);
+ uintptr_t esize = ent->size();
if ((esize&0x3) != 0) {
printf("NON-INTEGER ResTable_entry SIZE: %p\n", (void *)esize);
continue;
@@ -7746,30 +7795,27 @@ void ResTable::print(bool inclValues) const
}
const Res_value* valuePtr = NULL;
- const ResTable_map_entry* bagPtr = NULL;
+ const ResTable_map_entry* bagPtr = ent->map_entry();
Res_value value;
- if ((dtohs(ent->flags)&ResTable_entry::FLAG_COMPLEX) != 0) {
+ if (bagPtr) {
printf("<bag>");
- bagPtr = (const ResTable_map_entry*)ent;
} else {
- valuePtr = (const Res_value*)
- (((const uint8_t*)ent) + esize);
- value.copyFrom_dtoh(*valuePtr);
+ value = ent->value();
printf("t=0x%02x d=0x%08x (s=0x%04x r=0x%02x)",
(int)value.dataType, (int)value.data,
(int)value.size, (int)value.res0);
}
- if ((dtohs(ent->flags)&ResTable_entry::FLAG_PUBLIC) != 0) {
+ if (ent->flags() & ResTable_entry::FLAG_PUBLIC) {
printf(" (PUBLIC)");
}
printf("\n");
if (inclValues) {
- if (valuePtr != NULL) {
+ if (bagPtr == NULL) {
printf(" ");
print_value(typeConfigs->package, value);
- } else if (bagPtr != NULL) {
+ } else {
const int N = dtohl(bagPtr->count);
const uint8_t* baseMapPtr = (const uint8_t*)ent;
size_t mapOffset = esize;
diff --git a/libs/androidfw/ResourceUtils.cpp b/libs/androidfw/ResourceUtils.cpp
index 87fb2c038c9f..ccb61561578f 100644
--- a/libs/androidfw/ResourceUtils.cpp
+++ b/libs/androidfw/ResourceUtils.cpp
@@ -18,7 +18,7 @@
namespace android {
-bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, StringPiece* out_type,
+bool ExtractResourceName(StringPiece str, StringPiece* out_package, StringPiece* out_type,
StringPiece* out_entry) {
*out_package = "";
*out_type = "";
@@ -33,16 +33,16 @@ bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, Strin
while (current != end) {
if (out_type->size() == 0 && *current == '/') {
has_type_separator = true;
- out_type->assign(start, current - start);
+ *out_type = StringPiece(start, current - start);
start = current + 1;
} else if (out_package->size() == 0 && *current == ':') {
has_package_separator = true;
- out_package->assign(start, current - start);
+ *out_package = StringPiece(start, current - start);
start = current + 1;
}
current++;
}
- out_entry->assign(start, end - start);
+ *out_entry = StringPiece(start, end - start);
return !(has_package_separator && out_package->empty()) &&
!(has_type_separator && out_type->empty());
@@ -50,7 +50,7 @@ bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, Strin
base::expected<AssetManager2::ResourceName, NullOrIOError> ToResourceName(
const StringPoolRef& type_string_ref, const StringPoolRef& entry_string_ref,
- const StringPiece& package_name) {
+ StringPiece package_name) {
AssetManager2::ResourceName name{
.package = package_name.data(),
.package_len = package_name.size(),
diff --git a/libs/androidfw/StringPool.cpp b/libs/androidfw/StringPool.cpp
new file mode 100644
index 000000000000..1cb8df311c89
--- /dev/null
+++ b/libs/androidfw/StringPool.cpp
@@ -0,0 +1,507 @@
+/*
+ * 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/BigBuffer.h>
+#include <androidfw/StringPool.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+
+#include "android-base/logging.h"
+#include "androidfw/ResourceTypes.h"
+#include "androidfw/StringPiece.h"
+#include "androidfw/Util.h"
+
+using ::android::StringPiece;
+
+namespace android {
+
+StringPool::Ref::Ref() : entry_(nullptr) {
+}
+
+StringPool::Ref::Ref(const StringPool::Ref& rhs) : entry_(rhs.entry_) {
+ if (entry_ != nullptr) {
+ entry_->ref_++;
+ }
+}
+
+StringPool::Ref::Ref(StringPool::Entry* entry) : entry_(entry) {
+ if (entry_ != nullptr) {
+ entry_->ref_++;
+ }
+}
+
+StringPool::Ref::~Ref() {
+ if (entry_ != nullptr) {
+ entry_->ref_--;
+ }
+}
+
+StringPool::Ref& StringPool::Ref::operator=(const StringPool::Ref& rhs) {
+ if (rhs.entry_ != nullptr) {
+ rhs.entry_->ref_++;
+ }
+
+ if (entry_ != nullptr) {
+ entry_->ref_--;
+ }
+ entry_ = rhs.entry_;
+ return *this;
+}
+
+bool StringPool::Ref::operator==(const Ref& rhs) const {
+ return entry_->value == rhs.entry_->value;
+}
+
+bool StringPool::Ref::operator!=(const Ref& rhs) const {
+ return entry_->value != rhs.entry_->value;
+}
+
+const std::string* StringPool::Ref::operator->() const {
+ return &entry_->value;
+}
+
+const std::string& StringPool::Ref::operator*() const {
+ return entry_->value;
+}
+
+size_t StringPool::Ref::index() const {
+ // Account for the styles, which *always* come first.
+ return entry_->pool_->styles_.size() + entry_->index_;
+}
+
+const StringPool::Context& StringPool::Ref::GetContext() const {
+ return entry_->context;
+}
+
+StringPool::StyleRef::StyleRef() : entry_(nullptr) {
+}
+
+StringPool::StyleRef::StyleRef(const StringPool::StyleRef& rhs) : entry_(rhs.entry_) {
+ if (entry_ != nullptr) {
+ entry_->ref_++;
+ }
+}
+
+StringPool::StyleRef::StyleRef(StringPool::StyleEntry* entry) : entry_(entry) {
+ if (entry_ != nullptr) {
+ entry_->ref_++;
+ }
+}
+
+StringPool::StyleRef::~StyleRef() {
+ if (entry_ != nullptr) {
+ entry_->ref_--;
+ }
+}
+
+StringPool::StyleRef& StringPool::StyleRef::operator=(const StringPool::StyleRef& rhs) {
+ if (rhs.entry_ != nullptr) {
+ rhs.entry_->ref_++;
+ }
+
+ if (entry_ != nullptr) {
+ entry_->ref_--;
+ }
+ entry_ = rhs.entry_;
+ return *this;
+}
+
+bool StringPool::StyleRef::operator==(const StyleRef& rhs) const {
+ if (entry_->value != rhs.entry_->value) {
+ return false;
+ }
+
+ if (entry_->spans.size() != rhs.entry_->spans.size()) {
+ return false;
+ }
+
+ auto rhs_iter = rhs.entry_->spans.begin();
+ for (const Span& span : entry_->spans) {
+ const Span& rhs_span = *rhs_iter;
+ if (span.first_char != rhs_span.first_char || span.last_char != rhs_span.last_char ||
+ span.name != rhs_span.name) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool StringPool::StyleRef::operator!=(const StyleRef& rhs) const {
+ return !operator==(rhs);
+}
+
+const StringPool::StyleEntry* StringPool::StyleRef::operator->() const {
+ return entry_;
+}
+
+const StringPool::StyleEntry& StringPool::StyleRef::operator*() const {
+ return *entry_;
+}
+
+size_t StringPool::StyleRef::index() const {
+ return entry_->index_;
+}
+
+const StringPool::Context& StringPool::StyleRef::GetContext() const {
+ return entry_->context;
+}
+
+StringPool::Ref StringPool::MakeRef(StringPiece str) {
+ return MakeRefImpl(str, Context{}, true);
+}
+
+StringPool::Ref StringPool::MakeRef(StringPiece str, const Context& context) {
+ return MakeRefImpl(str, context, true);
+}
+
+StringPool::Ref StringPool::MakeRefImpl(StringPiece str, const Context& context, bool unique) {
+ if (unique) {
+ auto range = indexed_strings_.equal_range(str);
+ for (auto iter = range.first; iter != range.second; ++iter) {
+ if (context.priority == iter->second->context.priority) {
+ return Ref(iter->second);
+ }
+ }
+ }
+
+ std::unique_ptr<Entry> entry(new Entry());
+ entry->value = std::string(str);
+ entry->context = context;
+ entry->index_ = strings_.size();
+ entry->ref_ = 0;
+ entry->pool_ = this;
+
+ Entry* borrow = entry.get();
+ strings_.emplace_back(std::move(entry));
+ indexed_strings_.insert(std::make_pair(StringPiece(borrow->value), borrow));
+ return Ref(borrow);
+}
+
+StringPool::Ref StringPool::MakeRef(const Ref& ref) {
+ if (ref.entry_->pool_ == this) {
+ return ref;
+ }
+ return MakeRef(ref.entry_->value, ref.entry_->context);
+}
+
+StringPool::StyleRef StringPool::MakeRef(const StyleString& str) {
+ return MakeRef(str, Context{});
+}
+
+StringPool::StyleRef StringPool::MakeRef(const StyleString& str, const Context& context) {
+ std::unique_ptr<StyleEntry> entry(new StyleEntry());
+ entry->value = str.str;
+ entry->context = context;
+ entry->index_ = styles_.size();
+ entry->ref_ = 0;
+ for (const android::Span& span : str.spans) {
+ entry->spans.emplace_back(Span{MakeRef(span.name), span.first_char, span.last_char});
+ }
+
+ StyleEntry* borrow = entry.get();
+ styles_.emplace_back(std::move(entry));
+ return StyleRef(borrow);
+}
+
+StringPool::StyleRef StringPool::MakeRef(const StyleRef& ref) {
+ std::unique_ptr<StyleEntry> entry(new StyleEntry());
+ entry->value = ref.entry_->value;
+ entry->context = ref.entry_->context;
+ entry->index_ = styles_.size();
+ entry->ref_ = 0;
+ for (const Span& span : ref.entry_->spans) {
+ entry->spans.emplace_back(Span{MakeRef(*span.name), span.first_char, span.last_char});
+ }
+
+ StyleEntry* borrow = entry.get();
+ styles_.emplace_back(std::move(entry));
+ return StyleRef(borrow);
+}
+
+void StringPool::ReAssignIndices() {
+ // Assign the style indices.
+ const size_t style_len = styles_.size();
+ for (size_t index = 0; index < style_len; index++) {
+ styles_[index]->index_ = index;
+ }
+
+ // Assign the string indices.
+ const size_t string_len = strings_.size();
+ for (size_t index = 0; index < string_len; index++) {
+ strings_[index]->index_ = index;
+ }
+}
+
+void StringPool::Merge(StringPool&& pool) {
+ // First, change the owning pool for the incoming strings.
+ for (std::unique_ptr<Entry>& entry : pool.strings_) {
+ entry->pool_ = this;
+ }
+
+ // Now move the styles, strings, and indices over.
+ std::move(pool.styles_.begin(), pool.styles_.end(), std::back_inserter(styles_));
+ pool.styles_.clear();
+ std::move(pool.strings_.begin(), pool.strings_.end(), std::back_inserter(strings_));
+ pool.strings_.clear();
+ indexed_strings_.insert(pool.indexed_strings_.begin(), pool.indexed_strings_.end());
+ pool.indexed_strings_.clear();
+
+ ReAssignIndices();
+}
+
+void StringPool::HintWillAdd(size_t string_count, size_t style_count) {
+ strings_.reserve(strings_.size() + string_count);
+ styles_.reserve(styles_.size() + style_count);
+}
+
+void StringPool::Prune() {
+ const auto iter_end = indexed_strings_.end();
+ auto index_iter = indexed_strings_.begin();
+ while (index_iter != iter_end) {
+ if (index_iter->second->ref_ <= 0) {
+ index_iter = indexed_strings_.erase(index_iter);
+ } else {
+ ++index_iter;
+ }
+ }
+
+ auto end_iter2 =
+ std::remove_if(strings_.begin(), strings_.end(),
+ [](const std::unique_ptr<Entry>& entry) -> bool { return entry->ref_ <= 0; });
+ auto end_iter3 = std::remove_if(
+ styles_.begin(), styles_.end(),
+ [](const std::unique_ptr<StyleEntry>& entry) -> bool { return entry->ref_ <= 0; });
+
+ // Remove the entries at the end or else we'll be accessing a deleted string from the StyleEntry.
+ strings_.erase(end_iter2, strings_.end());
+ styles_.erase(end_iter3, styles_.end());
+
+ ReAssignIndices();
+}
+
+template <typename E>
+static void SortEntries(
+ std::vector<std::unique_ptr<E>>& entries,
+ const std::function<int(const StringPool::Context&, const StringPool::Context&)>& cmp) {
+ using UEntry = std::unique_ptr<E>;
+
+ if (cmp != nullptr) {
+ std::sort(entries.begin(), entries.end(), [&cmp](const UEntry& a, const UEntry& b) -> bool {
+ int r = cmp(a->context, b->context);
+ if (r == 0) {
+ r = a->value.compare(b->value);
+ }
+ return r < 0;
+ });
+ } else {
+ std::sort(entries.begin(), entries.end(),
+ [](const UEntry& a, const UEntry& b) -> bool { return a->value < b->value; });
+ }
+}
+
+void StringPool::Sort(const std::function<int(const Context&, const Context&)>& cmp) {
+ SortEntries(styles_, cmp);
+ SortEntries(strings_, cmp);
+ ReAssignIndices();
+}
+
+template <typename T>
+static T* EncodeLength(T* data, size_t length) {
+ static_assert(std::is_integral<T>::value, "wat.");
+
+ constexpr size_t kMask = 1 << ((sizeof(T) * 8) - 1);
+ constexpr size_t kMaxSize = kMask - 1;
+ if (length > kMaxSize) {
+ *data++ = kMask | (kMaxSize & (length >> (sizeof(T) * 8)));
+ }
+ *data++ = length;
+ return data;
+}
+
+/**
+ * Returns the maximum possible string length that can be successfully encoded
+ * using 2 units of the specified T.
+ * EncodeLengthMax<char> -> maximum unit length of 0x7FFF
+ * EncodeLengthMax<char16_t> -> maximum unit length of 0x7FFFFFFF
+ **/
+template <typename T>
+static size_t EncodeLengthMax() {
+ static_assert(std::is_integral<T>::value, "wat.");
+
+ constexpr size_t kMask = 1 << ((sizeof(T) * 8 * 2) - 1);
+ constexpr size_t max = kMask - 1;
+ return max;
+}
+
+/**
+ * Returns the number of units (1 or 2) needed to encode the string length
+ * before writing the string.
+ */
+template <typename T>
+static size_t EncodedLengthUnits(size_t length) {
+ static_assert(std::is_integral<T>::value, "wat.");
+
+ constexpr size_t kMask = 1 << ((sizeof(T) * 8) - 1);
+ constexpr size_t kMaxSize = kMask - 1;
+ return length > kMaxSize ? 2 : 1;
+}
+
+const std::string kStringTooLarge = "STRING_TOO_LARGE";
+
+static bool EncodeString(const std::string& str, const bool utf8, BigBuffer* out,
+ IDiagnostics* diag) {
+ if (utf8) {
+ const std::string& encoded = util::Utf8ToModifiedUtf8(str);
+ const ssize_t utf16_length =
+ utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(encoded.data()), encoded.size());
+ CHECK(utf16_length >= 0);
+
+ // Make sure the lengths to be encoded do not exceed the maximum length that
+ // can be encoded using chars
+ if ((((size_t)encoded.size()) > EncodeLengthMax<char>()) ||
+ (((size_t)utf16_length) > EncodeLengthMax<char>())) {
+ diag->Error(DiagMessage() << "string too large to encode using UTF-8 "
+ << "written instead as '" << kStringTooLarge << "'");
+
+ EncodeString(kStringTooLarge, utf8, out, diag);
+ return false;
+ }
+
+ const size_t total_size = EncodedLengthUnits<char>(utf16_length) +
+ EncodedLengthUnits<char>(encoded.size()) + encoded.size() + 1;
+
+ char* data = out->NextBlock<char>(total_size);
+
+ // First encode the UTF16 string length.
+ data = EncodeLength(data, utf16_length);
+
+ // Now encode the size of the real UTF8 string.
+ data = EncodeLength(data, encoded.size());
+ strncpy(data, encoded.data(), encoded.size());
+
+ } else {
+ const std::u16string encoded = util::Utf8ToUtf16(str);
+ const ssize_t utf16_length = encoded.size();
+
+ // Make sure the length to be encoded does not exceed the maximum possible
+ // length that can be encoded
+ if (((size_t)utf16_length) > EncodeLengthMax<char16_t>()) {
+ diag->Error(DiagMessage() << "string too large to encode using UTF-16 "
+ << "written instead as '" << kStringTooLarge << "'");
+
+ EncodeString(kStringTooLarge, utf8, out, diag);
+ return false;
+ }
+
+ // Total number of 16-bit words to write.
+ const size_t total_size = EncodedLengthUnits<char16_t>(utf16_length) + encoded.size() + 1;
+
+ char16_t* data = out->NextBlock<char16_t>(total_size);
+
+ // Encode the actual UTF16 string length.
+ data = EncodeLength(data, utf16_length);
+ const size_t byte_length = encoded.size() * sizeof(char16_t);
+
+ // NOTE: For some reason, strncpy16(data, entry->value.data(),
+ // entry->value.size()) truncates the string.
+ memcpy(data, encoded.data(), byte_length);
+
+ // The null-terminating character is already here due to the block of data
+ // being set to 0s on allocation.
+ }
+
+ return true;
+}
+
+bool StringPool::Flatten(BigBuffer* out, const StringPool& pool, bool utf8, IDiagnostics* diag) {
+ bool no_error = true;
+ const size_t start_index = out->size();
+ android::ResStringPool_header* header = out->NextBlock<android::ResStringPool_header>();
+ header->header.type = util::HostToDevice16(android::RES_STRING_POOL_TYPE);
+ header->header.headerSize = util::HostToDevice16(sizeof(*header));
+ header->stringCount = util::HostToDevice32(pool.size());
+ header->styleCount = util::HostToDevice32(pool.styles_.size());
+ if (utf8) {
+ header->flags |= android::ResStringPool_header::UTF8_FLAG;
+ }
+
+ uint32_t* indices = pool.size() != 0 ? out->NextBlock<uint32_t>(pool.size()) : nullptr;
+ uint32_t* style_indices =
+ pool.styles_.size() != 0 ? out->NextBlock<uint32_t>(pool.styles_.size()) : nullptr;
+
+ const size_t before_strings_index = out->size();
+ header->stringsStart = before_strings_index - start_index;
+
+ // Styles always come first.
+ for (const std::unique_ptr<StyleEntry>& entry : pool.styles_) {
+ *indices++ = out->size() - before_strings_index;
+ no_error = EncodeString(entry->value, utf8, out, diag) && no_error;
+ }
+
+ for (const std::unique_ptr<Entry>& entry : pool.strings_) {
+ *indices++ = out->size() - before_strings_index;
+ no_error = EncodeString(entry->value, utf8, out, diag) && no_error;
+ }
+
+ out->Align4();
+
+ if (style_indices != nullptr) {
+ const size_t before_styles_index = out->size();
+ header->stylesStart = util::HostToDevice32(before_styles_index - start_index);
+
+ for (const std::unique_ptr<StyleEntry>& entry : pool.styles_) {
+ *style_indices++ = out->size() - before_styles_index;
+
+ if (!entry->spans.empty()) {
+ android::ResStringPool_span* span =
+ out->NextBlock<android::ResStringPool_span>(entry->spans.size());
+ for (const Span& s : entry->spans) {
+ span->name.index = util::HostToDevice32(s.name.index());
+ span->firstChar = util::HostToDevice32(s.first_char);
+ span->lastChar = util::HostToDevice32(s.last_char);
+ span++;
+ }
+ }
+
+ uint32_t* spanEnd = out->NextBlock<uint32_t>();
+ *spanEnd = android::ResStringPool_span::END;
+ }
+
+ // The error checking code in the platform looks for an entire
+ // ResStringPool_span structure worth of 0xFFFFFFFF at the end
+ // of the style block, so fill in the remaining 2 32bit words
+ // with 0xFFFFFFFF.
+ const size_t padding_length =
+ sizeof(android::ResStringPool_span) - sizeof(android::ResStringPool_span::name);
+ uint8_t* padding = out->NextBlock<uint8_t>(padding_length);
+ memset(padding, 0xff, padding_length);
+ out->Align4();
+ }
+ header->header.size = util::HostToDevice32(out->size() - start_index);
+ return no_error;
+}
+
+bool StringPool::FlattenUtf8(BigBuffer* out, const StringPool& pool, IDiagnostics* diag) {
+ return Flatten(out, pool, true, diag);
+}
+
+bool StringPool::FlattenUtf16(BigBuffer* out, const StringPool& pool, IDiagnostics* diag) {
+ return Flatten(out, pool, false, diag);
+}
+
+} // namespace android
diff --git a/libs/androidfw/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp
index 647aa197a94d..70d14a11830e 100644
--- a/libs/androidfw/TypeWrappers.cpp
+++ b/libs/androidfw/TypeWrappers.cpp
@@ -59,7 +59,9 @@ const ResTable_entry* TypeVariant::iterator::operator*() const {
+ dtohl(type->header.size);
const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>(
reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize));
- if (reinterpret_cast<uintptr_t>(entryIndices) + (sizeof(uint32_t) * entryCount) > containerEnd) {
+ const size_t indexSize = type->flags & ResTable_type::FLAG_OFFSET16 ?
+ sizeof(uint16_t) : sizeof(uint32_t);
+ if (reinterpret_cast<uintptr_t>(entryIndices) + (indexSize * entryCount) > containerEnd) {
ALOGE("Type's entry indices extend beyond its boundaries");
return NULL;
}
@@ -73,6 +75,9 @@ const ResTable_entry* TypeVariant::iterator::operator*() const {
}
entryOffset = static_cast<uint32_t>(dtohs(ResTable_sparseTypeEntry{*iter}.offset)) * 4u;
+ } else if (type->flags & ResTable_type::FLAG_OFFSET16) {
+ auto entryIndices16 = reinterpret_cast<const uint16_t*>(entryIndices);
+ entryOffset = offset_from16(entryIndices16[mIndex]);
} else {
entryOffset = dtohl(entryIndices[mIndex]);
}
@@ -91,11 +96,11 @@ const ResTable_entry* TypeVariant::iterator::operator*() const {
if (reinterpret_cast<uintptr_t>(entry) > containerEnd - sizeof(*entry)) {
ALOGE("Entry offset at index %u points outside the Type's boundaries", mIndex);
return NULL;
- } else if (reinterpret_cast<uintptr_t>(entry) + dtohs(entry->size) > containerEnd) {
+ } else if (reinterpret_cast<uintptr_t>(entry) + entry->size() > containerEnd) {
ALOGE("Entry at index %u extends beyond Type's boundaries", mIndex);
return NULL;
- } else if (dtohs(entry->size) < sizeof(*entry)) {
- ALOGE("Entry at index %u is too small (%u)", mIndex, dtohs(entry->size));
+ } else if (entry->size() < sizeof(*entry)) {
+ ALOGE("Entry at index %u is too small (%zu)", mIndex, entry->size());
return NULL;
}
return entry;
diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp
index 59c9d640bb91..be55fe8b4bb6 100644
--- a/libs/androidfw/Util.cpp
+++ b/libs/androidfw/Util.cpp
@@ -42,7 +42,7 @@ void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out
}
}
-std::u16string Utf8ToUtf16(const StringPiece& utf8) {
+std::u16string Utf8ToUtf16(StringPiece utf8) {
ssize_t utf16_length =
utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(utf8.data()), utf8.length());
if (utf16_length <= 0) {
@@ -56,7 +56,7 @@ std::u16string Utf8ToUtf16(const StringPiece& utf8) {
return utf16;
}
-std::string Utf16ToUtf8(const StringPiece16& utf16) {
+std::string Utf16ToUtf8(StringPiece16 utf16) {
ssize_t utf8_length = utf16_to_utf8_length(utf16.data(), utf16.length());
if (utf8_length <= 0) {
return {};
@@ -68,28 +68,151 @@ std::string Utf16ToUtf8(const StringPiece16& utf16) {
return utf8;
}
-static std::vector<std::string> SplitAndTransform(
- const StringPiece& str, char sep, const std::function<char(char)>& f) {
+std::string Utf8ToModifiedUtf8(std::string_view utf8) {
+ // Java uses Modified UTF-8 which only supports the 1, 2, and 3 byte formats of UTF-8. To encode
+ // 4 byte UTF-8 codepoints, Modified UTF-8 allows the use of surrogate pairs in the same format
+ // of CESU-8 surrogate pairs. Calculate the size of the utf8 string with all 4 byte UTF-8
+ // codepoints replaced with 2 3 byte surrogate pairs
+ size_t modified_size = 0;
+ const size_t size = utf8.size();
+ for (size_t i = 0; i < size; i++) {
+ if (((uint8_t)utf8[i] >> 4) == 0xF) {
+ modified_size += 6;
+ i += 3;
+ } else {
+ modified_size++;
+ }
+ }
+
+ // Early out if no 4 byte codepoints are found
+ if (size == modified_size) {
+ return std::string(utf8);
+ }
+
+ std::string output;
+ output.reserve(modified_size);
+ for (size_t i = 0; i < size; i++) {
+ if (((uint8_t)utf8[i] >> 4) == 0xF) {
+ int32_t codepoint = utf32_from_utf8_at(utf8.data(), size, i, nullptr);
+
+ // Calculate the high and low surrogates as UTF-16 would
+ int32_t high = ((codepoint - 0x10000) / 0x400) + 0xD800;
+ int32_t low = ((codepoint - 0x10000) % 0x400) + 0xDC00;
+
+ // Encode each surrogate in UTF-8
+ output.push_back((char)(0xE4 | ((high >> 12) & 0xF)));
+ output.push_back((char)(0x80 | ((high >> 6) & 0x3F)));
+ output.push_back((char)(0x80 | (high & 0x3F)));
+ output.push_back((char)(0xE4 | ((low >> 12) & 0xF)));
+ output.push_back((char)(0x80 | ((low >> 6) & 0x3F)));
+ output.push_back((char)(0x80 | (low & 0x3F)));
+ i += 3;
+ } else {
+ output.push_back(utf8[i]);
+ }
+ }
+
+ return output;
+}
+
+std::string ModifiedUtf8ToUtf8(std::string_view modified_utf8) {
+ // The UTF-8 representation will have a byte length less than or equal to the Modified UTF-8
+ // representation.
+ std::string output;
+ output.reserve(modified_utf8.size());
+
+ size_t index = 0;
+ const size_t modified_size = modified_utf8.size();
+ while (index < modified_size) {
+ size_t next_index;
+ int32_t high_surrogate =
+ utf32_from_utf8_at(modified_utf8.data(), modified_size, index, &next_index);
+ if (high_surrogate < 0) {
+ return {};
+ }
+
+ // Check that the first codepoint is within the high surrogate range
+ if (high_surrogate >= 0xD800 && high_surrogate <= 0xDB7F) {
+ int32_t low_surrogate =
+ utf32_from_utf8_at(modified_utf8.data(), modified_size, next_index, &next_index);
+ if (low_surrogate < 0) {
+ return {};
+ }
+
+ // Check that the second codepoint is within the low surrogate range
+ if (low_surrogate >= 0xDC00 && low_surrogate <= 0xDFFF) {
+ const char32_t codepoint =
+ (char32_t)(((high_surrogate - 0xD800) * 0x400) + (low_surrogate - 0xDC00) + 0x10000);
+
+ // The decoded codepoint should represent a 4 byte, UTF-8 character
+ const size_t utf8_length = (size_t)utf32_to_utf8_length(&codepoint, 1);
+ if (utf8_length != 4) {
+ return {};
+ }
+
+ // Encode the UTF-8 representation of the codepoint into the string
+ const size_t start_index = output.size();
+ output.resize(start_index + utf8_length);
+ char* start = &output[start_index];
+ utf32_to_utf8((char32_t*)&codepoint, 1, start, utf8_length + 1);
+
+ index = next_index;
+ continue;
+ }
+ }
+
+ // Append non-surrogate pairs to the output string
+ for (size_t i = index; i < next_index; i++) {
+ output.push_back(modified_utf8[i]);
+ }
+ index = next_index;
+ }
+ return output;
+}
+
+template <class Func>
+static std::vector<std::string> SplitAndTransform(StringPiece str, char sep, Func&& f) {
std::vector<std::string> parts;
const StringPiece::const_iterator end = std::end(str);
StringPiece::const_iterator start = std::begin(str);
StringPiece::const_iterator current;
do {
current = std::find(start, end, sep);
- parts.emplace_back(str.substr(start, current).to_string());
- if (f) {
- std::string& part = parts.back();
- std::transform(part.begin(), part.end(), part.begin(), f);
- }
+ parts.emplace_back(StringPiece(start, current - start));
+ std::string& part = parts.back();
+ std::transform(part.begin(), part.end(), part.begin(), f);
start = current + 1;
} while (current != end);
return parts;
}
-std::vector<std::string> SplitAndLowercase(const StringPiece& str, char sep) {
- return SplitAndTransform(str, sep, ::tolower);
+std::vector<std::string> SplitAndLowercase(StringPiece str, char sep) {
+ return SplitAndTransform(str, sep, [](char c) { return ::tolower(c); });
}
+std::unique_ptr<uint8_t[]> Copy(const BigBuffer& buffer) {
+ auto data = std::unique_ptr<uint8_t[]>(new uint8_t[buffer.size()]);
+ uint8_t* p = data.get();
+ for (const auto& block : buffer) {
+ memcpy(p, block.buffer.get(), block.size);
+ p += block.size;
+ }
+ return data;
+}
+
+StringPiece16 GetString16(const android::ResStringPool& pool, size_t idx) {
+ if (auto str = pool.stringAt(idx); str.ok()) {
+ return *str;
+ }
+ return StringPiece16();
+}
+
+std::string GetString(const android::ResStringPool& pool, size_t idx) {
+ if (auto str = pool.string8At(idx); str.ok()) {
+ return ModifiedUtf8ToUtf8(*str);
+ }
+ return Utf16ToUtf8(GetString16(pool, idx));
+}
} // namespace util
} // namespace android
diff --git a/libs/androidfw/include/androidfw/ApkParsing.h b/libs/androidfw/include/androidfw/ApkParsing.h
new file mode 100644
index 000000000000..194eaae8e12a
--- /dev/null
+++ b/libs/androidfw/include/androidfw/ApkParsing.h
@@ -0,0 +1,31 @@
+/*
+ * 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 <string_view>
+#include <sys/types.h>
+
+extern const std::string_view APK_LIB;
+extern const size_t APK_LIB_LEN;
+
+namespace android::util {
+// Checks if filename is a valid library path and returns a pointer to the last slash in the path
+// if it is, nullptr otherwise
+const char* ValidLibraryPathLastSlash(const char* filename, bool suppress64Bit, bool debuggable);
+
+// Equivalent to android.os.FileUtils.isFilenameSafe
+bool isFilenameSafe(const char* filename);
+}
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index 1bde792da2ba..f10cb9bf480a 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -17,6 +17,7 @@
#ifndef ANDROIDFW_ASSETMANAGER2_H_
#define ANDROIDFW_ASSETMANAGER2_H_
+#include "android-base/function_ref.h"
#include "android-base/macros.h"
#include <array>
@@ -104,7 +105,7 @@ class AssetManager2 {
// new resource IDs.
bool SetApkAssets(std::vector<const ApkAssets*> apk_assets, bool invalidate_caches = true);
- inline const std::vector<const ApkAssets*> GetApkAssets() const {
+ inline const std::vector<const ApkAssets*>& GetApkAssets() const {
return apk_assets_;
}
@@ -124,8 +125,7 @@ class AssetManager2 {
uint8_t GetAssignedPackageId(const LoadedPackage* package) const;
// Returns a string representation of the overlayable API of a package.
- bool GetOverlayablesToString(const android::StringPiece& package_name,
- std::string* out) const;
+ bool GetOverlayablesToString(android::StringPiece package_name, std::string* out) const;
const std::unordered_map<std::string, std::string>* GetOverlayableMapForPackage(
uint32_t package_id) const;
@@ -321,17 +321,8 @@ class AssetManager2 {
// Creates a new Theme from this AssetManager.
std::unique_ptr<Theme> NewTheme();
- void ForEachPackage(const std::function<bool(const std::string&, uint8_t)> func,
- package_property_t excluded_property_flags = 0U) const {
- for (const PackageGroup& package_group : package_groups_) {
- const auto loaded_package = package_group.packages_.front().loaded_package_;
- if ((loaded_package->GetPropertyFlags() & excluded_property_flags) == 0U
- && !func(loaded_package->GetPackageName(),
- package_group.dynamic_ref_table->mAssignedPackageId)) {
- return;
- }
- }
- }
+ void ForEachPackage(base::function_ref<bool(const std::string&, uint8_t)> func,
+ package_property_t excluded_property_flags = 0U) const;
void DumpToLog() const;
@@ -572,6 +563,7 @@ class Theme {
AssetManager2* asset_manager_ = nullptr;
uint32_t type_spec_flags_ = 0u;
+ std::vector<uint32_t> keys_;
std::vector<Entry> entries_;
};
diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h
index 966ec74c1786..d33c325ff369 100644
--- a/libs/androidfw/include/androidfw/AssetsProvider.h
+++ b/libs/androidfw/include/androidfw/AssetsProvider.h
@@ -20,6 +20,7 @@
#include <memory>
#include <string>
+#include "android-base/function_ref.h"
#include "android-base/macros.h"
#include "android-base/unique_fd.h"
@@ -46,7 +47,7 @@ struct AssetsProvider {
// Iterate over all files and directories provided by the interface. The order of iteration is
// stable.
virtual bool ForEachFile(const std::string& path,
- const std::function<void(const StringPiece&, FileType)>& f) const = 0;
+ base::function_ref<void(StringPiece, FileType)> f) const = 0;
// Retrieves the path to the contents of the AssetsProvider on disk. The path could represent an
// APk, a directory, or some other file type.
@@ -80,8 +81,8 @@ struct AssetsProvider {
// Supplies assets from a zip archive.
struct ZipAssetsProvider : public AssetsProvider {
- static std::unique_ptr<ZipAssetsProvider> Create(std::string path,
- package_property_t flags);
+ static std::unique_ptr<ZipAssetsProvider> Create(std::string path, package_property_t flags,
+ base::unique_fd fd = {});
static std::unique_ptr<ZipAssetsProvider> Create(base::unique_fd fd,
std::string friendly_name,
@@ -90,7 +91,7 @@ struct ZipAssetsProvider : public AssetsProvider {
off64_t len = kUnknownLength);
bool ForEachFile(const std::string& root_path,
- const std::function<void(const StringPiece&, FileType)>& f) const override;
+ base::function_ref<void(StringPiece, FileType)> f) const override;
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
@@ -108,7 +109,12 @@ struct ZipAssetsProvider : public AssetsProvider {
time_t last_mod_time);
struct PathOrDebugName {
- PathOrDebugName(std::string&& value, bool is_path);
+ static PathOrDebugName Path(std::string value) {
+ return {std::move(value), true};
+ }
+ static PathOrDebugName DebugName(std::string value) {
+ return {std::move(value), false};
+ }
// Retrieves the path or null if this class represents a debug name.
WARN_UNUSED const std::string* GetPath() const;
@@ -117,11 +123,16 @@ struct ZipAssetsProvider : public AssetsProvider {
WARN_UNUSED const std::string& GetDebugName() const;
private:
+ PathOrDebugName(std::string value, bool is_path) : value_(std::move(value)), is_path_(is_path) {
+ }
std::string value_;
bool is_path_;
};
- std::unique_ptr<ZipArchive, void (*)(ZipArchive*)> zip_handle_;
+ struct ZipCloser {
+ void operator()(ZipArchive* a) const;
+ };
+ std::unique_ptr<ZipArchive, ZipCloser> zip_handle_;
PathOrDebugName name_;
package_property_t flags_;
time_t last_mod_time_;
@@ -132,7 +143,7 @@ struct DirectoryAssetsProvider : public AssetsProvider {
static std::unique_ptr<DirectoryAssetsProvider> Create(std::string root_dir);
bool ForEachFile(const std::string& path,
- const std::function<void(const StringPiece&, FileType)>& f) const override;
+ base::function_ref<void(StringPiece, FileType)> f) const override;
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
@@ -157,7 +168,7 @@ struct MultiAssetsProvider : public AssetsProvider {
std::unique_ptr<AssetsProvider>&& secondary);
bool ForEachFile(const std::string& root_path,
- const std::function<void(const StringPiece&, FileType)>& f) const override;
+ base::function_ref<void(StringPiece, FileType)> f) const override;
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
@@ -181,10 +192,10 @@ struct MultiAssetsProvider : public AssetsProvider {
// Does not provide any assets.
struct EmptyAssetsProvider : public AssetsProvider {
static std::unique_ptr<AssetsProvider> Create();
- static std::unique_ptr<AssetsProvider> Create(const std::string& path);
+ static std::unique_ptr<AssetsProvider> Create(std::string path);
bool ForEachFile(const std::string& path,
- const std::function<void(const StringPiece&, FileType)>& f) const override;
+ base::function_ref<void(StringPiece, FileType)> f) const override;
WARN_UNUSED std::optional<std::string_view> GetPath() const override;
WARN_UNUSED const std::string& GetDebugName() const override;
diff --git a/libs/androidfw/include/androidfw/BigBuffer.h b/libs/androidfw/include/androidfw/BigBuffer.h
new file mode 100644
index 000000000000..b99a4edf9d88
--- /dev/null
+++ b/libs/androidfw/include/androidfw/BigBuffer.h
@@ -0,0 +1,192 @@
+/*
+ * 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.
+ */
+
+#ifndef _ANDROID_BIG_BUFFER_H
+#define _ANDROID_BIG_BUFFER_H
+
+#include <cstring>
+#include <memory>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include "android-base/logging.h"
+#include "android-base/macros.h"
+
+namespace android {
+
+/**
+ * Inspired by protobuf's ZeroCopyOutputStream, offers blocks of memory
+ * in which to write without knowing the full size of the entire payload.
+ * This is essentially a list of memory blocks. As one fills up, another
+ * block is allocated and appended to the end of the list.
+ */
+class BigBuffer {
+ public:
+ /**
+ * A contiguous block of allocated memory.
+ */
+ struct Block {
+ /**
+ * Pointer to the memory.
+ */
+ std::unique_ptr<uint8_t[]> buffer;
+
+ /**
+ * Size of memory that is currently occupied. The actual
+ * allocation may be larger.
+ */
+ size_t size;
+
+ private:
+ friend class BigBuffer;
+
+ /**
+ * The size of the memory block allocation.
+ */
+ size_t block_size_;
+ };
+
+ typedef std::vector<Block>::const_iterator const_iterator;
+
+ /**
+ * Create a BigBuffer with block allocation sizes
+ * of block_size.
+ */
+ explicit BigBuffer(size_t block_size);
+
+ BigBuffer(BigBuffer&& rhs) noexcept;
+
+ /**
+ * Number of occupied bytes in all the allocated blocks.
+ */
+ size_t size() const;
+
+ /**
+ * Returns a pointer to an array of T, where T is
+ * a POD type. The elements are zero-initialized.
+ */
+ template <typename T>
+ T* NextBlock(size_t count = 1);
+
+ /**
+ * Returns the next block available and puts the size in out_count.
+ * This is useful for grabbing blocks where the size doesn't matter.
+ * Use BackUp() to give back any bytes that were not used.
+ */
+ void* NextBlock(size_t* out_count);
+
+ /**
+ * Backs up count bytes. This must only be called after NextBlock()
+ * and can not be larger than sizeof(T) * count of the last NextBlock()
+ * call.
+ */
+ void BackUp(size_t count);
+
+ /**
+ * Moves the specified BigBuffer into this one. When this method
+ * returns, buffer is empty.
+ */
+ void AppendBuffer(BigBuffer&& buffer);
+
+ /**
+ * Pads the block with 'bytes' bytes of zero values.
+ */
+ void Pad(size_t bytes);
+
+ /**
+ * Pads the block so that it aligns on a 4 byte boundary.
+ */
+ void Align4();
+
+ size_t block_size() const;
+
+ const_iterator begin() const;
+ const_iterator end() const;
+
+ std::string to_string() const;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(BigBuffer);
+
+ /**
+ * Returns a pointer to a buffer of the requested size.
+ * The buffer is zero-initialized.
+ */
+ void* NextBlockImpl(size_t size);
+
+ size_t block_size_;
+ size_t size_;
+ std::vector<Block> blocks_;
+};
+
+inline BigBuffer::BigBuffer(size_t block_size) : block_size_(block_size), size_(0) {
+}
+
+inline BigBuffer::BigBuffer(BigBuffer&& rhs) noexcept
+ : block_size_(rhs.block_size_), size_(rhs.size_), blocks_(std::move(rhs.blocks_)) {
+}
+
+inline size_t BigBuffer::size() const {
+ return size_;
+}
+
+inline size_t BigBuffer::block_size() const {
+ return block_size_;
+}
+
+template <typename T>
+inline T* BigBuffer::NextBlock(size_t count) {
+ static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type");
+ CHECK(count != 0);
+ return reinterpret_cast<T*>(NextBlockImpl(sizeof(T) * count));
+}
+
+inline void BigBuffer::BackUp(size_t count) {
+ Block& block = blocks_.back();
+ block.size -= count;
+ size_ -= count;
+}
+
+inline void BigBuffer::AppendBuffer(BigBuffer&& buffer) {
+ std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_));
+ size_ += buffer.size_;
+ buffer.blocks_.clear();
+ buffer.size_ = 0;
+}
+
+inline void BigBuffer::Pad(size_t bytes) {
+ NextBlock<char>(bytes);
+}
+
+inline void BigBuffer::Align4() {
+ const size_t unaligned = size_ % 4;
+ if (unaligned != 0) {
+ Pad(4 - unaligned);
+ }
+}
+
+inline BigBuffer::const_iterator BigBuffer::begin() const {
+ return blocks_.begin();
+}
+
+inline BigBuffer::const_iterator BigBuffer::end() const {
+ return blocks_.end();
+}
+
+} // namespace android
+
+#endif // _ANDROID_BIG_BUFFER_H
diff --git a/libs/androidfw/include/androidfw/ByteBucketArray.h b/libs/androidfw/include/androidfw/ByteBucketArray.h
index 949c9445b3e8..ca0a9eda9caa 100644
--- a/libs/androidfw/include/androidfw/ByteBucketArray.h
+++ b/libs/androidfw/include/androidfw/ByteBucketArray.h
@@ -17,6 +17,7 @@
#ifndef __BYTE_BUCKET_ARRAY_H
#define __BYTE_BUCKET_ARRAY_H
+#include <algorithm>
#include <cstdint>
#include <cstring>
@@ -31,14 +32,16 @@ namespace android {
template <typename T>
class ByteBucketArray {
public:
- ByteBucketArray() : default_() { memset(buckets_, 0, sizeof(buckets_)); }
+ ByteBucketArray() {
+ memset(buckets_, 0, sizeof(buckets_));
+ }
~ByteBucketArray() {
- for (size_t i = 0; i < kNumBuckets; i++) {
- if (buckets_[i] != NULL) {
- delete[] buckets_[i];
- }
- }
+ deleteBuckets();
+ }
+
+ void clear() {
+ deleteBuckets();
memset(buckets_, 0, sizeof(buckets_));
}
@@ -53,7 +56,7 @@ class ByteBucketArray {
uint8_t bucket_index = static_cast<uint8_t>(index) >> 4;
T* bucket = buckets_[bucket_index];
- if (bucket == NULL) {
+ if (bucket == nullptr) {
return default_;
}
return bucket[0x0f & static_cast<uint8_t>(index)];
@@ -64,9 +67,9 @@ class ByteBucketArray {
<< ") with size=" << size();
uint8_t bucket_index = static_cast<uint8_t>(index) >> 4;
- T* bucket = buckets_[bucket_index];
- if (bucket == NULL) {
- bucket = buckets_[bucket_index] = new T[kBucketSize]();
+ T*& bucket = buckets_[bucket_index];
+ if (bucket == nullptr) {
+ bucket = new T[kBucketSize]();
}
return bucket[0x0f & static_cast<uint8_t>(index)];
}
@@ -80,11 +83,44 @@ class ByteBucketArray {
return true;
}
+ template <class Func>
+ void forEachItem(Func f) {
+ for (size_t i = 0; i < kNumBuckets; i++) {
+ const auto bucket = buckets_[i];
+ if (bucket != nullptr) {
+ for (size_t j = 0; j < kBucketSize; j++) {
+ f((i << 4) | j, bucket[j]);
+ }
+ }
+ }
+ }
+
+ template <class Func>
+ void trimBuckets(Func isEmptyFunc) {
+ for (size_t i = 0; i < kNumBuckets; i++) {
+ const auto bucket = buckets_[i];
+ if (bucket != nullptr) {
+ if (std::all_of(bucket, bucket + kBucketSize, isEmptyFunc)) {
+ delete[] bucket;
+ buckets_[i] = nullptr;
+ }
+ }
+ }
+ }
+
private:
enum { kNumBuckets = 16, kBucketSize = 16 };
+ void deleteBuckets() {
+ for (size_t i = 0; i < kNumBuckets; i++) {
+ if (buckets_[i] != nullptr) {
+ delete[] buckets_[i];
+ }
+ }
+ }
+
T* buckets_[kNumBuckets];
- T default_;
+ static inline const T default_ = {};
};
} // namespace android
diff --git a/libs/androidfw/include/androidfw/ConfigDescription.h b/libs/androidfw/include/androidfw/ConfigDescription.h
index 1701f1b8bc0d..83a80ced855b 100644
--- a/libs/androidfw/include/androidfw/ConfigDescription.h
+++ b/libs/androidfw/include/androidfw/ConfigDescription.h
@@ -53,6 +53,12 @@ enum : ApiVersion {
SDK_O = 26,
SDK_O_MR1 = 27,
SDK_P = 28,
+ SDK_Q = 29,
+ SDK_R = 30,
+ SDK_S = 31,
+ SDK_S_V2 = 32,
+ SDK_TIRAMISU = 33,
+ SDK_U = 34,
};
/*
@@ -72,7 +78,7 @@ struct ConfigDescription : public ResTable_config {
* The resulting configuration has the appropriate sdkVersion defined
* for backwards compatibility.
*/
- static bool Parse(const android::StringPiece& str, ConfigDescription* out = nullptr);
+ static bool Parse(android::StringPiece str, ConfigDescription* out = nullptr);
/**
* If the configuration uses an axis that was added after
diff --git a/libs/androidfw/include/androidfw/CursorWindow.h b/libs/androidfw/include/androidfw/CursorWindow.h
index 6e55a9a0eb8b..9ec026a19c4c 100644
--- a/libs/androidfw/include/androidfw/CursorWindow.h
+++ b/libs/androidfw/include/androidfw/CursorWindow.h
@@ -90,6 +90,9 @@ public:
inline uint32_t getNumRows() { return mNumRows; }
inline uint32_t getNumColumns() { return mNumColumns; }
+ inline size_t sizeOfSlots() const { return mSize - mSlotsOffset; }
+ inline size_t sizeInUse() const { return mAllocOffset + sizeOfSlots(); }
+
status_t clear();
status_t setNumColumns(uint32_t numColumns);
diff --git a/libs/androidfw/include/androidfw/IDiagnostics.h b/libs/androidfw/include/androidfw/IDiagnostics.h
new file mode 100644
index 000000000000..4d5844eaa069
--- /dev/null
+++ b/libs/androidfw/include/androidfw/IDiagnostics.h
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+#ifndef _ANDROID_DIAGNOSTICS_H
+#define _ANDROID_DIAGNOSTICS_H
+
+#include <sstream>
+#include <string>
+
+#include "Source.h"
+#include "android-base/macros.h"
+#include "androidfw/StringPiece.h"
+
+namespace android {
+
+struct DiagMessageActual {
+ Source source;
+ std::string message;
+};
+
+struct DiagMessage {
+ public:
+ DiagMessage() = default;
+
+ explicit DiagMessage(android::StringPiece src) : source_(src) {
+ }
+
+ explicit DiagMessage(const Source& src) : source_(src) {
+ }
+
+ explicit DiagMessage(size_t line) : source_(Source().WithLine(line)) {
+ }
+
+ template <typename T>
+ DiagMessage& operator<<(const T& value) {
+ message_ << value;
+ return *this;
+ }
+
+ DiagMessageActual Build() const {
+ return DiagMessageActual{source_, message_.str()};
+ }
+
+ private:
+ Source source_;
+ std::stringstream message_;
+};
+
+template <>
+inline DiagMessage& DiagMessage::operator<<(const ::std::u16string& value) {
+ message_ << value;
+ return *this;
+}
+
+struct IDiagnostics {
+ virtual ~IDiagnostics() = default;
+
+ enum class Level { Note, Warn, Error };
+
+ virtual void Log(Level level, DiagMessageActual& actualMsg) = 0;
+
+ virtual void Error(const DiagMessage& message) {
+ DiagMessageActual actual = message.Build();
+ Log(Level::Error, actual);
+ }
+
+ virtual void Warn(const DiagMessage& message) {
+ DiagMessageActual actual = message.Build();
+ Log(Level::Warn, actual);
+ }
+
+ virtual void Note(const DiagMessage& message) {
+ DiagMessageActual actual = message.Build();
+ Log(Level::Note, actual);
+ }
+};
+
+class SourcePathDiagnostics : public IDiagnostics {
+ public:
+ SourcePathDiagnostics(const Source& src, IDiagnostics* diag) : source_(src), diag_(diag) {
+ }
+
+ void Log(Level level, DiagMessageActual& actual_msg) override {
+ actual_msg.source.path = source_.path;
+ diag_->Log(level, actual_msg);
+ if (level == Level::Error) {
+ error = true;
+ }
+ }
+
+ bool HadError() {
+ return error;
+ }
+
+ private:
+ Source source_;
+ IDiagnostics* diag_;
+ bool error = false;
+
+ DISALLOW_COPY_AND_ASSIGN(SourcePathDiagnostics);
+};
+
+class NoOpDiagnostics : public IDiagnostics {
+ public:
+ NoOpDiagnostics() = default;
+
+ void Log(Level level, DiagMessageActual& actual_msg) override {
+ (void)level;
+ (void)actual_msg;
+ }
+
+ DISALLOW_COPY_AND_ASSIGN(NoOpDiagnostics);
+};
+
+} // namespace android
+
+#endif /* _ANDROID_DIAGNOSTICS_H */
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index 6804472b3d17..60689128dffb 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -23,6 +23,7 @@
#include <variant>
#include "android-base/macros.h"
+#include "androidfw/ConfigDescription.h"
#include "androidfw/StringPiece.h"
#include "androidfw/ResourceTypes.h"
#include "utils/ByteOrder.h"
@@ -35,6 +36,7 @@ struct Idmap_header;
struct Idmap_data_header;
struct Idmap_target_entry;
struct Idmap_target_entry_inline;
+struct Idmap_target_entry_inline_value;
struct Idmap_overlay_entry;
// A string pool for overlay apk assets. The string pool holds the strings of the overlay resources
@@ -91,7 +93,8 @@ class IdmapResMap {
public:
Result() = default;
explicit Result(uint32_t value) : data_(value) {};
- explicit Result(const Res_value& value) : data_(value) { };
+ explicit Result(std::map<ConfigDescription, Res_value> value) : data_(std::move(value)) {
+ }
// Returns `true` if the resource is overlaid.
explicit operator bool() const {
@@ -107,15 +110,16 @@ class IdmapResMap {
}
bool IsInlineValue() const {
- return std::get_if<Res_value>(&data_) != nullptr;
+ return std::get_if<2>(&data_) != nullptr;
}
- const Res_value& GetInlineValue() const {
- return std::get<Res_value>(data_);
+ const std::map<ConfigDescription, Res_value>& GetInlineValue() const {
+ return std::get<2>(data_);
}
private:
- std::variant<std::monostate, uint32_t, Res_value> data_;
+ std::variant<std::monostate, uint32_t,
+ std::map<ConfigDescription, Res_value> > data_;
};
// Looks up the value that overlays the target resource id.
@@ -129,12 +133,16 @@ class IdmapResMap {
explicit IdmapResMap(const Idmap_data_header* data_header,
const Idmap_target_entry* entries,
const Idmap_target_entry_inline* inline_entries,
+ const Idmap_target_entry_inline_value* inline_entry_values,
+ const ConfigDescription* configs,
uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table);
const Idmap_data_header* data_header_;
const Idmap_target_entry* entries_;
const Idmap_target_entry_inline* inline_entries_;
+ const Idmap_target_entry_inline_value* inline_entry_values_;
+ const ConfigDescription* configurations_;
const uint8_t target_assigned_package_id_;
const OverlayDynamicRefTable* overlay_ref_table_;
@@ -149,8 +157,7 @@ class IdmapResMap {
class LoadedIdmap {
public:
// Loads an IDMAP from a chunk of memory. Returns nullptr if the IDMAP data was malformed.
- static std::unique_ptr<LoadedIdmap> Load(const StringPiece& idmap_path,
- const StringPiece& idmap_data);
+ static std::unique_ptr<LoadedIdmap> Load(StringPiece idmap_path, StringPiece idmap_data);
// Returns the path to the IDMAP.
std::string_view IdmapPath() const {
@@ -170,8 +177,8 @@ class LoadedIdmap {
// Returns a mapping from target resource ids to overlay values.
const IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table) const {
- return IdmapResMap(data_header_, target_entries_, target_inline_entries_,
- target_assigned_package_id, overlay_ref_table);
+ return IdmapResMap(data_header_, target_entries_, target_inline_entries_, inline_entry_values_,
+ configurations_, target_assigned_package_id, overlay_ref_table);
}
// Returns a dynamic reference table for a loaded overlay package.
@@ -191,6 +198,8 @@ class LoadedIdmap {
const Idmap_data_header* data_header_;
const Idmap_target_entry* target_entries_;
const Idmap_target_entry_inline* target_inline_entries_;
+ const Idmap_target_entry_inline_value* inline_entry_values_;
+ const ConfigDescription* configurations_;
const Idmap_overlay_entry* overlay_entries_;
const std::unique_ptr<ResStringPool> string_pool_;
@@ -207,6 +216,8 @@ class LoadedIdmap {
const Idmap_data_header* data_header,
const Idmap_target_entry* target_entries,
const Idmap_target_entry_inline* target_inline_entries,
+ const Idmap_target_entry_inline_value* inline_entry_values_,
+ const ConfigDescription* configs,
const Idmap_overlay_entry* overlay_entries,
std::unique_ptr<ResStringPool>&& string_pool,
std::string_view overlay_apk_path,
diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h
index b3d6a4dcb955..4d12885ad291 100644
--- a/libs/androidfw/include/androidfw/LoadedArsc.h
+++ b/libs/androidfw/include/androidfw/LoadedArsc.h
@@ -99,8 +99,8 @@ enum : package_property_t {
};
struct OverlayableInfo {
- std::string name;
- std::string actor;
+ std::string_view name;
+ std::string_view actor;
uint32_t policy_flags;
};
@@ -166,14 +166,14 @@ class LoadedPackage {
base::expected<uint32_t, NullOrIOError> FindEntryByName(const std::u16string& type_name,
const std::u16string& entry_name) const;
- static base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> GetEntry(
- incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index);
+ static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError>
+ GetEntry(incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index);
static base::expected<uint32_t, NullOrIOError> GetEntryOffset(
incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index);
- static base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> GetEntryFromOffset(
- incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset);
+ static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError>
+ GetEntryFromOffset(incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset);
// Returns the string pool where type names are stored.
const ResStringPool* GetTypeStringPool() const {
@@ -275,7 +275,7 @@ class LoadedPackage {
return overlayable_map_;
}
- const std::map<uint32_t, uint32_t>& GetAliasResourceIdMap() const {
+ const std::vector<std::pair<uint32_t, uint32_t>>& GetAliasResourceIdMap() const {
return alias_id_map_;
}
@@ -295,8 +295,8 @@ class LoadedPackage {
std::unordered_map<uint8_t, TypeSpec> type_specs_;
ByteBucketArray<uint32_t> resource_ids_;
std::vector<DynamicPackageEntry> dynamic_package_map_;
- std::vector<const std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_;
- std::map<uint32_t, uint32_t> alias_id_map_;
+ std::vector<std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_;
+ std::vector<std::pair<uint32_t, uint32_t>> alias_id_map_;
// A map of overlayable name to actor
std::unordered_map<std::string, std::string> overlayable_map_;
@@ -314,6 +314,8 @@ class LoadedArsc {
const LoadedIdmap* loaded_idmap = nullptr,
package_property_t property_flags = 0U);
+ static std::unique_ptr<LoadedArsc> Load(const LoadedIdmap* loaded_idmap = nullptr);
+
// Create an empty LoadedArsc. This is used when an APK has no resources.arsc.
static std::unique_ptr<LoadedArsc> CreateEmpty();
@@ -338,6 +340,7 @@ class LoadedArsc {
LoadedArsc() = default;
bool LoadTable(
const Chunk& chunk, const LoadedIdmap* loaded_idmap, package_property_t property_flags);
+ bool LoadStringPool(const LoadedIdmap* loaded_idmap);
std::unique_ptr<ResStringPool> global_string_pool_ = util::make_unique<ResStringPool>();
std::vector<std::unique_ptr<const LoadedPackage>> packages_;
diff --git a/libs/androidfw/include/androidfw/Locale.h b/libs/androidfw/include/androidfw/Locale.h
index 484ed79a8efd..8934bed098fe 100644
--- a/libs/androidfw/include/androidfw/Locale.h
+++ b/libs/androidfw/include/androidfw/Locale.h
@@ -39,10 +39,10 @@ struct LocaleValue {
/**
* Initialize this LocaleValue from a config string.
*/
- bool InitFromFilterString(const android::StringPiece& config);
+ bool InitFromFilterString(android::StringPiece config);
// Initializes this LocaleValue from a BCP-47 locale tag.
- bool InitFromBcp47Tag(const android::StringPiece& bcp47tag);
+ bool InitFromBcp47Tag(android::StringPiece bcp47tag);
/**
* Initialize this LocaleValue from parts of a vector.
@@ -70,7 +70,7 @@ struct LocaleValue {
inline bool operator>(const LocaleValue& o) const;
private:
- bool InitFromBcp47TagImpl(const android::StringPiece& bcp47tag, const char separator);
+ bool InitFromBcp47TagImpl(android::StringPiece bcp47tag, const char separator);
void set_language(const char* language);
void set_region(const char* language);
diff --git a/libs/androidfw/include/androidfw/PosixUtils.h b/libs/androidfw/include/androidfw/PosixUtils.h
index bb2084740a44..c46e5e6b3fb5 100644
--- a/libs/androidfw/include/androidfw/PosixUtils.h
+++ b/libs/androidfw/include/androidfw/PosixUtils.h
@@ -25,12 +25,18 @@ struct ProcResult {
int status;
std::string stdout_str;
std::string stderr_str;
+
+ explicit ProcResult(int status) : status(status) {}
+ ProcResult(ProcResult&&) noexcept = default;
+ ProcResult& operator=(ProcResult&&) noexcept = default;
+
+ explicit operator bool() const { return status >= 0; }
};
-// Fork, exec and wait for an external process. Return nullptr if the process could not be launched,
-// otherwise a ProcResult containing the external process' exit status and captured stdout and
-// stderr.
-std::unique_ptr<ProcResult> ExecuteBinary(const std::vector<std::string>& argv);
+// Fork, exec and wait for an external process. Returns status < 0 if the process could not be
+// launched, otherwise a ProcResult containing the external process' exit status and captured
+// stdout and stderr.
+ProcResult ExecuteBinary(const std::vector<std::string>& argv);
} // namespace util
} // namespace android
diff --git a/libs/androidfw/include/androidfw/ResourceTimer.h b/libs/androidfw/include/androidfw/ResourceTimer.h
new file mode 100644
index 000000000000..74613519a920
--- /dev/null
+++ b/libs/androidfw/include/androidfw/ResourceTimer.h
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROIDFW_RESOURCETIMER_H_
+#define ANDROIDFW_RESOURCETIMER_H_
+
+#include <time.h>
+#include <atomic>
+#include <vector>
+
+#include <utils/Mutex.h>
+#include <android-base/macros.h>
+#include <androidfw/Util.h>
+
+namespace android {
+
+// ResourceTimer captures the duration of short functions. Durations are accumulated in registers
+// and statistics are pulled back to the Java layer as needed.
+// To monitor an API, first add it to the Counter enumeration. Then, inside the API, create an
+// instance of ResourceTimer with the appropriate enumeral. The corresponding counter will be
+// updated when the ResourceTimer destructor is called, normally at the end of the enclosing block.
+class ResourceTimer {
+ public:
+ enum class Counter {
+ GetResourceValue,
+ RetrieveAttributes,
+
+ LastCounter = RetrieveAttributes,
+ };
+ static const int counterSize = static_cast<int>(Counter::LastCounter) + 1;
+ static char const *toString(Counter);
+
+ // Start a timer for the specified counter.
+ ResourceTimer(Counter);
+ // The block is exiting. If the timer is active, record it.
+ ~ResourceTimer();
+ // This records the elapsed time and disables further recording. Use this if the containing
+ // block includes extra processing that should not be included in the timer. The method is
+ // destructive in that the timer is no longer valid and further calls to record() will be
+ // ignored.
+ void record();
+ // This cancels a timer. Elapsed time will neither be computed nor recorded.
+ void cancel();
+
+ // A single timer contains the count of events and the cumulative time spent handling the
+ // events. It also includes the smallest value seen and 10 largest values seen. Finally, it
+ // includes a histogram of values that approximates a semi-log.
+
+ // The timer can compute percentiles of recorded events. For example, the p50 value is a time
+ // such that 50% of the readings are below the value and 50% are above the value. The
+ // granularity in the readings means that a percentile cannot always be computed. In this case,
+ // the percentile is reported as zero. (The simplest example is when there is a single
+ // reading.) Even if the value can be computed, it will not be exact. Therefore, a percentile
+ // is actually reported as two values: the lowest time at which it might be valid and the
+ // highest time at which it might be valid.
+ struct Timer {
+ static const size_t MaxLargest = 5;
+
+ // The construct zeros all the fields. The destructor releases memory allocated to the
+ // buckets.
+ Timer();
+ ~Timer();
+
+ // The following summary values are set to zero on a reset. All times are in ns.
+
+ // The total number of events recorded.
+ int count;
+ // The total duration of events.
+ int64_t total;
+ // The smallest event duration seen. This is guaranteed to be non-zero if count is greater
+ // than 0.
+ int mintime;
+ // The largest event duration seen.
+ int maxtime;
+
+ // The largest values seen. Element 0 is the largest value seen (and is the same as maxtime,
+ // above). Element 1 is the next largest, and so on. If count is less than MaxLargest,
+ // unused elements will be zero.
+ int largest[MaxLargest];
+
+ // The p50 value is a time such that 50% of the readings are below that time and 50% of the
+ // readings.
+
+ // A single percentile is defined by the lowest value supported by the readings and the
+ // highest value supported by the readings.
+ struct Percentile {
+ // The nominal time (in ns) of the percentile. The true percentile is guaranteed to be less
+ // than or equal to this time.
+ int nominal;
+ // The actual percentile of the nominal time.
+ int nominal_actual;
+ // The time of the next lower bin. The true percentile is guaranteed to be greater than
+ // this time.
+ int floor;
+ // The actual percentile of the floor time.
+ int floor_actual;
+
+ // Fill in a percentile given the cumulative to the bin, the count in the current bin, the
+ // total count, the width of the bin, and the time of the bin.
+ void compute(int cumulative, int current, int count, int width, int time);
+ };
+
+ // The structure that holds the percentiles.
+ struct {
+ Percentile p50;
+ Percentile p90;
+ Percentile p95;
+ Percentile p99;
+ } pvalues;
+
+ // Set all counters to zero.
+ void reset();
+ // Record an event. The input time is in ns.
+ void record(int);
+ // Compute the percentiles. Percentiles are computed on demand, as the computation is too
+ // expensive to be done inline.
+ void compute();
+
+ // Copy one timer to another. If reset is true then the src is reset immediately after the
+ // copy. The reset flag is exploited to make the copy faster. Any data in dst is lost.
+ static void copy(Timer &dst, Timer &src, bool reset);
+
+ private:
+ // Free any buckets.
+ void freeBuckets();
+
+ // Readings are placed in bins, which are orgzanized into decades. The decade 0 covers
+ // [0,100) in steps of 1us. Decade 1 covers [0,1000) in steps of 10us. Decade 2 covers
+ // [0,10000) in steps of 100us. And so on.
+
+ // An event is placed in the first bin that can hold it. This means that events in the range
+ // of [0,100) are placed in the first decade, events in the range of [0,1000) are placed in
+ // the second decade, and so on. This also means that the first 10% of the bins are unused
+ // in each decade after the first.
+
+ // The design provides at least two significant digits across the range of [0,10000).
+
+ static const size_t MaxDimension = 4;
+ static const size_t MaxBuckets = 100;
+
+ // The range of each dimension. The lower value is always zero.
+ static const int range[MaxDimension];
+ // The width of each bin, by dimension
+ static const int width[MaxDimension];
+
+ // A histogram of the values seen. Centuries are allocated as needed, to minimize the memory
+ // impact.
+ int *buckets[MaxDimension];
+ };
+
+ // Fetch one Timer. The function has a short-circuit behavior: if the count is zero then
+ // destination count is set to zero and the function returns false. Otherwise, the destination
+ // is a copy of the source and the function returns true. This behavior lowers the cost of
+ // handling unused timers.
+ static bool copy(int src, Timer &dst, bool reset);
+
+ // Enable the timers. Timers are initially disabled. Enabling timers allocates memory for the
+ // counters. Timers cannot be disabled.
+ static void enable();
+
+ private:
+ // An internal reset method. This does not take a lock.
+ static void reset();
+
+ // Helper method to convert a counter into an enum. Presumably, this will be inlined into zero
+ // actual cpu instructions.
+ static inline std::vector<unsigned int>::size_type toIndex(Counter c) {
+ return static_cast<std::vector<unsigned int>::size_type>(c);
+ }
+
+ // Every counter has an associated lock. The lock has been factored into a separate class to
+ // keep the Timer class a POD.
+ struct GuardedTimer {
+ Mutex lock_;
+ Timer timer_;
+ };
+
+ // Scoped timer
+ struct ScopedTimer {
+ AutoMutex _l;
+ Timer &t;
+ ScopedTimer(GuardedTimer &g) :
+ _l(g.lock_), t(g.timer_) {
+ }
+ Timer *operator->() {
+ return &t;
+ }
+ Timer& operator*() {
+ return t;
+ }
+ };
+
+ // An individual timer is active (or not), is tracking a specific API, and has a start time.
+ // The api and the start time are undefined if the timer is not active.
+ bool active_;
+ Counter api_;
+ struct timespec start_;
+
+ // The global enable flag. This is initially false and may be set true by the java runtime.
+ static std::atomic<bool> enabled_;
+
+ // The global timers. The memory for the timers is not allocated until the timers are enabled.
+ static std::atomic<GuardedTimer *> counter_;
+};
+
+} // namespace android
+
+#endif /* ANDROIDFW_RESOURCETIMER_H_ */
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 3d66244646d5..631bda4f886c 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -21,11 +21,13 @@
#define _LIBS_UTILS_RESOURCE_TYPES_H
#include <android-base/expected.h>
+#include <android-base/unique_fd.h>
#include <androidfw/Asset.h>
#include <androidfw/Errors.h>
#include <androidfw/LocaleData.h>
#include <androidfw/StringPiece.h>
+#include <utils/ByteOrder.h>
#include <utils/Errors.h>
#include <utils/String16.h>
#include <utils/Vector.h>
@@ -45,7 +47,7 @@
namespace android {
constexpr const uint32_t kIdmapMagic = 0x504D4449u;
-constexpr const uint32_t kIdmapCurrentVersion = 0x00000008u;
+constexpr const uint32_t kIdmapCurrentVersion = 0x00000009u;
// This must never change.
constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endian)
@@ -53,10 +55,12 @@ constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endi
// The version should only be changed when a backwards-incompatible change must be made to the
// fabricated overlay file format. Old fabricated overlays must be migrated to the new file format
// to prevent losing fabricated overlay data.
-constexpr const uint32_t kFabricatedOverlayCurrentVersion = 1;
+constexpr const uint32_t kFabricatedOverlayCurrentVersion = 3;
// Returns whether or not the path represents a fabricated overlay.
bool IsFabricatedOverlay(const std::string& path);
+bool IsFabricatedOverlay(const char* path);
+bool IsFabricatedOverlay(android::base::borrowed_fd fd);
/**
* In C++11, char16_t is defined as *at least* 16 bits. We do a lot of
@@ -1067,15 +1071,32 @@ struct ResTable_config
NAVHIDDEN_NO = ACONFIGURATION_NAVHIDDEN_NO << SHIFT_NAVHIDDEN,
NAVHIDDEN_YES = ACONFIGURATION_NAVHIDDEN_YES << SHIFT_NAVHIDDEN,
};
-
- union {
- struct {
- uint8_t keyboard;
- uint8_t navigation;
- uint8_t inputFlags;
- uint8_t inputPad0;
+
+ enum {
+ GRAMMATICAL_GENDER_ANY = ACONFIGURATION_GRAMMATICAL_GENDER_ANY,
+ GRAMMATICAL_GENDER_NEUTER = ACONFIGURATION_GRAMMATICAL_GENDER_NEUTER,
+ GRAMMATICAL_GENDER_FEMININE = ACONFIGURATION_GRAMMATICAL_GENDER_FEMININE,
+ GRAMMATICAL_GENDER_MASCULINE = ACONFIGURATION_GRAMMATICAL_GENDER_MASCULINE,
+ GRAMMATICAL_INFLECTION_GENDER_MASK = 0b11,
+ };
+
+ struct {
+ union {
+ struct {
+ uint8_t keyboard;
+ uint8_t navigation;
+ uint8_t inputFlags;
+ uint8_t inputFieldPad0;
+ };
+ struct {
+ uint32_t input : 24;
+ uint32_t inputFullPad0 : 8;
+ };
+ struct {
+ uint8_t grammaticalInflectionPad0[3];
+ uint8_t grammaticalInflection;
+ };
};
- uint32_t input;
};
enum {
@@ -1098,7 +1119,7 @@ struct ResTable_config
SDKVERSION_ANY = 0
};
- enum {
+ enum {
MINORVERSION_ANY = 0
};
@@ -1259,6 +1280,7 @@ struct ResTable_config
CONFIG_LAYOUTDIR = ACONFIGURATION_LAYOUTDIR,
CONFIG_SCREEN_ROUND = ACONFIGURATION_SCREEN_ROUND,
CONFIG_COLOR_MODE = ACONFIGURATION_COLOR_MODE,
+ CONFIG_GRAMMATICAL_GENDER = ACONFIGURATION_GRAMMATICAL_GENDER,
};
// Compare two configuration, returning CONFIG_* flags set for each value
@@ -1437,6 +1459,10 @@ struct ResTable_type
// Mark any types that use this with a v26 qualifier to prevent runtime issues on older
// platforms.
FLAG_SPARSE = 0x01,
+
+ // If set, the offsets to the entries are encoded in 16-bit, real_offset = offset * 4u
+ // An 16-bit offset of 0xffffu means a NO_ENTRY
+ FLAG_OFFSET16 = 0x02,
};
uint8_t flags;
@@ -1453,6 +1479,11 @@ struct ResTable_type
ResTable_config config;
};
+// Convert a 16-bit offset to 32-bit if FLAG_OFFSET16 is set
+static inline uint32_t offset_from16(uint16_t off16) {
+ return dtohs(off16) == 0xffffu ? ResTable_type::NO_ENTRY : dtohs(off16) * 4u;
+}
+
// The minimum size required to read any version of ResTable_type.
constexpr size_t kResTableTypeMinSize =
sizeof(ResTable_type) - sizeof(ResTable_config) + sizeof(ResTable_config::size);
@@ -1480,6 +1511,8 @@ union ResTable_sparseTypeEntry {
static_assert(sizeof(ResTable_sparseTypeEntry) == sizeof(uint32_t),
"ResTable_sparseTypeEntry must be 4 bytes in size");
+struct ResTable_map_entry;
+
/**
* This is the beginning of information about an entry in the resource
* table. It holds the reference to the name of this entry, and is
@@ -1487,12 +1520,11 @@ static_assert(sizeof(ResTable_sparseTypeEntry) == sizeof(uint32_t),
* * A Res_value structure, if FLAG_COMPLEX is -not- set.
* * An array of ResTable_map structures, if FLAG_COMPLEX is set.
* These supply a set of name/value mappings of data.
+ * * If FLAG_COMPACT is set, this entry is a compact entry for
+ * simple values only
*/
-struct ResTable_entry
+union ResTable_entry
{
- // Number of bytes in this structure.
- uint16_t size;
-
enum {
// If set, this is a complex entry, holding a set of name/value
// mappings. It is followed by an array of ResTable_map structures.
@@ -1504,18 +1536,91 @@ struct ResTable_entry
// resources of the same name/type. This is only useful during
// linking with other resource tables.
FLAG_WEAK = 0x0004,
+ // If set, this is a compact entry with data type and value directly
+ // encoded in the this entry, see ResTable_entry::compact
+ FLAG_COMPACT = 0x0008,
};
- uint16_t flags;
-
- // Reference into ResTable_package::keyStrings identifying this entry.
- struct ResStringPool_ref key;
+
+ struct Full {
+ // Number of bytes in this structure.
+ uint16_t size;
+
+ uint16_t flags;
+
+ // Reference into ResTable_package::keyStrings identifying this entry.
+ struct ResStringPool_ref key;
+ } full;
+
+ /* A compact entry is indicated by FLAG_COMPACT, with flags at the same
+ * offset as a normal entry. This is only for simple data values where
+ *
+ * - size for entry or value can be inferred (both being 8 bytes).
+ * - key index is encoded in 16-bit
+ * - dataType is encoded as the higher 8-bit of flags
+ * - data is encoded directly in this entry
+ */
+ struct Compact {
+ uint16_t key;
+ uint16_t flags;
+ uint32_t data;
+ } compact;
+
+ uint16_t flags() const { return dtohs(full.flags); };
+ bool is_compact() const { return flags() & FLAG_COMPACT; }
+ bool is_complex() const { return flags() & FLAG_COMPLEX; }
+
+ size_t size() const {
+ return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size);
+ }
+
+ uint32_t key() const {
+ return is_compact() ? dtohs(this->compact.key) : dtohl(this->full.key.index);
+ }
+
+ /* Always verify the memory associated with this entry and its value
+ * before calling value() or map_entry()
+ */
+ Res_value value() const {
+ Res_value v;
+ if (is_compact()) {
+ v.size = sizeof(Res_value);
+ v.res0 = 0;
+ v.data = dtohl(this->compact.data);
+ v.dataType = dtohs(compact.flags) >> 8;
+ } else {
+ auto vaddr = reinterpret_cast<const uint8_t*>(this) + dtohs(this->full.size);
+ auto value = reinterpret_cast<const Res_value*>(vaddr);
+ v.size = dtohs(value->size);
+ v.res0 = value->res0;
+ v.data = dtohl(value->data);
+ v.dataType = value->dataType;
+ }
+ return v;
+ }
+
+ const ResTable_map_entry* map_entry() const {
+ return is_complex() && !is_compact() ?
+ reinterpret_cast<const ResTable_map_entry*>(this) : nullptr;
+ }
};
+/* Make sure size of ResTable_entry::Full and ResTable_entry::Compact
+ * be the same as ResTable_entry. This is to allow iteration of entries
+ * to work in either cases.
+ *
+ * The offset of flags must be at the same place for both structures,
+ * to ensure the correct reading to decide whether this is a full entry
+ * or a compact entry.
+ */
+static_assert(sizeof(ResTable_entry) == sizeof(ResTable_entry::Full));
+static_assert(sizeof(ResTable_entry) == sizeof(ResTable_entry::Compact));
+static_assert(offsetof(ResTable_entry, full.flags) == offsetof(ResTable_entry, compact.flags));
+
/**
* Extended form of a ResTable_entry for map entries, defining a parent map
* resource from which to inherit values.
*/
-struct ResTable_map_entry : public ResTable_entry
+struct ResTable_map_entry : public ResTable_entry::Full
{
// Resource identifier of the parent mapping, or 0 if there is none.
// This is always treated as a TYPE_DYNAMIC_REFERENCE.
@@ -1746,6 +1851,28 @@ inline ResTable_overlayable_policy_header::PolicyFlags& operator |=(
return first;
}
+using ResourceId = uint32_t; // 0xpptteeee
+
+using DataType = uint8_t; // Res_value::dataType
+using DataValue = uint32_t; // Res_value::data
+
+struct OverlayManifestInfo {
+ std::string package_name; // NOLINT(misc-non-private-member-variables-in-classes)
+ std::string name; // NOLINT(misc-non-private-member-variables-in-classes)
+ std::string target_package; // NOLINT(misc-non-private-member-variables-in-classes)
+ std::string target_name; // NOLINT(misc-non-private-member-variables-in-classes)
+ ResourceId resource_mapping; // NOLINT(misc-non-private-member-variables-in-classes)
+};
+
+struct FabricatedOverlayEntryParameters {
+ std::string resource_name;
+ DataType data_type;
+ DataValue data_value;
+ std::string data_string_value;
+ std::optional<android::base::borrowed_fd> data_binary_value;
+ std::string configuration;
+};
+
class AssetManager2;
/**
@@ -1776,7 +1903,10 @@ public:
void addMapping(uint8_t buildPackageId, uint8_t runtimePackageId);
- void addAlias(uint32_t stagedId, uint32_t finalizedId);
+ using AliasMap = std::vector<std::pair<uint32_t, uint32_t>>;
+ void setAliases(AliasMap aliases) {
+ mAliasId = std::move(aliases);
+ }
// Returns whether or not the value must be looked up.
bool requiresLookup(const Res_value* value) const;
@@ -1790,12 +1920,12 @@ public:
return mEntries;
}
-private:
- uint8_t mAssignedPackageId;
- uint8_t mLookupTable[256];
- KeyedVector<String16, uint8_t> mEntries;
- bool mAppAsLib;
- std::map<uint32_t, uint32_t> mAliasId;
+ private:
+ uint8_t mLookupTable[256];
+ uint8_t mAssignedPackageId;
+ bool mAppAsLib;
+ KeyedVector<String16, uint8_t> mEntries;
+ AliasMap mAliasId;
};
bool U16StringToInt(const char16_t* s, size_t len, Res_value* outValue);
diff --git a/libs/androidfw/include/androidfw/ResourceUtils.h b/libs/androidfw/include/androidfw/ResourceUtils.h
index bd1c44033b88..2d90a526dfbe 100644
--- a/libs/androidfw/include/androidfw/ResourceUtils.h
+++ b/libs/androidfw/include/androidfw/ResourceUtils.h
@@ -25,14 +25,14 @@ namespace android {
// Extracts the package, type, and name from a string of the format: [[package:]type/]name
// Validation must be performed on each extracted piece.
// Returns false if there was a syntax error.
-bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, StringPiece* out_type,
+bool ExtractResourceName(StringPiece str, StringPiece* out_package, StringPiece* out_type,
StringPiece* out_entry);
// Convert a type_string_ref, entry_string_ref, and package to AssetManager2::ResourceName.
// Useful for getting resource name without re-running AssetManager2::FindEntry searches.
base::expected<AssetManager2::ResourceName, NullOrIOError> ToResourceName(
const StringPoolRef& type_string_ref, const StringPoolRef& entry_string_ref,
- const StringPiece& package_name);
+ StringPiece package_name);
// Formats a ResourceName to "package:type/entry_name".
std::string ToFormattedResourceString(const AssetManager2::ResourceName& resource_name);
diff --git a/libs/androidfw/include/androidfw/Source.h b/libs/androidfw/include/androidfw/Source.h
new file mode 100644
index 000000000000..ddc9ba421101
--- /dev/null
+++ b/libs/androidfw/include/androidfw/Source.h
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+#ifndef _ANDROID_SOURCE_H
+#define _ANDROID_SOURCE_H
+
+#include <optional>
+#include <ostream>
+#include <string>
+
+#include "android-base/stringprintf.h"
+#include "androidfw/StringPiece.h"
+
+namespace android {
+
+// Represents a file on disk. Used for logging and showing errors.
+struct Source {
+ std::string path;
+ std::optional<size_t> line;
+ std::optional<std::string> archive;
+
+ Source() = default;
+
+ inline Source(android::StringPiece path) : path(path) { // NOLINT(implicit)
+ }
+
+ inline Source(android::StringPiece path, android::StringPiece archive)
+ : path(path), archive(archive) {
+ }
+
+ inline Source(android::StringPiece path, size_t line) : path(path), line(line) {
+ }
+
+ inline Source WithLine(size_t line) const {
+ return Source(path, line);
+ }
+
+ std::string to_string() const {
+ std::string s = path;
+ if (archive) {
+ s = ::android::base::StringPrintf("%s@%s", archive.value().c_str(), s.c_str());
+ }
+ if (line) {
+ s = ::android::base::StringPrintf("%s:%zd", s.c_str(), line.value());
+ }
+ return s;
+ }
+};
+
+//
+// Implementations
+//
+
+inline ::std::ostream& operator<<(::std::ostream& out, const Source& source) {
+ return out << source.to_string();
+}
+
+inline bool operator==(const Source& lhs, const Source& rhs) {
+ return lhs.path == rhs.path && lhs.line == rhs.line;
+}
+
+inline bool operator<(const Source& lhs, const Source& rhs) {
+ int cmp = lhs.path.compare(rhs.path);
+ if (cmp < 0) return true;
+ if (cmp > 0) return false;
+ if (lhs.line) {
+ if (rhs.line) {
+ return lhs.line.value() < rhs.line.value();
+ }
+ return false;
+ }
+ return bool(rhs.line);
+}
+
+} // namespace android
+
+#endif // _ANDROID_SOURCE_H
diff --git a/libs/androidfw/include/androidfw/StringPiece.h b/libs/androidfw/include/androidfw/StringPiece.h
index 921877dc4982..f6cc64edfb5a 100644
--- a/libs/androidfw/include/androidfw/StringPiece.h
+++ b/libs/androidfw/include/androidfw/StringPiece.h
@@ -19,307 +19,37 @@
#include <ostream>
#include <string>
+#include <string_view>
-#include "utils/JenkinsHash.h"
#include "utils/Unicode.h"
namespace android {
-// Read only wrapper around basic C strings. Prevents excessive copying.
-// StringPiece does not own the data it is wrapping. The lifetime of the underlying
-// data must outlive this StringPiece.
-//
-// WARNING: When creating from std::basic_string<>, moving the original
-// std::basic_string<> will invalidate the data held in a BasicStringPiece<>.
-// BasicStringPiece<> should only be used transitively.
-//
-// NOTE: When creating an std::pair<StringPiece, T> using std::make_pair(),
-// passing an std::string will first copy the string, then create a StringPiece
-// on the copy, which is then immediately destroyed.
-// Instead, create a StringPiece explicitly:
-//
-// std::string my_string = "foo";
-// std::make_pair<StringPiece, T>(StringPiece(my_string), ...);
-template <typename TChar>
-class BasicStringPiece {
- public:
- using const_iterator = const TChar*;
- using difference_type = size_t;
- using size_type = size_t;
-
- // End of string marker.
- constexpr static const size_t npos = static_cast<size_t>(-1);
-
- BasicStringPiece();
- BasicStringPiece(const BasicStringPiece<TChar>& str);
- BasicStringPiece(const std::basic_string<TChar>& str); // NOLINT(google-explicit-constructor)
- BasicStringPiece(const TChar* str); // NOLINT(google-explicit-constructor)
- BasicStringPiece(const TChar* str, size_t len);
-
- BasicStringPiece<TChar>& operator=(const BasicStringPiece<TChar>& rhs);
- BasicStringPiece<TChar>& assign(const TChar* str, size_t len);
-
- BasicStringPiece<TChar> substr(size_t start, size_t len = npos) const;
- BasicStringPiece<TChar> substr(BasicStringPiece<TChar>::const_iterator begin,
- BasicStringPiece<TChar>::const_iterator end) const;
-
- const TChar* data() const;
- size_t length() const;
- size_t size() const;
- bool empty() const;
- std::basic_string<TChar> to_string() const;
-
- bool contains(const BasicStringPiece<TChar>& rhs) const;
- int compare(const BasicStringPiece<TChar>& rhs) const;
- bool operator<(const BasicStringPiece<TChar>& rhs) const;
- bool operator>(const BasicStringPiece<TChar>& rhs) const;
- bool operator==(const BasicStringPiece<TChar>& rhs) const;
- bool operator!=(const BasicStringPiece<TChar>& rhs) const;
-
- const_iterator begin() const;
- const_iterator end() const;
-
- private:
- const TChar* data_;
- size_t length_;
-};
+template <class T>
+using BasicStringPiece = std::basic_string_view<T>;
using StringPiece = BasicStringPiece<char>;
using StringPiece16 = BasicStringPiece<char16_t>;
-//
-// BasicStringPiece implementation.
-//
-
-template <typename TChar>
-constexpr const size_t BasicStringPiece<TChar>::npos;
-
-template <typename TChar>
-inline BasicStringPiece<TChar>::BasicStringPiece() : data_(nullptr), length_(0) {}
-
-template <typename TChar>
-inline BasicStringPiece<TChar>::BasicStringPiece(const BasicStringPiece<TChar>& str)
- : data_(str.data_), length_(str.length_) {}
-
-template <typename TChar>
-inline BasicStringPiece<TChar>::BasicStringPiece(const std::basic_string<TChar>& str)
- : data_(str.data()), length_(str.length()) {}
-
-template <>
-inline BasicStringPiece<char>::BasicStringPiece(const char* str)
- : data_(str), length_(str != nullptr ? strlen(str) : 0) {}
-
-template <>
-inline BasicStringPiece<char16_t>::BasicStringPiece(const char16_t* str)
- : data_(str), length_(str != nullptr ? strlen16(str) : 0) {}
-
-template <typename TChar>
-inline BasicStringPiece<TChar>::BasicStringPiece(const TChar* str, size_t len)
- : data_(str), length_(len) {}
-
-template <typename TChar>
-inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::operator=(
- const BasicStringPiece<TChar>& rhs) {
- data_ = rhs.data_;
- length_ = rhs.length_;
- return *this;
-}
-
-template <typename TChar>
-inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::assign(const TChar* str, size_t len) {
- data_ = str;
- length_ = len;
- return *this;
-}
-
-template <typename TChar>
-inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr(size_t start, size_t len) const {
- if (len == npos) {
- len = length_ - start;
- }
-
- if (start > length_ || start + len > length_) {
- return BasicStringPiece<TChar>();
- }
- return BasicStringPiece<TChar>(data_ + start, len);
-}
-
-template <typename TChar>
-inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr(
- BasicStringPiece<TChar>::const_iterator begin,
- BasicStringPiece<TChar>::const_iterator end) const {
- return BasicStringPiece<TChar>(begin, end - begin);
-}
-
-template <typename TChar>
-inline const TChar* BasicStringPiece<TChar>::data() const {
- return data_;
-}
-
-template <typename TChar>
-inline size_t BasicStringPiece<TChar>::length() const {
- return length_;
-}
-
-template <typename TChar>
-inline size_t BasicStringPiece<TChar>::size() const {
- return length_;
-}
-
-template <typename TChar>
-inline bool BasicStringPiece<TChar>::empty() const {
- return length_ == 0;
-}
-
-template <typename TChar>
-inline std::basic_string<TChar> BasicStringPiece<TChar>::to_string() const {
- return std::basic_string<TChar>(data_, length_);
-}
-
-template <>
-inline bool BasicStringPiece<char>::contains(const BasicStringPiece<char>& rhs) const {
- if (!data_ || !rhs.data_) {
- return false;
- }
- if (rhs.length_ > length_) {
- return false;
- }
- return strstr(data_, rhs.data_) != nullptr;
-}
-
-template <>
-inline int BasicStringPiece<char>::compare(const BasicStringPiece<char>& rhs) const {
- const char nullStr = '\0';
- const char* b1 = data_ != nullptr ? data_ : &nullStr;
- const char* e1 = b1 + length_;
- const char* b2 = rhs.data_ != nullptr ? rhs.data_ : &nullStr;
- const char* e2 = b2 + rhs.length_;
-
- while (b1 < e1 && b2 < e2) {
- const int d = static_cast<int>(*b1++) - static_cast<int>(*b2++);
- if (d) {
- return d;
- }
- }
- return static_cast<int>(length_ - rhs.length_);
-}
-
-inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char16_t>& str) {
- const ssize_t result_len = utf16_to_utf8_length(str.data(), str.size());
- if (result_len < 0) {
- // Empty string.
- return out;
- }
-
- std::string result;
- result.resize(static_cast<size_t>(result_len));
- utf16_to_utf8(str.data(), str.length(), &*result.begin(), static_cast<size_t>(result_len) + 1);
- return out << result;
-}
-
-template <>
-inline bool BasicStringPiece<char16_t>::contains(const BasicStringPiece<char16_t>& rhs) const {
- if (!data_ || !rhs.data_) {
- return false;
- }
- if (rhs.length_ > length_) {
- return false;
- }
- return strstr16(data_, rhs.data_) != nullptr;
-}
-
-template <>
-inline int BasicStringPiece<char16_t>::compare(const BasicStringPiece<char16_t>& rhs) const {
- const char16_t nullStr = u'\0';
- const char16_t* b1 = data_ != nullptr ? data_ : &nullStr;
- const char16_t* b2 = rhs.data_ != nullptr ? rhs.data_ : &nullStr;
- return strzcmp16(b1, length_, b2, rhs.length_);
-}
-
-template <typename TChar>
-inline bool BasicStringPiece<TChar>::operator<(const BasicStringPiece<TChar>& rhs) const {
- return compare(rhs) < 0;
-}
-
-template <typename TChar>
-inline bool BasicStringPiece<TChar>::operator>(const BasicStringPiece<TChar>& rhs) const {
- return compare(rhs) > 0;
-}
-
-template <typename TChar>
-inline bool BasicStringPiece<TChar>::operator==(const BasicStringPiece<TChar>& rhs) const {
- return compare(rhs) == 0;
-}
-
-template <typename TChar>
-inline bool BasicStringPiece<TChar>::operator!=(const BasicStringPiece<TChar>& rhs) const {
- return compare(rhs) != 0;
-}
-
-template <typename TChar>
-inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::begin() const {
- return data_;
-}
-
-template <typename TChar>
-inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::end() const {
- return data_ + length_;
-}
-
-template <typename TChar>
-inline bool operator==(const TChar* lhs, const BasicStringPiece<TChar>& rhs) {
- return BasicStringPiece<TChar>(lhs) == rhs;
-}
-
-template <typename TChar>
-inline bool operator!=(const TChar* lhs, const BasicStringPiece<TChar>& rhs) {
- return BasicStringPiece<TChar>(lhs) != rhs;
-}
-
-inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char>& str) {
- return out.write(str.data(), str.size());
-}
-
-template <typename TChar>
-inline ::std::basic_string<TChar>& operator+=(::std::basic_string<TChar>& lhs,
- const BasicStringPiece<TChar>& rhs) {
- return lhs.append(rhs.data(), rhs.size());
-}
-
-template <typename TChar>
-inline bool operator==(const ::std::basic_string<TChar>& lhs, const BasicStringPiece<TChar>& rhs) {
- return rhs == lhs;
-}
-
-template <typename TChar>
-inline bool operator!=(const ::std::basic_string<TChar>& lhs, const BasicStringPiece<TChar>& rhs) {
- return rhs != lhs;
-}
-
} // namespace android
-inline ::std::ostream& operator<<(::std::ostream& out, const std::u16string& str) {
+namespace std {
+
+inline ::std::ostream& operator<<(::std::ostream& out, ::std::u16string_view str) {
ssize_t utf8_len = utf16_to_utf8_length(str.data(), str.size());
if (utf8_len < 0) {
- return out << "???";
+ return out; // empty
}
std::string utf8;
utf8.resize(static_cast<size_t>(utf8_len));
- utf16_to_utf8(str.data(), str.size(), &*utf8.begin(), utf8_len + 1);
+ utf16_to_utf8(str.data(), str.size(), utf8.data(), utf8_len + 1);
return out << utf8;
}
-namespace std {
-
-template <typename TChar>
-struct hash<android::BasicStringPiece<TChar>> {
- size_t operator()(const android::BasicStringPiece<TChar>& str) const {
- uint32_t hashCode = android::JenkinsHashMixBytes(
- 0, reinterpret_cast<const uint8_t*>(str.data()), sizeof(TChar) * str.size());
- return static_cast<size_t>(hashCode);
- }
-};
+inline ::std::ostream& operator<<(::std::ostream& out, const ::std::u16string& str) {
+ return out << std::u16string_view(str);
+}
} // namespace std
diff --git a/libs/androidfw/include/androidfw/StringPool.h b/libs/androidfw/include/androidfw/StringPool.h
new file mode 100644
index 000000000000..0190ab57bf23
--- /dev/null
+++ b/libs/androidfw/include/androidfw/StringPool.h
@@ -0,0 +1,228 @@
+/*
+ * 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.
+ */
+
+#ifndef _ANDROID_STRING_POOL_H
+#define _ANDROID_STRING_POOL_H
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "BigBuffer.h"
+#include "IDiagnostics.h"
+#include "android-base/macros.h"
+#include "androidfw/ConfigDescription.h"
+#include "androidfw/StringPiece.h"
+
+namespace android {
+
+struct Span {
+ std::string name;
+ uint32_t first_char;
+ uint32_t last_char;
+
+ bool operator==(const Span& right) const {
+ return name == right.name && first_char == right.first_char && last_char == right.last_char;
+ }
+};
+
+struct StyleString {
+ std::string str;
+ std::vector<Span> spans;
+};
+
+// A StringPool for storing the value of String and StyledString resources.
+// Styles and Strings are stored separately, since the runtime variant of this
+// class -- ResStringPool -- requires that styled strings *always* appear first, since their
+// style data is stored as an array indexed by the same indices as the main string pool array.
+// Otherwise, the style data array would have to be sparse and take up more space.
+class StringPool {
+ public:
+ using size_type = size_t;
+
+ class Context {
+ public:
+ enum : uint32_t {
+ kHighPriority = 1u,
+ kNormalPriority = 0x7fffffffu,
+ kLowPriority = 0xffffffffu,
+ };
+ uint32_t priority = kNormalPriority;
+ android::ConfigDescription config;
+
+ Context() = default;
+ Context(uint32_t p, const android::ConfigDescription& c) : priority(p), config(c) {
+ }
+ explicit Context(uint32_t p) : priority(p) {
+ }
+ explicit Context(const android::ConfigDescription& c) : priority(kNormalPriority), config(c) {
+ }
+ };
+
+ class Entry;
+
+ class Ref {
+ public:
+ Ref();
+ Ref(const Ref&);
+ ~Ref();
+
+ Ref& operator=(const Ref& rhs);
+ bool operator==(const Ref& rhs) const;
+ bool operator!=(const Ref& rhs) const;
+ const std::string* operator->() const;
+ const std::string& operator*() const;
+
+ size_t index() const;
+ const Context& GetContext() const;
+
+ private:
+ friend class StringPool;
+
+ explicit Ref(Entry* entry);
+
+ Entry* entry_;
+ };
+
+ class StyleEntry;
+
+ class StyleRef {
+ public:
+ StyleRef();
+ StyleRef(const StyleRef&);
+ ~StyleRef();
+
+ StyleRef& operator=(const StyleRef& rhs);
+ bool operator==(const StyleRef& rhs) const;
+ bool operator!=(const StyleRef& rhs) const;
+ const StyleEntry* operator->() const;
+ const StyleEntry& operator*() const;
+
+ size_t index() const;
+ const Context& GetContext() const;
+
+ private:
+ friend class StringPool;
+
+ explicit StyleRef(StyleEntry* entry);
+
+ StyleEntry* entry_;
+ };
+
+ class Entry {
+ public:
+ std::string value;
+ Context context;
+
+ private:
+ friend class StringPool;
+ friend class Ref;
+
+ size_t index_;
+ int ref_;
+ const StringPool* pool_;
+ };
+
+ struct Span {
+ Ref name;
+ uint32_t first_char;
+ uint32_t last_char;
+ };
+
+ class StyleEntry {
+ public:
+ std::string value;
+ Context context;
+ std::vector<Span> spans;
+
+ private:
+ friend class StringPool;
+ friend class StyleRef;
+
+ size_t index_;
+ int ref_;
+ };
+
+ static bool FlattenUtf8(BigBuffer* out, const StringPool& pool, IDiagnostics* diag);
+ static bool FlattenUtf16(BigBuffer* out, const StringPool& pool, IDiagnostics* diag);
+
+ StringPool() = default;
+ StringPool(StringPool&&) = default;
+ StringPool& operator=(StringPool&&) = default;
+
+ // Adds a string to the pool, unless it already exists. Returns a reference to the string in the
+ // pool.
+ Ref MakeRef(android::StringPiece str);
+
+ // Adds a string to the pool, unless it already exists, with a context object that can be used
+ // when sorting the string pool. Returns a reference to the string in the pool.
+ Ref MakeRef(android::StringPiece str, const Context& context);
+
+ // Adds a string from another string pool. Returns a reference to the string in the string pool.
+ Ref MakeRef(const Ref& ref);
+
+ // Adds a style to the string pool and returns a reference to it.
+ StyleRef MakeRef(const StyleString& str);
+
+ // Adds a style to the string pool with a context object that can be used when sorting the string
+ // pool. Returns a reference to the style in the string pool.
+ StyleRef MakeRef(const StyleString& str, const Context& context);
+
+ // Adds a style from another string pool. Returns a reference to the style in the string pool.
+ StyleRef MakeRef(const StyleRef& ref);
+
+ // Moves pool into this one without coalescing strings. When this function returns, pool will be
+ // empty.
+ void Merge(StringPool&& pool);
+
+ inline const std::vector<std::unique_ptr<Entry>>& strings() const {
+ return strings_;
+ }
+
+ // Returns the number of strings in the table.
+ inline size_t size() const {
+ return styles_.size() + strings_.size();
+ }
+
+ // Reserves space for strings and styles as an optimization.
+ void HintWillAdd(size_t string_count, size_t style_count);
+
+ // Sorts the strings according to their Context using some comparison function.
+ // Equal Contexts are further sorted by string value, lexicographically.
+ // If no comparison function is provided, values are only sorted lexicographically.
+ void Sort(const std::function<int(const Context&, const Context&)>& cmp = nullptr);
+
+ // Removes any strings that have no references.
+ void Prune();
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(StringPool);
+
+ static bool Flatten(BigBuffer* out, const StringPool& pool, bool utf8, IDiagnostics* diag);
+
+ Ref MakeRefImpl(android::StringPiece str, const Context& context, bool unique);
+ void ReAssignIndices();
+
+ std::vector<std::unique_ptr<Entry>> strings_;
+ std::vector<std::unique_ptr<StyleEntry>> styles_;
+ std::unordered_multimap<android::StringPiece, Entry*> indexed_strings_;
+};
+
+} // namespace android
+
+#endif // _ANDROID_STRING_POOL_H
diff --git a/libs/androidfw/include/androidfw/Util.h b/libs/androidfw/include/androidfw/Util.h
index c59b5b6c51a2..a188abb7ecb5 100644
--- a/libs/androidfw/include/androidfw/Util.h
+++ b/libs/androidfw/include/androidfw/Util.h
@@ -17,20 +17,29 @@
#ifndef UTIL_H_
#define UTIL_H_
+#include <android-base/macros.h>
+#include <util/map_ptr.h>
+
#include <cstdlib>
#include <memory>
-#include <sstream>
#include <vector>
-#include <android-base/macros.h>
-#include <util/map_ptr.h>
-
+#include "androidfw/BigBuffer.h"
+#include "androidfw/ResourceTypes.h"
#include "androidfw/StringPiece.h"
+#include "utils/ByteOrder.h"
#ifdef __ANDROID__
#define ANDROID_LOG(x) LOG(x)
#else
-#define ANDROID_LOG(x) std::stringstream()
+namespace android {
+// No default logging for aapt2, as it's too noisy for a command line dev tool.
+struct NullLogger {
+ template <class T>
+ friend const NullLogger& operator<<(const NullLogger& l, const T&) { return l; }
+};
+}
+#define ANDROID_LOG(x) (android::NullLogger{})
#endif
namespace android {
@@ -46,95 +55,67 @@ std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
}
-// Based on std::unique_ptr, but uses free() to release malloc'ed memory
-// without incurring the size increase of holding on to a custom deleter.
-template <typename T>
-class unique_cptr {
- public:
- using pointer = typename std::add_pointer<T>::type;
-
- constexpr unique_cptr() : ptr_(nullptr) {}
- constexpr explicit unique_cptr(std::nullptr_t) : ptr_(nullptr) {}
- explicit unique_cptr(pointer ptr) : ptr_(ptr) {}
- unique_cptr(unique_cptr&& o) noexcept : ptr_(o.ptr_) { o.ptr_ = nullptr; }
-
- ~unique_cptr() { std::free(reinterpret_cast<void*>(ptr_)); }
-
- inline unique_cptr& operator=(unique_cptr&& o) noexcept {
- if (&o == this) {
- return *this;
- }
-
- std::free(reinterpret_cast<void*>(ptr_));
- ptr_ = o.ptr_;
- o.ptr_ = nullptr;
- return *this;
- }
-
- inline unique_cptr& operator=(std::nullptr_t) {
- std::free(reinterpret_cast<void*>(ptr_));
- ptr_ = nullptr;
- return *this;
- }
-
- pointer release() {
- pointer result = ptr_;
- ptr_ = nullptr;
- return result;
- }
-
- inline pointer get() const { return ptr_; }
-
- void reset(pointer ptr = pointer()) {
- if (ptr == ptr_) {
- return;
- }
-
- pointer old_ptr = ptr_;
- ptr_ = ptr;
- std::free(reinterpret_cast<void*>(old_ptr));
+// Based on std::unique_ptr, but uses free() to release malloc'ed memory.
+struct FreeDeleter {
+ void operator()(void* ptr) const {
+ ::free(ptr);
}
+};
+template <typename T>
+using unique_cptr = std::unique_ptr<T, FreeDeleter>;
- inline void swap(unique_cptr& o) { std::swap(ptr_, o.ptr_); }
-
- inline explicit operator bool() const { return ptr_ != nullptr; }
-
- inline typename std::add_lvalue_reference<T>::type operator*() const { return *ptr_; }
-
- inline pointer operator->() const { return ptr_; }
+void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out);
- inline bool operator==(const unique_cptr& o) const { return ptr_ == o.ptr_; }
+// Converts a UTF-8 string to a UTF-16 string.
+std::u16string Utf8ToUtf16(StringPiece utf8);
- inline bool operator!=(const unique_cptr& o) const { return ptr_ != o.ptr_; }
+// Converts a UTF-16 string to a UTF-8 string.
+std::string Utf16ToUtf8(StringPiece16 utf16);
- inline bool operator==(std::nullptr_t) const { return ptr_ == nullptr; }
+// Converts a UTF8 string into Modified UTF8
+std::string Utf8ToModifiedUtf8(std::string_view utf8);
- inline bool operator!=(std::nullptr_t) const { return ptr_ != nullptr; }
+// Converts a Modified UTF8 string into a UTF8 string
+std::string ModifiedUtf8ToUtf8(std::string_view modified_utf8);
- private:
- DISALLOW_COPY_AND_ASSIGN(unique_cptr);
+inline uint16_t HostToDevice16(uint16_t value) {
+ return htods(value);
+}
- pointer ptr_;
-};
+inline uint32_t HostToDevice32(uint32_t value) {
+ return htodl(value);
+}
-void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out);
+inline uint16_t DeviceToHost16(uint16_t value) {
+ return dtohs(value);
+}
-// Converts a UTF-8 string to a UTF-16 string.
-std::u16string Utf8ToUtf16(const StringPiece& utf8);
+inline uint32_t DeviceToHost32(uint32_t value) {
+ return dtohl(value);
+}
-// Converts a UTF-16 string to a UTF-8 string.
-std::string Utf16ToUtf8(const StringPiece16& utf16);
+std::vector<std::string> SplitAndLowercase(android::StringPiece str, char sep);
-std::vector<std::string> SplitAndLowercase(const android::StringPiece& str, char sep);
+inline bool IsFourByteAligned(const void* data) {
+ return ((uintptr_t)data & 0x3U) == 0;
+}
template <typename T>
inline bool IsFourByteAligned(const incfs::map_ptr<T>& data) {
- return ((size_t)data.unsafe_ptr() & 0x3U) == 0;
+ return IsFourByteAligned(data.unsafe_ptr());
}
-inline bool IsFourByteAligned(const void* data) {
- return ((size_t)data & 0x3U) == 0;
-}
+// Helper method to extract a UTF-16 string from a StringPool. If the string is stored as UTF-8,
+// the conversion to UTF-16 happens within ResStringPool.
+android::StringPiece16 GetString16(const android::ResStringPool& pool, size_t idx);
+
+// Helper method to extract a UTF-8 string from a StringPool. If the string is stored as UTF-16,
+// the conversion from UTF-16 to UTF-8 does not happen in ResStringPool and is done by this method,
+// which maintains no state or cache. This means we must return an std::string copy.
+std::string GetString(const android::ResStringPool& pool, size_t idx);
+
+// Copies the entire BigBuffer into a single buffer.
+std::unique_ptr<uint8_t[]> Copy(const android::BigBuffer& buffer);
} // namespace util
} // namespace android
diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h
index 5a5a0e29125d..d40d24ede769 100644
--- a/libs/androidfw/include/androidfw/misc.h
+++ b/libs/androidfw/include/androidfw/misc.h
@@ -44,6 +44,10 @@ FileType getFileType(const char* fileName);
/* get the file's modification date; returns -1 w/errno set on failure */
time_t getFileModDate(const char* fileName);
+// Check if |path| or |fd| resides on a readonly filesystem.
+bool isReadonlyFilesystem(const char* path);
+bool isReadonlyFilesystem(int fd);
+
}; // namespace android
#endif // _LIBS_ANDROID_FW_MISC_H
diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp
index 52854205207c..d3949e9cf69f 100644
--- a/libs/androidfw/misc.cpp
+++ b/libs/androidfw/misc.cpp
@@ -21,12 +21,17 @@
//
#include <androidfw/misc.h>
-#include <sys/stat.h>
+#include "android-base/logging.h"
+
+#ifdef __linux__
+#include <sys/statvfs.h>
+#include <sys/vfs.h>
+#endif // __linux__
+
#include <cstring>
-#include <errno.h>
#include <cstdio>
-
-using namespace android;
+#include <errno.h>
+#include <sys/stat.h>
namespace android {
@@ -41,8 +46,7 @@ FileType getFileType(const char* fileName)
if (errno == ENOENT || errno == ENOTDIR)
return kFileTypeNonexistent;
else {
- fprintf(stderr, "getFileType got errno=%d on '%s'\n",
- errno, fileName);
+ PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed";
return kFileTypeUnknown;
}
} else {
@@ -82,4 +86,32 @@ time_t getFileModDate(const char* fileName)
return sb.st_mtime;
}
+#ifndef __linux__
+// No need to implement these on the host, the functions only matter on a device.
+bool isReadonlyFilesystem(const char*) {
+ return false;
+}
+bool isReadonlyFilesystem(int) {
+ return false;
+}
+#else // __linux__
+bool isReadonlyFilesystem(const char* path) {
+ struct statfs sfs;
+ if (::statfs(path, &sfs)) {
+ PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed";
+ return false;
+ }
+ return (sfs.f_flags & ST_RDONLY) != 0;
+}
+
+bool isReadonlyFilesystem(int fd) {
+ struct statfs sfs;
+ if (::fstatfs(fd, &sfs)) {
+ PLOG(ERROR) << "isReadonlyFilesystem(): fstatfs(" << fd << ") failed";
+ return false;
+ }
+ return (sfs.f_flags & ST_RDONLY) != 0;
+}
+#endif // __linux__
+
}; // namespace android
diff --git a/libs/androidfw/tests/ApkParsing_test.cpp b/libs/androidfw/tests/ApkParsing_test.cpp
new file mode 100644
index 000000000000..62e88c619e5c
--- /dev/null
+++ b/libs/androidfw/tests/ApkParsing_test.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 "androidfw/ApkParsing.h"
+
+#include "android-base/test_utils.h"
+
+#include "TestHelpers.h"
+
+using ::testing::Eq;
+using ::testing::IsNull;
+using ::testing::NotNull;
+
+namespace android {
+TEST(ApkParsingTest, ValidArm64Path) {
+ const char* path = "lib/arm64-v8a/library.so";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false);
+ ASSERT_THAT(lastSlash, NotNull());
+ ASSERT_THAT(lastSlash, Eq(path + 13));
+}
+
+TEST(ApkParsingTest, ValidArm64PathButSuppressed) {
+ const char* path = "lib/arm64-v8a/library.so";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, true, false);
+ ASSERT_THAT(lastSlash, IsNull());
+}
+
+TEST(ApkParsingTest, ValidArm32Path) {
+ const char* path = "lib/armeabi-v7a/library.so";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false);
+ ASSERT_THAT(lastSlash, NotNull());
+ ASSERT_THAT(lastSlash, Eq(path + 15));
+}
+
+TEST(ApkParsingTest, InvalidMustStartWithLib) {
+ const char* path = "lib/arm64-v8a/random.so";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false);
+ ASSERT_THAT(lastSlash, IsNull());
+}
+
+TEST(ApkParsingTest, InvalidMustEndInSo) {
+ const char* path = "lib/arm64-v8a/library.txt";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false);
+ ASSERT_THAT(lastSlash, IsNull());
+}
+
+TEST(ApkParsingTest, InvalidCharacter) {
+ const char* path = "lib/arm64-v8a/lib#.so";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false);
+ ASSERT_THAT(lastSlash, IsNull());
+}
+
+TEST(ApkParsingTest, InvalidSubdirectories) {
+ const char* path = "lib/arm64-v8a/anything/library.so";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false);
+ ASSERT_THAT(lastSlash, IsNull());
+}
+
+TEST(ApkParsingTest, InvalidFileAtRoot) {
+ const char* path = "lib/library.so";
+ auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false);
+ ASSERT_THAT(lastSlash, IsNull());
+}
+} \ No newline at end of file
diff --git a/libs/androidfw/tests/AttributeResolution_bench.cpp b/libs/androidfw/tests/AttributeResolution_bench.cpp
index ddd8ab820cb1..1c89c61c8f78 100644
--- a/libs/androidfw/tests/AttributeResolution_bench.cpp
+++ b/libs/androidfw/tests/AttributeResolution_bench.cpp
@@ -120,8 +120,8 @@ static void BM_ApplyStyleFramework(benchmark::State& state) {
return;
}
- std::unique_ptr<Asset> asset = assetmanager.OpenNonAsset(layout_path->to_string(), value->cookie,
- Asset::ACCESS_BUFFER);
+ std::unique_ptr<Asset> asset =
+ assetmanager.OpenNonAsset(std::string(*layout_path), value->cookie, Asset::ACCESS_BUFFER);
if (asset == nullptr) {
state.SkipWithError("failed to load layout");
return;
diff --git a/libs/androidfw/tests/BigBuffer_test.cpp b/libs/androidfw/tests/BigBuffer_test.cpp
new file mode 100644
index 000000000000..382d21e20846
--- /dev/null
+++ b/libs/androidfw/tests/BigBuffer_test.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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/BigBuffer.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+using ::testing::NotNull;
+
+namespace android {
+
+TEST(BigBufferTest, AllocateSingleBlock) {
+ BigBuffer buffer(4);
+
+ EXPECT_THAT(buffer.NextBlock<char>(2), NotNull());
+ EXPECT_EQ(2u, buffer.size());
+}
+
+TEST(BigBufferTest, ReturnSameBlockIfNextAllocationFits) {
+ BigBuffer buffer(16);
+
+ char* b1 = buffer.NextBlock<char>(8);
+ EXPECT_THAT(b1, NotNull());
+
+ char* b2 = buffer.NextBlock<char>(4);
+ EXPECT_THAT(b2, NotNull());
+
+ EXPECT_EQ(b1 + 8, b2);
+}
+
+TEST(BigBufferTest, AllocateExactSizeBlockIfLargerThanBlockSize) {
+ BigBuffer buffer(16);
+
+ EXPECT_THAT(buffer.NextBlock<char>(32), NotNull());
+ EXPECT_EQ(32u, buffer.size());
+}
+
+TEST(BigBufferTest, AppendAndMoveBlock) {
+ BigBuffer buffer(16);
+
+ uint32_t* b1 = buffer.NextBlock<uint32_t>();
+ ASSERT_THAT(b1, NotNull());
+ *b1 = 33;
+
+ {
+ BigBuffer buffer2(16);
+ b1 = buffer2.NextBlock<uint32_t>();
+ ASSERT_THAT(b1, NotNull());
+ *b1 = 44;
+
+ buffer.AppendBuffer(std::move(buffer2));
+ EXPECT_EQ(0u, buffer2.size()); // NOLINT
+ EXPECT_EQ(buffer2.begin(), buffer2.end());
+ }
+
+ EXPECT_EQ(2 * sizeof(uint32_t), buffer.size());
+
+ auto b = buffer.begin();
+ ASSERT_NE(b, buffer.end());
+ ASSERT_EQ(sizeof(uint32_t), b->size);
+ ASSERT_EQ(33u, *reinterpret_cast<uint32_t*>(b->buffer.get()));
+ ++b;
+
+ ASSERT_NE(b, buffer.end());
+ ASSERT_EQ(sizeof(uint32_t), b->size);
+ ASSERT_EQ(44u, *reinterpret_cast<uint32_t*>(b->buffer.get()));
+ ++b;
+
+ ASSERT_EQ(b, buffer.end());
+}
+
+TEST(BigBufferTest, PadAndAlignProperly) {
+ BigBuffer buffer(16);
+
+ ASSERT_THAT(buffer.NextBlock<char>(2), NotNull());
+ ASSERT_EQ(2u, buffer.size());
+ buffer.Pad(2);
+ ASSERT_EQ(4u, buffer.size());
+ buffer.Align4();
+ ASSERT_EQ(4u, buffer.size());
+ buffer.Pad(2);
+ ASSERT_EQ(6u, buffer.size());
+ buffer.Align4();
+ ASSERT_EQ(8u, buffer.size());
+}
+
+} // namespace android
diff --git a/libs/androidfw/tests/ByteBucketArray_test.cpp b/libs/androidfw/tests/ByteBucketArray_test.cpp
index 5d464c7dc0f7..9c36cfb212c5 100644
--- a/libs/androidfw/tests/ByteBucketArray_test.cpp
+++ b/libs/androidfw/tests/ByteBucketArray_test.cpp
@@ -52,4 +52,57 @@ TEST(ByteBucketArrayTest, TestSparseInsertion) {
}
}
+TEST(ByteBucketArrayTest, TestForEach) {
+ ByteBucketArray<int> bba;
+ ASSERT_TRUE(bba.set(0, 1));
+ ASSERT_TRUE(bba.set(10, 2));
+ ASSERT_TRUE(bba.set(26, 3));
+ ASSERT_TRUE(bba.set(129, 4));
+ ASSERT_TRUE(bba.set(234, 5));
+
+ int count = 0;
+ bba.forEachItem([&count](auto i, auto val) {
+ ++count;
+ switch (i) {
+ case 0:
+ EXPECT_EQ(1, val);
+ break;
+ case 10:
+ EXPECT_EQ(2, val);
+ break;
+ case 26:
+ EXPECT_EQ(3, val);
+ break;
+ case 129:
+ EXPECT_EQ(4, val);
+ break;
+ case 234:
+ EXPECT_EQ(5, val);
+ break;
+ default:
+ EXPECT_EQ(0, val);
+ break;
+ }
+ });
+ ASSERT_EQ(4 * 16, count);
+}
+
+TEST(ByteBucketArrayTest, TestTrimBuckets) {
+ ByteBucketArray<int> bba;
+ ASSERT_TRUE(bba.set(0, 1));
+ ASSERT_TRUE(bba.set(255, 2));
+ {
+ bba.trimBuckets([](auto val) { return val < 2; });
+ int count = 0;
+ bba.forEachItem([&count](auto, auto) { ++count; });
+ ASSERT_EQ(1 * 16, count);
+ }
+ {
+ bba.trimBuckets([](auto val) { return val < 3; });
+ int count = 0;
+ bba.forEachItem([&count](auto, auto) { ++count; });
+ ASSERT_EQ(0, count);
+ }
+}
+
} // namespace android
diff --git a/libs/androidfw/tests/ConfigDescription_test.cpp b/libs/androidfw/tests/ConfigDescription_test.cpp
index 727035714df3..ec478b0900ef 100644
--- a/libs/androidfw/tests/ConfigDescription_test.cpp
+++ b/libs/androidfw/tests/ConfigDescription_test.cpp
@@ -25,8 +25,8 @@
namespace android {
-static ::testing::AssertionResult TestParse(
- const StringPiece& input, ConfigDescription* config = nullptr) {
+static ::testing::AssertionResult TestParse(StringPiece input,
+ ConfigDescription* config = nullptr) {
if (ConfigDescription::Parse(input, config)) {
return ::testing::AssertionSuccess() << input << " was successfully parsed";
}
@@ -138,7 +138,7 @@ TEST(ConfigDescriptionTest, ParseVrAttribute) {
EXPECT_EQ(std::string("vrheadset-v26"), config.toString().c_str());
}
-static inline ConfigDescription ParseConfigOrDie(const android::StringPiece& str) {
+static inline ConfigDescription ParseConfigOrDie(android::StringPiece str) {
ConfigDescription config;
CHECK(ConfigDescription::Parse(str, &config)) << "invalid configuration: " << str;
return config;
@@ -154,4 +154,22 @@ TEST(ConfigDescriptionTest, RangeQualifiersDoNotConflict) {
EXPECT_FALSE(ParseConfigOrDie("600x400").ConflictsWith(ParseConfigOrDie("300x200")));
}
+TEST(ConfigDescriptionTest, TestGrammaticalGenderQualifier) {
+ ConfigDescription config;
+ EXPECT_TRUE(TestParse("feminine", &config));
+ EXPECT_EQ(android::ResTable_config::GRAMMATICAL_GENDER_FEMININE, config.grammaticalInflection);
+ EXPECT_EQ(SDK_U, config.sdkVersion);
+ EXPECT_EQ(std::string("feminine-v34"), config.toString().string());
+
+ EXPECT_TRUE(TestParse("masculine", &config));
+ EXPECT_EQ(android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE, config.grammaticalInflection);
+ EXPECT_EQ(SDK_U, config.sdkVersion);
+ EXPECT_EQ(std::string("masculine-v34"), config.toString().string());
+
+ EXPECT_TRUE(TestParse("neuter", &config));
+ EXPECT_EQ(android::ResTable_config::GRAMMATICAL_GENDER_NEUTER, config.grammaticalInflection);
+ EXPECT_EQ(SDK_U, config.sdkVersion);
+ EXPECT_EQ(std::string("neuter-v34"), config.toString().string());
+}
+
} // namespace android
diff --git a/libs/androidfw/tests/Config_test.cpp b/libs/androidfw/tests/Config_test.cpp
index 698c36f09301..5477621ce9fd 100644
--- a/libs/androidfw/tests/Config_test.cpp
+++ b/libs/androidfw/tests/Config_test.cpp
@@ -205,4 +205,18 @@ TEST(ConfigTest, ScreenIsHdr) {
EXPECT_EQ(defaultConfig.diff(hdrConfig), ResTable_config::CONFIG_COLOR_MODE);
}
+TEST(ConfigTest, GrammaticalGender) {
+ ResTable_config defaultConfig = {};
+ ResTable_config masculine = {};
+ masculine.grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_MASCULINE;
+
+ EXPECT_EQ(defaultConfig.diff(masculine), ResTable_config::CONFIG_GRAMMATICAL_GENDER);
+
+ ResTable_config feminine = {};
+ feminine.grammaticalInflection = ResTable_config::GRAMMATICAL_GENDER_FEMININE;
+
+ EXPECT_EQ(defaultConfig.diff(feminine), ResTable_config::CONFIG_GRAMMATICAL_GENDER);
+ EXPECT_EQ(masculine.diff(feminine), ResTable_config::CONFIG_GRAMMATICAL_GENDER);
+}
+
} // namespace android.
diff --git a/libs/androidfw/tests/CursorWindow_test.cpp b/libs/androidfw/tests/CursorWindow_test.cpp
index d1cfd03276c2..3cdb31e982b1 100644
--- a/libs/androidfw/tests/CursorWindow_test.cpp
+++ b/libs/androidfw/tests/CursorWindow_test.cpp
@@ -21,9 +21,16 @@
#include "TestHelpers.h"
+// Verify that the memory in use is a multiple of 4 bytes
+#define ASSERT_ALIGNED(w) \
+ ASSERT_EQ(((w)->sizeInUse() & 3), 0); \
+ ASSERT_EQ(((w)->freeSpace() & 3), 0); \
+ ASSERT_EQ(((w)->sizeOfSlots() & 3), 0)
+
#define CREATE_WINDOW_1K \
CursorWindow* w; \
- CursorWindow::create(String8("test"), 1 << 10, &w);
+ CursorWindow::create(String8("test"), 1 << 10, &w); \
+ ASSERT_ALIGNED(w);
#define CREATE_WINDOW_1K_3X3 \
CursorWindow* w; \
@@ -31,11 +38,13 @@
ASSERT_EQ(w->setNumColumns(3), OK); \
ASSERT_EQ(w->allocRow(), OK); \
ASSERT_EQ(w->allocRow(), OK); \
- ASSERT_EQ(w->allocRow(), OK);
+ ASSERT_EQ(w->allocRow(), OK); \
+ ASSERT_ALIGNED(w);
#define CREATE_WINDOW_2M \
CursorWindow* w; \
- CursorWindow::create(String8("test"), 1 << 21, &w);
+ CursorWindow::create(String8("test"), 1 << 21, &w); \
+ ASSERT_ALIGNED(w);
static constexpr const size_t kHalfInlineSize = 8192;
static constexpr const size_t kGiantSize = 1048576;
@@ -49,6 +58,7 @@ TEST(CursorWindowTest, Empty) {
ASSERT_EQ(w->getNumColumns(), 0);
ASSERT_EQ(w->size(), 1 << 10);
ASSERT_EQ(w->freeSpace(), 1 << 10);
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, SetNumColumns) {
@@ -60,6 +70,7 @@ TEST(CursorWindowTest, SetNumColumns) {
ASSERT_NE(w->setNumColumns(5), OK);
ASSERT_NE(w->setNumColumns(3), OK);
ASSERT_EQ(w->getNumColumns(), 4);
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, SetNumColumnsAfterRow) {
@@ -70,6 +81,7 @@ TEST(CursorWindowTest, SetNumColumnsAfterRow) {
ASSERT_EQ(w->allocRow(), OK);
ASSERT_NE(w->setNumColumns(4), OK);
ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, AllocRow) {
@@ -83,14 +95,17 @@ TEST(CursorWindowTest, AllocRow) {
ASSERT_EQ(w->allocRow(), OK);
ASSERT_LT(w->freeSpace(), before);
ASSERT_EQ(w->getNumRows(), 1);
+ ASSERT_ALIGNED(w);
// Verify we can unwind
ASSERT_EQ(w->freeLastRow(), OK);
ASSERT_EQ(w->freeSpace(), before);
ASSERT_EQ(w->getNumRows(), 0);
+ ASSERT_ALIGNED(w);
// Can't unwind when no rows left
ASSERT_NE(w->freeLastRow(), OK);
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, AllocRowBounds) {
@@ -100,6 +115,7 @@ TEST(CursorWindowTest, AllocRowBounds) {
ASSERT_EQ(w->setNumColumns(60), OK);
ASSERT_EQ(w->allocRow(), OK);
ASSERT_NE(w->allocRow(), OK);
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, StoreNull) {
@@ -116,6 +132,7 @@ TEST(CursorWindowTest, StoreNull) {
auto field = w->getFieldSlot(0, 0);
ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL);
}
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, StoreLong) {
@@ -134,6 +151,7 @@ TEST(CursorWindowTest, StoreLong) {
ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
}
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, StoreString) {
@@ -155,6 +173,7 @@ TEST(CursorWindowTest, StoreString) {
auto actual = w->getFieldSlotValueString(field, &size);
ASSERT_EQ(std::string(actual), "cafe");
}
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, StoreBounds) {
@@ -175,6 +194,7 @@ TEST(CursorWindowTest, StoreBounds) {
ASSERT_EQ(w->getFieldSlot(-1, 0), nullptr);
ASSERT_EQ(w->getFieldSlot(0, -1), nullptr);
ASSERT_EQ(w->getFieldSlot(-1, -1), nullptr);
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, Inflate) {
@@ -234,6 +254,7 @@ TEST(CursorWindowTest, Inflate) {
ASSERT_NE(actual, buf);
ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0);
}
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, ParcelEmpty) {
@@ -249,10 +270,12 @@ TEST(CursorWindowTest, ParcelEmpty) {
ASSERT_EQ(w->getNumColumns(), 0);
ASSERT_EQ(w->size(), 0);
ASSERT_EQ(w->freeSpace(), 0);
+ ASSERT_ALIGNED(w);
// We can't mutate the window after parceling
ASSERT_NE(w->setNumColumns(4), OK);
ASSERT_NE(w->allocRow(), OK);
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, ParcelSmall) {
@@ -311,6 +334,7 @@ TEST(CursorWindowTest, ParcelSmall) {
ASSERT_EQ(actualSize, 0);
ASSERT_NE(actual, nullptr);
}
+ ASSERT_ALIGNED(w);
}
TEST(CursorWindowTest, ParcelLarge) {
@@ -364,6 +388,7 @@ TEST(CursorWindowTest, ParcelLarge) {
ASSERT_EQ(actualSize, 0);
ASSERT_NE(actual, nullptr);
}
+ ASSERT_ALIGNED(w);
}
} // android
diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp
index d214e2dfef7b..c90ec197b5ef 100644
--- a/libs/androidfw/tests/LoadedArsc_test.cpp
+++ b/libs/androidfw/tests/LoadedArsc_test.cpp
@@ -71,62 +71,6 @@ TEST(LoadedArscTest, LoadSinglePackageArsc) {
ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
}
-TEST(LoadedArscTest, LoadSparseEntryApp) {
- std::string contents;
- ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
- &contents));
-
- std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
- contents.length());
- ASSERT_THAT(loaded_arsc, NotNull());
-
- const LoadedPackage* package =
- loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9));
- ASSERT_THAT(package, NotNull());
-
- const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1;
- const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9);
-
- const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
- ASSERT_THAT(type_spec, NotNull());
- ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
-
- auto type = type_spec->type_entries[0];
- ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
-}
-
-TEST(LoadedArscTest, FindSparseEntryApp) {
- std::string contents;
- ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
- &contents));
-
- std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
- contents.length());
- ASSERT_THAT(loaded_arsc, NotNull());
-
- const LoadedPackage* package =
- loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_v26));
- ASSERT_THAT(package, NotNull());
-
- const uint8_t type_index = get_type_id(sparse::R::string::only_v26) - 1;
- const uint16_t entry_index = get_entry_id(sparse::R::string::only_v26);
-
- const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
- ASSERT_THAT(type_spec, NotNull());
- ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
-
- // Ensure that AAPT2 sparsely encoded the v26 config as expected.
- auto type_entry = std::find_if(
- type_spec->type_entries.begin(), type_spec->type_entries.end(),
- [](const TypeSpec::TypeEntry& x) { return x.config.sdkVersion == 26; });
- ASSERT_NE(type_entry, type_spec->type_entries.end());
- ASSERT_NE(type_entry->type->flags & ResTable_type::FLAG_SPARSE, 0);
-
- // Test fetching a resource with only sparsely encoded configs by name.
- auto id = package->FindEntryByName(u"string", u"only_v26");
- ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_v26, 0));
-}
-
TEST(LoadedArscTest, LoadSharedLibrary) {
std::string contents;
ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/lib_one/lib_one.apk", "resources.arsc",
@@ -404,4 +348,84 @@ TEST(LoadedArscTest, LoadCustomLoader) {
// sizeof(Res_value) might not be backwards compatible.
// TEST(LoadedArscTest, LoadingShouldBeForwardsAndBackwardsCompatible) { ASSERT_TRUE(false); }
+class LoadedArscParameterizedTest :
+ public testing::TestWithParam<std::string> {
+};
+
+TEST_P(LoadedArscParameterizedTest, LoadSparseEntryApp) {
+ std::string contents;
+ ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+ std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
+ contents.length());
+ ASSERT_THAT(loaded_arsc, NotNull());
+
+ const LoadedPackage* package =
+ loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9));
+ ASSERT_THAT(package, NotNull());
+
+ const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1;
+ const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9);
+
+ const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
+ ASSERT_THAT(type_spec, NotNull());
+ ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
+
+ auto type = type_spec->type_entries[0];
+ ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
+}
+
+TEST_P(LoadedArscParameterizedTest, FindSparseEntryApp) {
+ std::string contents;
+ ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+ std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
+ contents.length());
+ ASSERT_THAT(loaded_arsc, NotNull());
+
+ const LoadedPackage* package =
+ loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_land));
+ ASSERT_THAT(package, NotNull());
+
+ const uint8_t type_index = get_type_id(sparse::R::string::only_land) - 1;
+
+ const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
+ ASSERT_THAT(type_spec, NotNull());
+ ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
+
+ // Type Entry with default orientation is not sparse encoded because the ratio of
+ // populated entries to total entries is above threshold.
+ // Only find out default locale because Soong build system will introduce pseudo
+ // locales for the apk generated at runtime.
+ auto type_entry_default = std::find_if(
+ type_spec->type_entries.begin(), type_spec->type_entries.end(),
+ [] (const TypeSpec::TypeEntry& x) { return x.config.orientation == 0 &&
+ x.config.locale == 0; });
+ ASSERT_NE(type_entry_default, type_spec->type_entries.end());
+ ASSERT_EQ(type_entry_default->type->flags & ResTable_type::FLAG_SPARSE, 0);
+
+ // Type Entry with land orientation is sparse encoded as expected.
+ // Only find out default locale because Soong build system will introduce pseudo
+ // locales for the apk generated at runtime.
+ auto type_entry_land = std::find_if(
+ type_spec->type_entries.begin(), type_spec->type_entries.end(),
+ [](const TypeSpec::TypeEntry& x) { return x.config.orientation ==
+ ResTable_config::ORIENTATION_LAND &&
+ x.config.locale == 0; });
+ ASSERT_NE(type_entry_land, type_spec->type_entries.end());
+ ASSERT_NE(type_entry_land->type->flags & ResTable_type::FLAG_SPARSE, 0);
+
+ // Test fetching a resource with only sparsely encoded configs by name.
+ auto id = package->FindEntryByName(u"string", u"only_land");
+ ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_land, 0));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ FrameWorkResourcesLoadedArscTests,
+ LoadedArscParameterizedTest,
+ ::testing::Values(
+ base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk",
+ base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk"
+ ));
+
} // namespace android
diff --git a/libs/androidfw/tests/PosixUtils_test.cpp b/libs/androidfw/tests/PosixUtils_test.cpp
index 8c49350796ec..097e6b0bba65 100644
--- a/libs/androidfw/tests/PosixUtils_test.cpp
+++ b/libs/androidfw/tests/PosixUtils_test.cpp
@@ -28,27 +28,27 @@ namespace util {
TEST(PosixUtilsTest, AbsolutePathToBinary) {
const auto result = ExecuteBinary({"/bin/date", "--help"});
- ASSERT_THAT(result, NotNull());
- ASSERT_EQ(result->status, 0);
- ASSERT_GE(result->stdout_str.find("usage: date "), 0);
+ ASSERT_TRUE((bool)result);
+ ASSERT_EQ(result.status, 0);
+ ASSERT_GE(result.stdout_str.find("usage: date "), 0);
}
TEST(PosixUtilsTest, RelativePathToBinary) {
const auto result = ExecuteBinary({"date", "--help"});
- ASSERT_THAT(result, NotNull());
- ASSERT_EQ(result->status, 0);
- ASSERT_GE(result->stdout_str.find("usage: date "), 0);
+ ASSERT_TRUE((bool)result);
+ ASSERT_EQ(result.status, 0);
+ ASSERT_GE(result.stdout_str.find("usage: date "), 0);
}
TEST(PosixUtilsTest, BadParameters) {
const auto result = ExecuteBinary({"/bin/date", "--this-parameter-is-not-supported"});
- ASSERT_THAT(result, NotNull());
- ASSERT_NE(result->status, 0);
+ ASSERT_TRUE((bool)result);
+ ASSERT_GT(result.status, 0);
}
TEST(PosixUtilsTest, NoSuchBinary) {
const auto result = ExecuteBinary({"/this/binary/does/not/exist"});
- ASSERT_THAT(result, IsNull());
+ ASSERT_FALSE((bool)result);
}
} // android
diff --git a/libs/androidfw/tests/ResTable_test.cpp b/libs/androidfw/tests/ResTable_test.cpp
index dc6dea9fe69c..945981b981a0 100644
--- a/libs/androidfw/tests/ResTable_test.cpp
+++ b/libs/androidfw/tests/ResTable_test.cpp
@@ -15,6 +15,7 @@
*/
#include "androidfw/ResourceTypes.h"
+#include "android-base/file.h"
#include <codecvt>
#include <locale>
@@ -41,34 +42,6 @@ TEST(ResTableTest, ShouldLoadSuccessfully) {
ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
}
-TEST(ResTableTest, ShouldLoadSparseEntriesSuccessfully) {
- std::string contents;
- ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
- &contents));
-
- ResTable table;
- ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
-
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- table.setParameters(&config);
-
- String16 name(u"com.android.sparse:integer/foo_9");
- uint32_t flags;
- uint32_t resid =
- table.identifierForName(name.c_str(), name.size(), nullptr, 0, nullptr, 0, &flags);
- ASSERT_NE(0u, resid);
-
- Res_value val;
- ResTable_config selected_config;
- ASSERT_GE(
- table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config),
- 0);
- EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType);
- EXPECT_EQ(900u, val.data);
-}
-
TEST(ResTableTest, SimpleTypeIsRetrievedCorrectly) {
std::string contents;
ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/basic/basic.apk",
@@ -476,4 +449,43 @@ TEST(ResTableTest, TruncatedEncodeLength) {
ASSERT_FALSE(invalid_pool->stringAt(invalid_val.data).has_value());
}
+class ResTableParameterizedTest :
+ public testing::TestWithParam<std::string> {
+};
+
+TEST_P(ResTableParameterizedTest, ShouldLoadSparseEntriesSuccessfully) {
+ std::string contents;
+ ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+ ResTable table;
+ ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
+
+ ResTable_config config;
+ memset(&config, 0, sizeof(config));
+ config.orientation = ResTable_config::ORIENTATION_LAND;
+ table.setParameters(&config);
+
+ String16 name(u"com.android.sparse:integer/foo_9");
+ uint32_t flags;
+ uint32_t resid =
+ table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags);
+ ASSERT_NE(0u, resid);
+
+ Res_value val;
+ ResTable_config selected_config;
+ ASSERT_GE(
+ table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config),
+ 0);
+ EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType);
+ EXPECT_EQ(900u, val.data);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ FrameWorkResourcesResTableTests,
+ ResTableParameterizedTest,
+ ::testing::Values(
+ base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk",
+ base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk"
+ ));
+
} // namespace android
diff --git a/libs/androidfw/tests/ResourceTimer_test.cpp b/libs/androidfw/tests/ResourceTimer_test.cpp
new file mode 100644
index 000000000000..4a1e9735de7a
--- /dev/null
+++ b/libs/androidfw/tests/ResourceTimer_test.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+
+
+#include <android-base/file.h>
+#include <android-base/test_utils.h>
+#include <androidfw/Util.h>
+
+#include "TestHelpers.h"
+
+#include <androidfw/ResourceTimer.h>
+
+namespace android {
+
+namespace {
+
+// Create a reading in us. This is a convenience function to avoid multiplying by 1000
+// everywhere.
+unsigned int US(int us) {
+ return us * 1000;
+}
+
+}
+
+TEST(ResourceTimerTest, TimerBasic) {
+ ResourceTimer::Timer timer;
+ ASSERT_THAT(timer.count, 0);
+ ASSERT_THAT(timer.total, 0);
+
+ for (int i = 1; i <= 100; i++) {
+ timer.record(US(i));
+ }
+ ASSERT_THAT(timer.count, 100);
+ ASSERT_THAT(timer.total, US((101 * 100)/2));
+ ASSERT_THAT(timer.mintime, US(1));
+ ASSERT_THAT(timer.maxtime, US(100));
+ ASSERT_THAT(timer.pvalues.p50.floor, 0);
+ ASSERT_THAT(timer.pvalues.p50.nominal, 0);
+ ASSERT_THAT(timer.largest[0], US(100));
+ ASSERT_THAT(timer.largest[1], US(99));
+ ASSERT_THAT(timer.largest[2], US(98));
+ ASSERT_THAT(timer.largest[3], US(97));
+ ASSERT_THAT(timer.largest[4], US(96));
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(49));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(50));
+ ASSERT_THAT(timer.pvalues.p90.floor, US(89));
+ ASSERT_THAT(timer.pvalues.p90.nominal, US(90));
+ ASSERT_THAT(timer.pvalues.p95.floor, US(94));
+ ASSERT_THAT(timer.pvalues.p95.nominal, US(95));
+ ASSERT_THAT(timer.pvalues.p99.floor, US(98));
+ ASSERT_THAT(timer.pvalues.p99.nominal, US(99));
+
+ // Test reset functionality. All values should be zero after the reset. Computing pvalues
+ // after the result should also yield zeros.
+ timer.reset();
+ ASSERT_THAT(timer.count, 0);
+ ASSERT_THAT(timer.total, 0);
+ ASSERT_THAT(timer.mintime, US(0));
+ ASSERT_THAT(timer.maxtime, US(0));
+ ASSERT_THAT(timer.pvalues.p50.floor, US(0));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(0));
+ ASSERT_THAT(timer.largest[0], US(0));
+ ASSERT_THAT(timer.largest[1], US(0));
+ ASSERT_THAT(timer.largest[2], US(0));
+ ASSERT_THAT(timer.largest[3], US(0));
+ ASSERT_THAT(timer.largest[4], US(0));
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(0));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(0));
+ ASSERT_THAT(timer.pvalues.p90.floor, US(0));
+ ASSERT_THAT(timer.pvalues.p90.nominal, US(0));
+ ASSERT_THAT(timer.pvalues.p95.floor, US(0));
+ ASSERT_THAT(timer.pvalues.p95.nominal, US(0));
+ ASSERT_THAT(timer.pvalues.p99.floor, US(0));
+ ASSERT_THAT(timer.pvalues.p99.nominal, US(0));
+
+ // Test again, adding elements in reverse.
+ for (int i = 100; i >= 1; i--) {
+ timer.record(US(i));
+ }
+ ASSERT_THAT(timer.count, 100);
+ ASSERT_THAT(timer.total, US((101 * 100)/2));
+ ASSERT_THAT(timer.mintime, US(1));
+ ASSERT_THAT(timer.maxtime, US(100));
+ ASSERT_THAT(timer.pvalues.p50.floor, 0);
+ ASSERT_THAT(timer.pvalues.p50.nominal, 0);
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(49));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(50));
+ ASSERT_THAT(timer.pvalues.p90.floor, US(89));
+ ASSERT_THAT(timer.pvalues.p90.nominal, US(90));
+ ASSERT_THAT(timer.pvalues.p95.floor, US(94));
+ ASSERT_THAT(timer.pvalues.p95.nominal, US(95));
+ ASSERT_THAT(timer.pvalues.p99.floor, US(98));
+ ASSERT_THAT(timer.pvalues.p99.nominal, US(99));
+ ASSERT_THAT(timer.largest[0], US(100));
+ ASSERT_THAT(timer.largest[1], US(99));
+ ASSERT_THAT(timer.largest[2], US(98));
+ ASSERT_THAT(timer.largest[3], US(97));
+ ASSERT_THAT(timer.largest[4], US(96));
+}
+
+TEST(ResourceTimerTest, TimerLimit) {
+ ResourceTimer::Timer timer;
+
+ // Event truncation means that a time of 1050us will be stored in the 1000us
+ // bucket. Since there is a single event, all p-values lie in the same range.
+ timer.record(US(1050));
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(900));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(1000));
+ ASSERT_THAT(timer.pvalues.p90.floor, US(900));
+ ASSERT_THAT(timer.pvalues.p90.nominal, US(1000));
+ ASSERT_THAT(timer.pvalues.p95.floor, US(900));
+ ASSERT_THAT(timer.pvalues.p95.nominal, US(1000));
+ ASSERT_THAT(timer.pvalues.p99.floor, US(900));
+ ASSERT_THAT(timer.pvalues.p99.nominal, US(1000));
+}
+
+TEST(ResourceTimerTest, TimerCopy) {
+ ResourceTimer::Timer source;
+ for (int i = 1; i <= 100; i++) {
+ source.record(US(i));
+ }
+ ResourceTimer::Timer timer;
+ ResourceTimer::Timer::copy(timer, source, true);
+ ASSERT_THAT(source.count, 0);
+ ASSERT_THAT(source.total, 0);
+ // compute() is not normally be called on a reset timer, but it should work and it should return
+ // all zeros.
+ source.compute();
+ ASSERT_THAT(source.pvalues.p50.floor, US(0));
+ ASSERT_THAT(source.pvalues.p50.nominal, US(0));
+ ASSERT_THAT(source.pvalues.p90.floor, US(0));
+ ASSERT_THAT(source.pvalues.p90.nominal, US(0));
+ ASSERT_THAT(source.pvalues.p95.floor, US(0));
+ ASSERT_THAT(source.pvalues.p95.nominal, US(0));
+ ASSERT_THAT(source.pvalues.p99.floor, US(0));
+ ASSERT_THAT(source.pvalues.p99.nominal, US(0));
+ ASSERT_THAT(source.largest[0], US(0));
+ ASSERT_THAT(source.largest[1], US(0));
+ ASSERT_THAT(source.largest[2], US(0));
+ ASSERT_THAT(source.largest[3], US(0));
+ ASSERT_THAT(source.largest[4], US(0));
+
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(49));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(50));
+ ASSERT_THAT(timer.pvalues.p90.floor, US(89));
+ ASSERT_THAT(timer.pvalues.p90.nominal, US(90));
+ ASSERT_THAT(timer.pvalues.p95.floor, US(94));
+ ASSERT_THAT(timer.pvalues.p95.nominal, US(95));
+ ASSERT_THAT(timer.pvalues.p99.floor, US(98));
+ ASSERT_THAT(timer.pvalues.p99.nominal, US(99));
+ ASSERT_THAT(timer.largest[0], US(100));
+ ASSERT_THAT(timer.largest[1], US(99));
+ ASSERT_THAT(timer.largest[2], US(98));
+ ASSERT_THAT(timer.largest[3], US(97));
+ ASSERT_THAT(timer.largest[4], US(96));
+
+ // Call compute a second time. The values must be the same.
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(49));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(50));
+ ASSERT_THAT(timer.pvalues.p90.floor, US(89));
+ ASSERT_THAT(timer.pvalues.p90.nominal, US(90));
+ ASSERT_THAT(timer.pvalues.p95.floor, US(94));
+ ASSERT_THAT(timer.pvalues.p95.nominal, US(95));
+ ASSERT_THAT(timer.pvalues.p99.floor, US(98));
+ ASSERT_THAT(timer.pvalues.p99.nominal, US(99));
+ ASSERT_THAT(timer.largest[0], US(100));
+ ASSERT_THAT(timer.largest[1], US(99));
+ ASSERT_THAT(timer.largest[2], US(98));
+ ASSERT_THAT(timer.largest[3], US(97));
+ ASSERT_THAT(timer.largest[4], US(96));
+
+ // Modify the source. If timer and source share histogram arrays, this will introduce an
+ // error.
+ for (int i = 1; i <= 100; i++) {
+ source.record(US(i));
+ }
+ // Call compute a third time. The values must be the same.
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(49));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(50));
+ ASSERT_THAT(timer.pvalues.p90.floor, US(89));
+ ASSERT_THAT(timer.pvalues.p90.nominal, US(90));
+ ASSERT_THAT(timer.pvalues.p95.floor, US(94));
+ ASSERT_THAT(timer.pvalues.p95.nominal, US(95));
+ ASSERT_THAT(timer.pvalues.p99.floor, US(98));
+ ASSERT_THAT(timer.pvalues.p99.nominal, US(99));
+ ASSERT_THAT(timer.largest[0], US(100));
+ ASSERT_THAT(timer.largest[1], US(99));
+ ASSERT_THAT(timer.largest[2], US(98));
+ ASSERT_THAT(timer.largest[3], US(97));
+ ASSERT_THAT(timer.largest[4], US(96));
+}
+
+// Verify that if too many oversize entries are reported, the percentile values cannot be computed
+// and are set to zero.
+TEST(ResourceTimerTest, TimerOversize) {
+ static const int oversize = US(2 * 1000 * 1000);
+
+ ResourceTimer::Timer timer;
+ for (int i = 1; i <= 100; i++) {
+ timer.record(US(i));
+ }
+
+ // Insert enough oversize values to invalidate the p90, p95, and p99 percentiles. The p50 is
+ // still computable.
+ for (int i = 1; i <= 50; i++) {
+ timer.record(oversize);
+ }
+ ASSERT_THAT(timer.largest[0], oversize);
+ ASSERT_THAT(timer.largest[1], oversize);
+ ASSERT_THAT(timer.largest[2], oversize);
+ ASSERT_THAT(timer.largest[3], oversize);
+ ASSERT_THAT(timer.largest[4], oversize);
+ timer.compute();
+ ASSERT_THAT(timer.pvalues.p50.floor, US(74));
+ ASSERT_THAT(timer.pvalues.p50.nominal, US(75));
+ ASSERT_THAT(timer.pvalues.p90.floor, 0);
+ ASSERT_THAT(timer.pvalues.p90.nominal, 0);
+ ASSERT_THAT(timer.pvalues.p95.floor, 0);
+ ASSERT_THAT(timer.pvalues.p95.nominal, 0);
+ ASSERT_THAT(timer.pvalues.p99.floor, 0);
+ ASSERT_THAT(timer.pvalues.p99.nominal, 0);
+}
+
+
+} // namespace android
diff --git a/libs/androidfw/tests/SparseEntry_bench.cpp b/libs/androidfw/tests/SparseEntry_bench.cpp
index c9b4ad8af278..fffeeb802873 100644
--- a/libs/androidfw/tests/SparseEntry_bench.cpp
+++ b/libs/androidfw/tests/SparseEntry_bench.cpp
@@ -16,6 +16,7 @@
#include "androidfw/AssetManager.h"
#include "androidfw/ResourceTypes.h"
+#include "android-base/file.h"
#include "BenchmarkHelpers.h"
#include "data/sparse/R.h"
@@ -24,40 +25,74 @@ namespace sparse = com::android::sparse;
namespace android {
+static void BM_SparseEntryGetResourceHelper(const std::vector<std::string>& paths,
+ uint32_t resid, benchmark::State& state, void (*GetResourceBenchmarkFunc)(
+ const std::vector<std::string>&, const ResTable_config*,
+ uint32_t, benchmark::State&)){
+ ResTable_config config;
+ memset(&config, 0, sizeof(config));
+ config.orientation = ResTable_config::ORIENTATION_LAND;
+ GetResourceBenchmarkFunc(paths, &config, resid, state);
+}
+
static void BM_SparseEntryGetResourceOldSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid,
+ state, &GetResourceBenchmarkOld);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Large, sparse::R::string::foo_999);
static void BM_SparseEntryGetResourceOldNotSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid,
+ state, &GetResourceBenchmarkOld);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Large, sparse::R::string::foo_999);
static void BM_SparseEntryGetResourceSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmark({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid,
+ state, &GetResourceBenchmark);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Large, sparse::R::string::foo_999);
static void BM_SparseEntryGetResourceNotSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmark({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid,
+ state, &GetResourceBenchmark);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Large, sparse::R::string::foo_999);
+static void BM_SparseEntryGetResourceOldSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmarkOld);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceOldNotSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmarkOld);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmark);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceNotSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmark);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Large, sparse::R::string::foo_999);
+
} // namespace android
diff --git a/libs/androidfw/tests/StringPiece_test.cpp b/libs/androidfw/tests/StringPiece_test.cpp
index 316a5c1bf40e..822e527253df 100644
--- a/libs/androidfw/tests/StringPiece_test.cpp
+++ b/libs/androidfw/tests/StringPiece_test.cpp
@@ -60,36 +60,4 @@ TEST(StringPieceTest, PiecesHaveCorrectSortOrderUtf8) {
EXPECT_TRUE(StringPiece(car) > banana);
}
-TEST(StringPieceTest, ContainsOtherStringPiece) {
- StringPiece text("I am a leaf on the wind.");
- StringPiece start_needle("I am");
- StringPiece end_needle("wind.");
- StringPiece middle_needle("leaf");
- StringPiece empty_needle("");
- StringPiece missing_needle("soar");
- StringPiece long_needle("This string is longer than the text.");
-
- EXPECT_TRUE(text.contains(start_needle));
- EXPECT_TRUE(text.contains(end_needle));
- EXPECT_TRUE(text.contains(middle_needle));
- EXPECT_TRUE(text.contains(empty_needle));
- EXPECT_FALSE(text.contains(missing_needle));
- EXPECT_FALSE(text.contains(long_needle));
-
- StringPiece16 text16(u"I am a leaf on the wind.");
- StringPiece16 start_needle16(u"I am");
- StringPiece16 end_needle16(u"wind.");
- StringPiece16 middle_needle16(u"leaf");
- StringPiece16 empty_needle16(u"");
- StringPiece16 missing_needle16(u"soar");
- StringPiece16 long_needle16(u"This string is longer than the text.");
-
- EXPECT_TRUE(text16.contains(start_needle16));
- EXPECT_TRUE(text16.contains(end_needle16));
- EXPECT_TRUE(text16.contains(middle_needle16));
- EXPECT_TRUE(text16.contains(empty_needle16));
- EXPECT_FALSE(text16.contains(missing_needle16));
- EXPECT_FALSE(text16.contains(long_needle16));
-}
-
} // namespace android
diff --git a/libs/androidfw/tests/StringPool_test.cpp b/libs/androidfw/tests/StringPool_test.cpp
new file mode 100644
index 000000000000..0e0acae165d9
--- /dev/null
+++ b/libs/androidfw/tests/StringPool_test.cpp
@@ -0,0 +1,388 @@
+/*
+ * 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/StringPool.h"
+
+#include <string>
+
+#include "androidfw/IDiagnostics.h"
+#include "androidfw/StringPiece.h"
+#include "androidfw/Util.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+using ::android::StringPiece;
+using ::android::StringPiece16;
+using ::testing::Eq;
+using ::testing::Ne;
+using ::testing::NotNull;
+using ::testing::Pointee;
+
+namespace android {
+
+TEST(StringPoolTest, InsertOneString) {
+ StringPool pool;
+
+ StringPool::Ref ref = pool.MakeRef("wut");
+ EXPECT_THAT(*ref, Eq("wut"));
+}
+
+TEST(StringPoolTest, InsertTwoUniqueStrings) {
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("wut");
+ StringPool::Ref ref_b = pool.MakeRef("hey");
+
+ EXPECT_THAT(*ref_a, Eq("wut"));
+ EXPECT_THAT(*ref_b, Eq("hey"));
+}
+
+TEST(StringPoolTest, DoNotInsertNewDuplicateString) {
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("wut");
+ StringPool::Ref ref_b = pool.MakeRef("wut");
+
+ EXPECT_THAT(*ref_a, Eq("wut"));
+ EXPECT_THAT(*ref_b, Eq("wut"));
+ EXPECT_THAT(pool.size(), Eq(1u));
+}
+
+TEST(StringPoolTest, DoNotDedupeSameStringDifferentPriority) {
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("wut", StringPool::Context(0x81010001));
+ StringPool::Ref ref_b = pool.MakeRef("wut", StringPool::Context(0x81010002));
+
+ EXPECT_THAT(*ref_a, Eq("wut"));
+ EXPECT_THAT(*ref_b, Eq("wut"));
+ EXPECT_THAT(pool.size(), Eq(2u));
+}
+
+TEST(StringPoolTest, MaintainInsertionOrderIndex) {
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("z");
+ StringPool::Ref ref_b = pool.MakeRef("a");
+ StringPool::Ref ref_c = pool.MakeRef("m");
+
+ EXPECT_THAT(ref_a.index(), Eq(0u));
+ EXPECT_THAT(ref_b.index(), Eq(1u));
+ EXPECT_THAT(ref_c.index(), Eq(2u));
+}
+
+TEST(StringPoolTest, PruneStringsWithNoReferences) {
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("foo");
+
+ {
+ StringPool::Ref ref_b = pool.MakeRef("wut");
+ EXPECT_THAT(*ref_b, Eq("wut"));
+ EXPECT_THAT(pool.size(), Eq(2u));
+ pool.Prune();
+ EXPECT_THAT(pool.size(), Eq(2u));
+ }
+ EXPECT_THAT(pool.size(), Eq(2u));
+
+ {
+ StringPool::Ref ref_c = pool.MakeRef("bar");
+ EXPECT_THAT(pool.size(), Eq(3u));
+
+ pool.Prune();
+ EXPECT_THAT(pool.size(), Eq(2u));
+ }
+ EXPECT_THAT(pool.size(), Eq(2u));
+
+ pool.Prune();
+ EXPECT_THAT(pool.size(), Eq(1u));
+}
+
+TEST(StringPoolTest, SortAndMaintainIndexesInStringReferences) {
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("z");
+ StringPool::Ref ref_b = pool.MakeRef("a");
+ StringPool::Ref ref_c = pool.MakeRef("m");
+
+ EXPECT_THAT(*ref_a, Eq("z"));
+ EXPECT_THAT(ref_a.index(), Eq(0u));
+
+ EXPECT_THAT(*ref_b, Eq("a"));
+ EXPECT_THAT(ref_b.index(), Eq(1u));
+
+ EXPECT_THAT(*ref_c, Eq("m"));
+ EXPECT_THAT(ref_c.index(), Eq(2u));
+
+ pool.Sort();
+
+ EXPECT_THAT(*ref_a, Eq("z"));
+ EXPECT_THAT(ref_a.index(), Eq(2u));
+
+ EXPECT_THAT(*ref_b, Eq("a"));
+ EXPECT_THAT(ref_b.index(), Eq(0u));
+
+ EXPECT_THAT(*ref_c, Eq("m"));
+ EXPECT_THAT(ref_c.index(), Eq(1u));
+}
+
+TEST(StringPoolTest, SortAndStillDedupe) {
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("z");
+ StringPool::Ref ref_b = pool.MakeRef("a");
+ StringPool::Ref ref_c = pool.MakeRef("m");
+
+ pool.Sort();
+
+ StringPool::Ref ref_d = pool.MakeRef("z");
+ StringPool::Ref ref_e = pool.MakeRef("a");
+ StringPool::Ref ref_f = pool.MakeRef("m");
+
+ EXPECT_THAT(ref_d.index(), Eq(ref_a.index()));
+ EXPECT_THAT(ref_e.index(), Eq(ref_b.index()));
+ EXPECT_THAT(ref_f.index(), Eq(ref_c.index()));
+}
+
+TEST(StringPoolTest, AddStyles) {
+ StringPool pool;
+
+ StringPool::StyleRef ref = pool.MakeRef(StyleString{{"android"}, {Span{{"b"}, 2, 6}}});
+ EXPECT_THAT(ref.index(), Eq(0u));
+ EXPECT_THAT(ref->value, Eq("android"));
+ ASSERT_THAT(ref->spans.size(), Eq(1u));
+
+ const StringPool::Span& span = ref->spans.front();
+ EXPECT_THAT(*span.name, Eq("b"));
+ EXPECT_THAT(span.first_char, Eq(2u));
+ EXPECT_THAT(span.last_char, Eq(6u));
+}
+
+TEST(StringPoolTest, DoNotDedupeStyleWithSameStringAsNonStyle) {
+ StringPool pool;
+
+ StringPool::Ref ref = pool.MakeRef("android");
+
+ StyleString str{{"android"}, {}};
+ StringPool::StyleRef style_ref = pool.MakeRef(StyleString{{"android"}, {}});
+
+ EXPECT_THAT(ref.index(), Ne(style_ref.index()));
+}
+
+TEST(StringPoolTest, StylesAndStringsAreSeparateAfterSorting) {
+ StringPool pool;
+
+ StringPool::StyleRef ref_a = pool.MakeRef(StyleString{{"beta"}, {}});
+ StringPool::Ref ref_b = pool.MakeRef("alpha");
+ StringPool::StyleRef ref_c = pool.MakeRef(StyleString{{"alpha"}, {}});
+
+ EXPECT_THAT(ref_b.index(), Ne(ref_c.index()));
+
+ pool.Sort();
+
+ EXPECT_THAT(ref_c.index(), Eq(0u));
+ EXPECT_THAT(ref_a.index(), Eq(1u));
+ EXPECT_THAT(ref_b.index(), Eq(2u));
+}
+
+TEST(StringPoolTest, FlattenEmptyStringPoolUtf8) {
+ using namespace android; // For NO_ERROR on Windows.
+ NoOpDiagnostics diag;
+
+ StringPool pool;
+ BigBuffer buffer(1024);
+ StringPool::FlattenUtf8(&buffer, pool, &diag);
+
+ std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer);
+ ResStringPool test;
+ ASSERT_THAT(test.setTo(data.get(), buffer.size()), Eq(NO_ERROR));
+}
+
+TEST(StringPoolTest, FlattenOddCharactersUtf16) {
+ using namespace android; // For NO_ERROR on Windows.
+ NoOpDiagnostics diag;
+
+ StringPool pool;
+ pool.MakeRef("\u093f");
+ BigBuffer buffer(1024);
+ StringPool::FlattenUtf16(&buffer, pool, &diag);
+
+ std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer);
+ ResStringPool test;
+ ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR);
+ auto str = test.stringAt(0);
+ ASSERT_TRUE(str.has_value());
+ EXPECT_THAT(str->size(), Eq(1u));
+ EXPECT_THAT(str->data(), Pointee(Eq(u'\u093f')));
+ EXPECT_THAT(str->data()[1], Eq(0u));
+}
+
+constexpr const char* sLongString =
+ "バッテリーを長持ちさせるため、バッテリーセーバーは端末のパフォーマンスを抑"
+ "え、バイブレーション、位置情報サービス、大半のバックグラウンドデータを制限"
+ "します。メール、SMSや、同期を使 "
+ "用するその他のアプリは、起動しても更新されないことがあります。バッテリーセ"
+ "ーバーは端末の充電中は自動的にOFFになります。";
+
+TEST(StringPoolTest, Flatten) {
+ using namespace android; // For NO_ERROR on Windows.
+ NoOpDiagnostics diag;
+
+ StringPool pool;
+
+ StringPool::Ref ref_a = pool.MakeRef("hello");
+ StringPool::Ref ref_b = pool.MakeRef("goodbye");
+ StringPool::Ref ref_c = pool.MakeRef(sLongString);
+ StringPool::Ref ref_d = pool.MakeRef("");
+ StringPool::StyleRef ref_e =
+ pool.MakeRef(StyleString{{"style"}, {Span{{"b"}, 0, 1}, Span{{"i"}, 2, 3}}});
+
+ // Styles are always first.
+ EXPECT_THAT(ref_e.index(), Eq(0u));
+
+ EXPECT_THAT(ref_a.index(), Eq(1u));
+ EXPECT_THAT(ref_b.index(), Eq(2u));
+ EXPECT_THAT(ref_c.index(), Eq(3u));
+ EXPECT_THAT(ref_d.index(), Eq(4u));
+
+ BigBuffer buffers[2] = {BigBuffer(1024), BigBuffer(1024)};
+ StringPool::FlattenUtf8(&buffers[0], pool, &diag);
+ StringPool::FlattenUtf16(&buffers[1], pool, &diag);
+
+ // Test both UTF-8 and UTF-16 buffers.
+ for (const BigBuffer& buffer : buffers) {
+ std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer);
+
+ ResStringPool test;
+ ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR);
+
+ EXPECT_THAT(android::util::GetString(test, 1), Eq("hello"));
+ EXPECT_THAT(android::util::GetString16(test, 1), Eq(u"hello"));
+
+ EXPECT_THAT(android::util::GetString(test, 2), Eq("goodbye"));
+ EXPECT_THAT(android::util::GetString16(test, 2), Eq(u"goodbye"));
+
+ EXPECT_THAT(android::util::GetString(test, 3), Eq(sLongString));
+ EXPECT_THAT(android::util::GetString16(test, 3), Eq(util::Utf8ToUtf16(sLongString)));
+
+ EXPECT_TRUE(test.stringAt(4).has_value() || test.string8At(4).has_value());
+
+ EXPECT_THAT(android::util::GetString(test, 0), Eq("style"));
+ EXPECT_THAT(android::util::GetString16(test, 0), Eq(u"style"));
+
+ auto span_result = test.styleAt(0);
+ ASSERT_TRUE(span_result.has_value());
+
+ const ResStringPool_span* span = span_result->unsafe_ptr();
+ EXPECT_THAT(android::util::GetString(test, span->name.index), Eq("b"));
+ EXPECT_THAT(android::util::GetString16(test, span->name.index), Eq(u"b"));
+ EXPECT_THAT(span->firstChar, Eq(0u));
+ EXPECT_THAT(span->lastChar, Eq(1u));
+ span++;
+
+ ASSERT_THAT(span->name.index, Ne(ResStringPool_span::END));
+ EXPECT_THAT(android::util::GetString(test, span->name.index), Eq("i"));
+ EXPECT_THAT(android::util::GetString16(test, span->name.index), Eq(u"i"));
+ EXPECT_THAT(span->firstChar, Eq(2u));
+ EXPECT_THAT(span->lastChar, Eq(3u));
+ span++;
+
+ EXPECT_THAT(span->name.index, Eq(ResStringPool_span::END));
+ }
+}
+
+TEST(StringPoolTest, ModifiedUTF8) {
+ using namespace android; // For NO_ERROR on Windows.
+ NoOpDiagnostics diag;
+ StringPool pool;
+ StringPool::Ref ref_a = pool.MakeRef("\xF0\x90\x90\x80"); // 𐐀 (U+10400)
+ StringPool::Ref ref_b = pool.MakeRef("foo \xF0\x90\x90\xB7 bar"); // 𐐷 (U+10437)
+ StringPool::Ref ref_c = pool.MakeRef("\xF0\x90\x90\x80\xF0\x90\x90\xB7");
+
+ BigBuffer buffer(1024);
+ StringPool::FlattenUtf8(&buffer, pool, &diag);
+ std::unique_ptr<uint8_t[]> data = android::util::Copy(buffer);
+
+ // Check that the codepoints are encoded using two three-byte surrogate pairs
+ ResStringPool test;
+ ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR);
+ auto str = test.string8At(0);
+ ASSERT_TRUE(str.has_value());
+ EXPECT_THAT(*str, Eq("\xED\xA0\x81\xED\xB0\x80"));
+
+ str = test.string8At(1);
+ ASSERT_TRUE(str.has_value());
+ EXPECT_THAT(*str, Eq("foo \xED\xA0\x81\xED\xB0\xB7 bar"));
+
+ str = test.string8At(2);
+ ASSERT_TRUE(str.has_value());
+ EXPECT_THAT(*str, Eq("\xED\xA0\x81\xED\xB0\x80\xED\xA0\x81\xED\xB0\xB7"));
+
+ // Check that retrieving the strings returns the original UTF-8 character bytes
+ EXPECT_THAT(android::util::GetString(test, 0), Eq("\xF0\x90\x90\x80"));
+ EXPECT_THAT(android::util::GetString(test, 1), Eq("foo \xF0\x90\x90\xB7 bar"));
+ EXPECT_THAT(android::util::GetString(test, 2), Eq("\xF0\x90\x90\x80\xF0\x90\x90\xB7"));
+}
+
+TEST(StringPoolTest, MaxEncodingLength) {
+ NoOpDiagnostics diag;
+ using namespace android; // For NO_ERROR on Windows.
+ ResStringPool test;
+
+ StringPool pool;
+ pool.MakeRef("aaaaaaaaaa");
+ BigBuffer buffers[2] = {BigBuffer(1024), BigBuffer(1024)};
+
+ // Make sure a UTF-8 string under the maximum length does not produce an error
+ EXPECT_THAT(StringPool::FlattenUtf8(&buffers[0], pool, &diag), Eq(true));
+ std::unique_ptr<uint8_t[]> data = android::util::Copy(buffers[0]);
+ test.setTo(data.get(), buffers[0].size());
+ EXPECT_THAT(android::util::GetString(test, 0), Eq("aaaaaaaaaa"));
+
+ // Make sure a UTF-16 string under the maximum length does not produce an error
+ EXPECT_THAT(StringPool::FlattenUtf16(&buffers[1], pool, &diag), Eq(true));
+ data = android::util::Copy(buffers[1]);
+ test.setTo(data.get(), buffers[1].size());
+ EXPECT_THAT(android::util::GetString16(test, 0), Eq(u"aaaaaaaaaa"));
+
+ StringPool pool2;
+ std::string longStr(50000, 'a');
+ pool2.MakeRef("this fits1");
+ pool2.MakeRef(longStr);
+ pool2.MakeRef("this fits2");
+ BigBuffer buffers2[2] = {BigBuffer(1024), BigBuffer(1024)};
+
+ // Make sure a string that exceeds the maximum length of UTF-8 produces an
+ // error and writes a shorter error string instead
+ EXPECT_THAT(StringPool::FlattenUtf8(&buffers2[0], pool2, &diag), Eq(false));
+ data = android::util::Copy(buffers2[0]);
+ test.setTo(data.get(), buffers2[0].size());
+ EXPECT_THAT(android::util::GetString(test, 0), "this fits1");
+ EXPECT_THAT(android::util::GetString(test, 1), "STRING_TOO_LARGE");
+ EXPECT_THAT(android::util::GetString(test, 2), "this fits2");
+
+ // Make sure a string that a string that exceeds the maximum length of UTF-8
+ // but not UTF-16 does not error for UTF-16
+ StringPool pool3;
+ std::u16string longStr16(50000, 'a');
+ pool3.MakeRef(longStr);
+ EXPECT_THAT(StringPool::FlattenUtf16(&buffers2[1], pool3, &diag), Eq(true));
+ data = android::util::Copy(buffers2[1]);
+ test.setTo(data.get(), buffers2[1].size());
+ EXPECT_THAT(android::util::GetString16(test, 0), Eq(longStr16));
+}
+
+} // namespace android
diff --git a/libs/androidfw/tests/TypeWrappers_test.cpp b/libs/androidfw/tests/TypeWrappers_test.cpp
index d69abe5d0f11..ed30904ec179 100644
--- a/libs/androidfw/tests/TypeWrappers_test.cpp
+++ b/libs/androidfw/tests/TypeWrappers_test.cpp
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+#include <algorithm>
#include <androidfw/ResourceTypes.h>
#include <androidfw/TypeWrappers.h>
#include <utils/String8.h>
@@ -22,88 +23,123 @@
namespace android {
-void* createTypeData() {
- ResTable_type t;
- memset(&t, 0, sizeof(t));
+// create a ResTable_type in memory with a vector of Res_value*
+static ResTable_type* createTypeTable(std::vector<Res_value*>& values,
+ bool compact_entry = false,
+ bool short_offsets = false)
+{
+ ResTable_type t{};
t.header.type = RES_TABLE_TYPE_TYPE;
t.header.headerSize = sizeof(t);
+ t.header.size = sizeof(t);
t.id = 1;
- t.entryCount = 3;
-
- uint32_t offsets[3];
- t.entriesStart = t.header.headerSize + sizeof(offsets);
- t.header.size = t.entriesStart;
-
- offsets[0] = 0;
- ResTable_entry e1;
- memset(&e1, 0, sizeof(e1));
- e1.size = sizeof(e1);
- e1.key.index = 0;
- t.header.size += sizeof(e1);
-
- Res_value v1;
- memset(&v1, 0, sizeof(v1));
- t.header.size += sizeof(v1);
-
- offsets[1] = ResTable_type::NO_ENTRY;
-
- offsets[2] = sizeof(e1) + sizeof(v1);
- ResTable_entry e2;
- memset(&e2, 0, sizeof(e2));
- e2.size = sizeof(e2);
- e2.key.index = 1;
- t.header.size += sizeof(e2);
-
- Res_value v2;
- memset(&v2, 0, sizeof(v2));
- t.header.size += sizeof(v2);
-
- uint8_t* data = (uint8_t*)malloc(t.header.size);
- uint8_t* p = data;
- memcpy(p, &t, sizeof(t));
- p += sizeof(t);
- memcpy(p, offsets, sizeof(offsets));
- p += sizeof(offsets);
- memcpy(p, &e1, sizeof(e1));
- p += sizeof(e1);
- memcpy(p, &v1, sizeof(v1));
- p += sizeof(v1);
- memcpy(p, &e2, sizeof(e2));
- p += sizeof(e2);
- memcpy(p, &v2, sizeof(v2));
- p += sizeof(v2);
- return data;
+ t.flags = short_offsets ? ResTable_type::FLAG_OFFSET16 : 0;
+
+ t.header.size += values.size() * (short_offsets ? sizeof(uint16_t) : sizeof(uint32_t));
+ t.entriesStart = t.header.size;
+ t.entryCount = values.size();
+
+ size_t entry_size = compact_entry ? sizeof(ResTable_entry)
+ : sizeof(ResTable_entry) + sizeof(Res_value);
+ for (auto const v : values) {
+ t.header.size += v ? entry_size : 0;
+ }
+
+ uint8_t* data = (uint8_t *)malloc(t.header.size);
+ uint8_t* p_header = data;
+ uint8_t* p_offsets = data + t.header.headerSize;
+ uint8_t* p_entries = data + t.entriesStart;
+
+ memcpy(p_header, &t, sizeof(t));
+
+ size_t i = 0, entry_offset = 0;
+ uint32_t k = 0;
+ for (auto const& v : values) {
+ if (short_offsets) {
+ uint16_t *p = reinterpret_cast<uint16_t *>(p_offsets) + i;
+ *p = v ? (entry_offset >> 2) & 0xffffu : 0xffffu;
+ } else {
+ uint32_t *p = reinterpret_cast<uint32_t *>(p_offsets) + i;
+ *p = v ? entry_offset : ResTable_type::NO_ENTRY;
+ }
+
+ if (v) {
+ ResTable_entry entry{};
+ if (compact_entry) {
+ entry.compact.key = i;
+ entry.compact.flags = ResTable_entry::FLAG_COMPACT | (v->dataType << 8);
+ entry.compact.data = v->data;
+ memcpy(p_entries, &entry, sizeof(entry)); p_entries += sizeof(entry);
+ entry_offset += sizeof(entry);
+ } else {
+ Res_value value{};
+ entry.full.size = sizeof(entry);
+ entry.full.key.index = i;
+ value = *v;
+ memcpy(p_entries, &entry, sizeof(entry)); p_entries += sizeof(entry);
+ memcpy(p_entries, &value, sizeof(value)); p_entries += sizeof(value);
+ entry_offset += sizeof(entry) + sizeof(value);
+ }
+ }
+ i++;
+ }
+ return reinterpret_cast<ResTable_type*>(data);
}
TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) {
- ResTable_type* data = (ResTable_type*) createTypeData();
+ std::vector<Res_value *> values;
- TypeVariant v(data);
+ Res_value *v1 = new Res_value{};
+ values.push_back(v1);
- TypeVariant::iterator iter = v.beginEntries();
- ASSERT_EQ(uint32_t(0), iter.index());
- ASSERT_TRUE(NULL != *iter);
- ASSERT_EQ(uint32_t(0), iter->key.index);
- ASSERT_NE(v.endEntries(), iter);
+ values.push_back(nullptr);
- iter++;
+ Res_value *v2 = new Res_value{};
+ values.push_back(v2);
- ASSERT_EQ(uint32_t(1), iter.index());
- ASSERT_TRUE(NULL == *iter);
- ASSERT_NE(v.endEntries(), iter);
+ Res_value *v3 = new Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x12345678};
+ values.push_back(v3);
- iter++;
+ // test for combinations of compact_entry and short_offsets
+ for (size_t i = 0; i < 4; i++) {
+ bool compact_entry = i & 0x1, short_offsets = i & 0x2;
+ ResTable_type* data = createTypeTable(values, compact_entry, short_offsets);
+ TypeVariant v(data);
- ASSERT_EQ(uint32_t(2), iter.index());
- ASSERT_TRUE(NULL != *iter);
- ASSERT_EQ(uint32_t(1), iter->key.index);
- ASSERT_NE(v.endEntries(), iter);
+ TypeVariant::iterator iter = v.beginEntries();
+ ASSERT_EQ(uint32_t(0), iter.index());
+ ASSERT_TRUE(NULL != *iter);
+ ASSERT_EQ(uint32_t(0), iter->key());
+ ASSERT_NE(v.endEntries(), iter);
- iter++;
+ iter++;
- ASSERT_EQ(v.endEntries(), iter);
+ ASSERT_EQ(uint32_t(1), iter.index());
+ ASSERT_TRUE(NULL == *iter);
+ ASSERT_NE(v.endEntries(), iter);
- free(data);
+ iter++;
+
+ ASSERT_EQ(uint32_t(2), iter.index());
+ ASSERT_TRUE(NULL != *iter);
+ ASSERT_EQ(uint32_t(2), iter->key());
+ ASSERT_NE(v.endEntries(), iter);
+
+ iter++;
+
+ ASSERT_EQ(uint32_t(3), iter.index());
+ ASSERT_TRUE(NULL != *iter);
+ ASSERT_EQ(iter->is_compact(), compact_entry);
+ ASSERT_EQ(uint32_t(3), iter->key());
+ ASSERT_EQ(uint32_t(0x12345678), iter->value().data);
+ ASSERT_EQ(Res_value::TYPE_STRING, iter->value().dataType);
+
+ iter++;
+
+ ASSERT_EQ(v.endEntries(), iter);
+
+ free(data);
+ }
}
} // namespace android
diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap
index 88eadccb38cf..8e847e81aa31 100644
--- a/libs/androidfw/tests/data/overlay/overlay.idmap
+++ b/libs/androidfw/tests/data/overlay/overlay.idmap
Binary files differ
diff --git a/libs/androidfw/tests/data/sparse/Android.bp b/libs/androidfw/tests/data/sparse/Android.bp
new file mode 100644
index 000000000000..b0da375c7971
--- /dev/null
+++ b/libs/androidfw/tests/data/sparse/Android.bp
@@ -0,0 +1,23 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_libs_androidfw_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_libs_androidfw_license"],
+}
+
+android_test_helper_app {
+ name: "FrameworkResourcesSparseTestApp",
+ sdk_version: "current",
+ min_sdk_version: "32",
+ aaptflags: [
+ "--enable-sparse-encoding",
+ ],
+}
+
+android_test_helper_app {
+ name: "FrameworkResourcesNotSparseTestApp",
+ sdk_version: "current",
+ min_sdk_version: "32",
+}
diff --git a/libs/androidfw/tests/data/sparse/AndroidManifest.xml b/libs/androidfw/tests/data/sparse/AndroidManifest.xml
index 27911b62447a..9c23a7227631 100644
--- a/libs/androidfw/tests/data/sparse/AndroidManifest.xml
+++ b/libs/androidfw/tests/data/sparse/AndroidManifest.xml
@@ -17,4 +17,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.sparse">
<application />
+ <uses-sdk android:minSdkVersion="32" />
</manifest>
diff --git a/libs/androidfw/tests/data/sparse/R.h b/libs/androidfw/tests/data/sparse/R.h
index 2492dbf33f4a..a66e1af150c4 100644
--- a/libs/androidfw/tests/data/sparse/R.h
+++ b/libs/androidfw/tests/data/sparse/R.h
@@ -42,7 +42,7 @@ struct R {
struct string {
enum : uint32_t {
foo_999 = 0x7f0203e7,
- only_v26 = 0x7f0203e8
+ only_land = 0x7f0203e8
};
};
};
diff --git a/libs/androidfw/tests/data/sparse/gen_strings.sh b/libs/androidfw/tests/data/sparse/gen_strings.sh
index 4ea5468c7df9..114ecbb7d860 100755
--- a/libs/androidfw/tests/data/sparse/gen_strings.sh
+++ b/libs/androidfw/tests/data/sparse/gen_strings.sh
@@ -1,20 +1,20 @@
#!/bin/bash
OUTPUT_default=res/values/strings.xml
-OUTPUT_v26=res/values-v26/strings.xml
+OUTPUT_land=res/values-land/strings.xml
echo "<resources>" > $OUTPUT_default
-echo "<resources>" > $OUTPUT_v26
+echo "<resources>" > $OUTPUT_land
for i in {0..999}
do
echo " <string name=\"foo_$i\">$i</string>" >> $OUTPUT_default
if [ "$(($i % 3))" -eq "0" ]
then
- echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_v26
+ echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_land
fi
done
echo "</resources>" >> $OUTPUT_default
-echo " <string name=\"only_v26\">only v26</string>" >> $OUTPUT_v26
-echo "</resources>" >> $OUTPUT_v26
+echo " <string name=\"only_land\">only land</string>" >> $OUTPUT_land
+echo "</resources>" >> $OUTPUT_land
diff --git a/libs/androidfw/tests/data/sparse/not_sparse.apk b/libs/androidfw/tests/data/sparse/not_sparse.apk
index b08a621195c0..4d4d4a849033 100644
--- a/libs/androidfw/tests/data/sparse/not_sparse.apk
+++ b/libs/androidfw/tests/data/sparse/not_sparse.apk
Binary files differ
diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml
index d116087ec3c0..66222c327416 100644
--- a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml
+++ b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml
@@ -333,5 +333,5 @@
<string name="foo_993">9930</string>
<string name="foo_996">9960</string>
<string name="foo_999">9990</string>
- <string name="only_v26">only v26</string>
+ <string name="only_land">only land</string>
</resources>
diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml b/libs/androidfw/tests/data/sparse/res/values-land/values.xml
index b396ad24aa8c..b396ad24aa8c 100644
--- a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml
+++ b/libs/androidfw/tests/data/sparse/res/values-land/values.xml
diff --git a/libs/androidfw/tests/data/sparse/sparse.apk b/libs/androidfw/tests/data/sparse/sparse.apk
index 9fd01fbf2941..0f2d75a62b96 100644
--- a/libs/androidfw/tests/data/sparse/sparse.apk
+++ b/libs/androidfw/tests/data/sparse/sparse.apk
Binary files differ
diff --git a/libs/dream/lowlight/Android.bp b/libs/dream/lowlight/Android.bp
index 5b5b0f07cabd..e4d2e022cd76 100644
--- a/libs/dream/lowlight/Android.bp
+++ b/libs/dream/lowlight/Android.bp
@@ -25,6 +25,7 @@ filegroup {
name: "low_light_dream_lib-sources",
srcs: [
"src/**/*.java",
+ "src/**/*.kt",
],
path: "src",
}
@@ -37,10 +38,15 @@ android_library {
resource_dirs: [
"res",
],
+ libs: [
+ "kotlin-annotations",
+ ],
static_libs: [
"androidx.arch.core_core-runtime",
"dagger2",
"jsr330",
+ "kotlinx-coroutines-android",
+ "kotlinx-coroutines-core",
],
manifest: "AndroidManifest.xml",
plugins: ["dagger2-compiler"],
diff --git a/libs/dream/lowlight/res/values/config.xml b/libs/dream/lowlight/res/values/config.xml
index 70fe0738a6f4..78fefbf41141 100644
--- a/libs/dream/lowlight/res/values/config.xml
+++ b/libs/dream/lowlight/res/values/config.xml
@@ -17,4 +17,7 @@
<resources>
<!-- The dream component used when the device is low light environment. -->
<string translatable="false" name="config_lowLightDreamComponent"/>
+ <!-- The max number of milliseconds to wait for the low light transition before setting
+ the system dream component -->
+ <integer name="config_lowLightTransitionTimeoutMs">2000</integer>
</resources>
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java
deleted file mode 100644
index 0e67ff58b1cf..000000000000
--- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.dream.lowlight;
-
-import static com.android.dream.lowlight.dagger.LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT;
-
-import android.annotation.IntDef;
-import android.annotation.RequiresPermission;
-import android.app.DreamManager;
-import android.content.ComponentName;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * Maintains the ambient light mode of the environment the device is in, and sets a low light dream
- * component, if present, as the system dream when the ambient light mode is low light.
- *
- * @hide
- */
-public final class LowLightDreamManager {
- private static final String TAG = "LowLightDreamManager";
- private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
- /**
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = { "AMBIENT_LIGHT_MODE_" }, value = {
- AMBIENT_LIGHT_MODE_UNKNOWN,
- AMBIENT_LIGHT_MODE_REGULAR,
- AMBIENT_LIGHT_MODE_LOW_LIGHT
- })
- public @interface AmbientLightMode {}
-
- /**
- * Constant for ambient light mode being unknown.
- * @hide
- */
- public static final int AMBIENT_LIGHT_MODE_UNKNOWN = 0;
-
- /**
- * Constant for ambient light mode being regular / bright.
- * @hide
- */
- public static final int AMBIENT_LIGHT_MODE_REGULAR = 1;
-
- /**
- * Constant for ambient light mode being low light / dim.
- * @hide
- */
- public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2;
-
- private final DreamManager mDreamManager;
-
- @Nullable
- private final ComponentName mLowLightDreamComponent;
-
- private int mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN;
-
- @Inject
- public LowLightDreamManager(
- DreamManager dreamManager,
- @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) {
- mDreamManager = dreamManager;
- mLowLightDreamComponent = lowLightDreamComponent;
- }
-
- /**
- * Sets the current ambient light mode.
- * @hide
- */
- @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE)
- public void setAmbientLightMode(@AmbientLightMode int ambientLightMode) {
- if (mLowLightDreamComponent == null) {
- if (DEBUG) {
- Log.d(TAG, "ignore ambient light mode change because low light dream component "
- + "is empty");
- }
- return;
- }
-
- if (mAmbientLightMode == ambientLightMode) {
- return;
- }
-
- if (DEBUG) {
- Log.d(TAG, "ambient light mode changed from " + mAmbientLightMode + " to "
- + ambientLightMode);
- }
-
- mAmbientLightMode = ambientLightMode;
-
- boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT;
- mDreamManager.setSystemDreamComponent(shouldEnterLowLight ? mLowLightDreamComponent : null);
- }
-}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
new file mode 100644
index 000000000000..96bfb78eff0d
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.dream.lowlight
+
+import android.Manifest
+import android.annotation.IntDef
+import android.annotation.RequiresPermission
+import android.app.DreamManager
+import android.content.ComponentName
+import android.util.Log
+import com.android.dream.lowlight.dagger.LowLightDreamModule
+import com.android.dream.lowlight.dagger.qualifiers.Application
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import javax.inject.Named
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+/**
+ * Maintains the ambient light mode of the environment the device is in, and sets a low light dream
+ * component, if present, as the system dream when the ambient light mode is low light.
+ *
+ * @hide
+ */
+class LowLightDreamManager @Inject constructor(
+ @Application private val coroutineScope: CoroutineScope,
+ private val dreamManager: DreamManager,
+ private val lowLightTransitionCoordinator: LowLightTransitionCoordinator,
+ @param:Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
+ private val lowLightDreamComponent: ComponentName?,
+ @param:Named(LowLightDreamModule.LOW_LIGHT_TRANSITION_TIMEOUT_MS)
+ private val lowLightTransitionTimeoutMs: Long
+) {
+ /**
+ * @hide
+ */
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ prefix = ["AMBIENT_LIGHT_MODE_"],
+ value = [
+ AMBIENT_LIGHT_MODE_UNKNOWN,
+ AMBIENT_LIGHT_MODE_REGULAR,
+ AMBIENT_LIGHT_MODE_LOW_LIGHT
+ ]
+ )
+ annotation class AmbientLightMode
+
+ private var mTransitionJob: Job? = null
+ private var mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN
+ private val mLowLightTransitionTimeout =
+ lowLightTransitionTimeoutMs.toDuration(DurationUnit.MILLISECONDS)
+
+ /**
+ * Sets the current ambient light mode.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_DREAM_STATE)
+ fun setAmbientLightMode(@AmbientLightMode ambientLightMode: Int) {
+ if (lowLightDreamComponent == null) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "ignore ambient light mode change because low light dream component is empty"
+ )
+ }
+ return
+ }
+ if (mAmbientLightMode == ambientLightMode) {
+ return
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG, "ambient light mode changed from $mAmbientLightMode to $ambientLightMode"
+ )
+ }
+ mAmbientLightMode = ambientLightMode
+ val shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT
+
+ // Cancel any previous transitions
+ mTransitionJob?.cancel()
+ mTransitionJob = coroutineScope.launch {
+ try {
+ lowLightTransitionCoordinator.waitForLowLightTransitionAnimation(
+ timeout = mLowLightTransitionTimeout,
+ entering = shouldEnterLowLight
+ )
+ } catch (ex: TimeoutCancellationException) {
+ Log.e(TAG, "timed out while waiting for low light animation", ex)
+ }
+ dreamManager.setSystemDreamComponent(
+ if (shouldEnterLowLight) lowLightDreamComponent else null
+ )
+ }
+ }
+
+ companion object {
+ private const val TAG = "LowLightDreamManager"
+ private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+
+ /**
+ * Constant for ambient light mode being unknown.
+ *
+ * @hide
+ */
+ const val AMBIENT_LIGHT_MODE_UNKNOWN = 0
+
+ /**
+ * Constant for ambient light mode being regular / bright.
+ *
+ * @hide
+ */
+ const val AMBIENT_LIGHT_MODE_REGULAR = 1
+
+ /**
+ * Constant for ambient light mode being low light / dim.
+ *
+ * @hide
+ */
+ const val AMBIENT_LIGHT_MODE_LOW_LIGHT = 2
+ }
+}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java
deleted file mode 100644
index 874a2d5af75e..000000000000
--- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java
+++ /dev/null
@@ -1,111 +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.
- */
-
-package com.android.dream.lowlight;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.Nullable;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * Helper class that allows listening and running animations before entering or exiting low light.
- */
-@Singleton
-public class LowLightTransitionCoordinator {
- /**
- * Listener that is notified before low light entry.
- */
- public interface LowLightEnterListener {
- /**
- * Callback that is notified before the device enters low light.
- *
- * @return an optional animator that will be waited upon before entering low light.
- */
- Animator onBeforeEnterLowLight();
- }
-
- /**
- * Listener that is notified before low light exit.
- */
- public interface LowLightExitListener {
- /**
- * Callback that is notified before the device exits low light.
- *
- * @return an optional animator that will be waited upon before exiting low light.
- */
- Animator onBeforeExitLowLight();
- }
-
- private LowLightEnterListener mLowLightEnterListener;
- private LowLightExitListener mLowLightExitListener;
-
- @Inject
- public LowLightTransitionCoordinator() {
- }
-
- /**
- * Sets the listener for the low light enter event.
- *
- * Only one listener can be set at a time. This method will overwrite any previously set
- * listener. Null can be used to unset the listener.
- */
- public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) {
- mLowLightEnterListener = lowLightEnterListener;
- }
-
- /**
- * Sets the listener for the low light exit event.
- *
- * Only one listener can be set at a time. This method will overwrite any previously set
- * listener. Null can be used to unset the listener.
- */
- public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) {
- mLowLightExitListener = lowLightExitListener;
- }
-
- /**
- * Notifies listeners that the device is about to enter or exit low light.
- *
- * @param entering true if listeners should be notified before entering low light, false if this
- * is notifying before exiting.
- * @param callback callback that will be run after listeners complete.
- */
- void notifyBeforeLowLightTransition(boolean entering, Runnable callback) {
- Animator animator = null;
-
- if (entering && mLowLightEnterListener != null) {
- animator = mLowLightEnterListener.onBeforeEnterLowLight();
- } else if (!entering && mLowLightExitListener != null) {
- animator = mLowLightExitListener.onBeforeExitLowLight();
- }
-
- // If the listener returned an animator to indicate it was running an animation, run the
- // callback after the animation completes, otherwise call the callback directly.
- if (animator != null) {
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- callback.run();
- }
- });
- } else {
- callback.run();
- }
- }
-}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
new file mode 100644
index 000000000000..473603002b21
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.dream.lowlight
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import com.android.dream.lowlight.util.suspendCoroutineWithTimeout
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.coroutines.resume
+import kotlin.time.Duration
+
+/**
+ * Helper class that allows listening and running animations before entering or exiting low light.
+ */
+@Singleton
+class LowLightTransitionCoordinator @Inject constructor() {
+ /**
+ * Listener that is notified before low light entry.
+ */
+ interface LowLightEnterListener {
+ /**
+ * Callback that is notified before the device enters low light.
+ *
+ * @return an optional animator that will be waited upon before entering low light.
+ */
+ fun onBeforeEnterLowLight(): Animator?
+ }
+
+ /**
+ * Listener that is notified before low light exit.
+ */
+ interface LowLightExitListener {
+ /**
+ * Callback that is notified before the device exits low light.
+ *
+ * @return an optional animator that will be waited upon before exiting low light.
+ */
+ fun onBeforeExitLowLight(): Animator?
+ }
+
+ private var mLowLightEnterListener: LowLightEnterListener? = null
+ private var mLowLightExitListener: LowLightExitListener? = null
+
+ /**
+ * Sets the listener for the low light enter event.
+ *
+ * Only one listener can be set at a time. This method will overwrite any previously set
+ * listener. Null can be used to unset the listener.
+ */
+ fun setLowLightEnterListener(lowLightEnterListener: LowLightEnterListener?) {
+ mLowLightEnterListener = lowLightEnterListener
+ }
+
+ /**
+ * Sets the listener for the low light exit event.
+ *
+ * Only one listener can be set at a time. This method will overwrite any previously set
+ * listener. Null can be used to unset the listener.
+ */
+ fun setLowLightExitListener(lowLightExitListener: LowLightExitListener?) {
+ mLowLightExitListener = lowLightExitListener
+ }
+
+ /**
+ * Notifies listeners that the device is about to enter or exit low light, and waits for the
+ * animation to complete. If this function is cancelled, the animation is also cancelled.
+ *
+ * @param timeout the maximum duration to wait for the transition animation. If the animation
+ * does not complete within this time period, a
+ * @param entering true if listeners should be notified before entering low light, false if this
+ * is notifying before exiting.
+ */
+ suspend fun waitForLowLightTransitionAnimation(timeout: Duration, entering: Boolean) =
+ suspendCoroutineWithTimeout(timeout) { continuation ->
+ var animator: Animator? = null
+ if (entering && mLowLightEnterListener != null) {
+ animator = mLowLightEnterListener!!.onBeforeEnterLowLight()
+ } else if (!entering && mLowLightExitListener != null) {
+ animator = mLowLightExitListener!!.onBeforeExitLowLight()
+ }
+
+ if (animator == null) {
+ continuation.resume(Unit)
+ return@suspendCoroutineWithTimeout
+ }
+
+ // If the listener returned an animator to indicate it was running an animation, run the
+ // callback after the animation completes, otherwise call the callback directly.
+ val listener = object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator) {
+ continuation.resume(Unit)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ continuation.cancel()
+ }
+ }
+ animator.addListener(listener)
+ continuation.invokeOnCancellation {
+ try {
+ animator.removeListener(listener)
+ animator.cancel()
+ } catch (exception: IndexOutOfBoundsException) {
+ // TODO(b/285666217): remove this try/catch once a proper fix is implemented.
+ // Cancelling the animator can cause an exception since we may be removing a
+ // listener during the cancellation. See b/285666217 for more details.
+ }
+ }
+ }
+}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java
deleted file mode 100644
index c183a04cb2f9..000000000000
--- a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.dream.lowlight.dagger;
-
-import android.app.DreamManager;
-import android.content.ComponentName;
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-
-import com.android.dream.lowlight.R;
-
-import javax.inject.Named;
-
-import dagger.Module;
-import dagger.Provides;
-
-/**
- * Dagger module for low light dream.
- *
- * @hide
- */
-@Module
-public interface LowLightDreamModule {
- String LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component";
-
- /**
- * Provides dream manager.
- */
- @Provides
- static DreamManager providesDreamManager(Context context) {
- return context.getSystemService(DreamManager.class);
- }
-
- /**
- * Provides the component name of the low light dream, or null if not configured.
- */
- @Provides
- @Named(LOW_LIGHT_DREAM_COMPONENT)
- @Nullable
- static ComponentName providesLowLightDreamComponent(Context context) {
- final String lowLightDreamComponent = context.getResources().getString(
- R.string.config_lowLightDreamComponent);
- return lowLightDreamComponent.isEmpty() ? null
- : ComponentName.unflattenFromString(lowLightDreamComponent);
- }
-}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt
new file mode 100644
index 000000000000..dd274bd9d509
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.dream.lowlight.dagger
+
+import android.app.DreamManager
+import android.content.ComponentName
+import android.content.Context
+import com.android.dream.lowlight.R
+import com.android.dream.lowlight.dagger.qualifiers.Application
+import com.android.dream.lowlight.dagger.qualifiers.Main
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Named
+
+/**
+ * Dagger module for low light dream.
+ *
+ * @hide
+ */
+@Module
+object LowLightDreamModule {
+ /**
+ * Provides dream manager.
+ */
+ @Provides
+ fun providesDreamManager(context: Context): DreamManager {
+ return requireNotNull(context.getSystemService(DreamManager::class.java))
+ }
+
+ /**
+ * Provides the component name of the low light dream, or null if not configured.
+ */
+ @Provides
+ @Named(LOW_LIGHT_DREAM_COMPONENT)
+ fun providesLowLightDreamComponent(context: Context): ComponentName? {
+ val lowLightDreamComponent = context.resources.getString(
+ R.string.config_lowLightDreamComponent
+ )
+ return if (lowLightDreamComponent.isEmpty()) {
+ null
+ } else {
+ ComponentName.unflattenFromString(lowLightDreamComponent)
+ }
+ }
+
+ @Provides
+ @Named(LOW_LIGHT_TRANSITION_TIMEOUT_MS)
+ fun providesLowLightTransitionTimeout(context: Context): Long {
+ return context.resources.getInteger(R.integer.config_lowLightTransitionTimeoutMs).toLong()
+ }
+
+ @Provides
+ @Main
+ fun providesMainDispatcher(): CoroutineDispatcher {
+ return Dispatchers.Main.immediate
+ }
+
+ @Provides
+ @Application
+ fun providesApplicationScope(@Main dispatcher: CoroutineDispatcher): CoroutineScope {
+ return CoroutineScope(dispatcher)
+ }
+
+ const val LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component"
+ const val LOW_LIGHT_TRANSITION_TIMEOUT_MS = "low_light_transition_timeout"
+}
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt
new file mode 100644
index 000000000000..541fe4017e6e
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt
@@ -0,0 +1,9 @@
+package com.android.dream.lowlight.dagger.qualifiers
+
+import android.content.Context
+import javax.inject.Qualifier
+
+/**
+ * Used to qualify a context as [Context.getApplicationContext]
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Application
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt
new file mode 100644
index 000000000000..ccd0710bdc60
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt
@@ -0,0 +1,8 @@
+package com.android.dream.lowlight.dagger.qualifiers
+
+import javax.inject.Qualifier
+
+/**
+ * Used to qualify code running on the main thread.
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt
index 4d0fbc4a0e38..ff675ccfaffb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,14 +14,16 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker.helpers
+package com.android.dream.lowlight.util
-import android.app.Instrumentation
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.wm.shell.flicker.testapp.Components
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
+import kotlin.time.Duration
-class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
- instrumentation,
- Components.SimpleActivity.LABEL,
- Components.SimpleActivity.COMPONENT.toFlickerComponent()
-) \ No newline at end of file
+suspend inline fun <T> suspendCoroutineWithTimeout(
+ timeout: Duration,
+ crossinline block: (CancellableContinuation<T>) -> Unit
+) = withTimeout(timeout) {
+ suspendCancellableCoroutine(block = block)
+}
diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp
index bd6f05eabac5..2d79090cd7d4 100644
--- a/libs/dream/lowlight/tests/Android.bp
+++ b/libs/dream/lowlight/tests/Android.bp
@@ -20,6 +20,7 @@ android_test {
name: "LowLightDreamTests",
srcs: [
"**/*.java",
+ "**/*.kt",
],
static_libs: [
"LowLightDreamLib",
@@ -28,6 +29,7 @@ android_test {
"androidx.test.ext.junit",
"frameworks-base-testutils",
"junit",
+ "kotlinx_coroutines_test",
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"testables",
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java
deleted file mode 100644
index f860613fc469..000000000000
--- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.dream.lowlight;
-
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import android.app.DreamManager;
-import android.content.ComponentName;
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class LowLightDreamManagerTest {
- @Mock
- private DreamManager mDreamManager;
-
- @Mock
- private ComponentName mDreamComponent;
-
- LowLightDreamManager mLowLightDreamManager;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mLowLightDreamManager = new LowLightDreamManager(mDreamManager,
- mDreamComponent);
- }
-
- @Test
- public void setAmbientLightMode_lowLight_setSystemDream() {
- mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
-
- verify(mDreamManager).setSystemDreamComponent(mDreamComponent);
- }
-
- @Test
- public void setAmbientLightMode_regularLight_clearSystemDream() {
- mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
-
- verify(mDreamManager).setSystemDreamComponent(null);
- }
-
- @Test
- public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() {
- // Set to low light first.
- mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
- clearInvocations(mDreamManager);
-
- // Return to default unknown mode.
- mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN);
-
- verify(mDreamManager).setSystemDreamComponent(null);
- }
-
- @Test
- public void setAmbientLightMode_dreamComponentNotSet_doNothing() {
- final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
- null /*dream component*/);
-
- lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
-
- verify(mDreamManager, never()).setSystemDreamComponent(any());
- }
-}
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
new file mode 100644
index 000000000000..2a886bc31788
--- /dev/null
+++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.dream.lowlight
+
+import android.animation.Animator
+import android.app.DreamManager
+import android.content.ComponentName
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import src.com.android.dream.lowlight.utils.any
+import src.com.android.dream.lowlight.utils.withArgCaptor
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class LowLightDreamManagerTest {
+ @Mock
+ private lateinit var mDreamManager: DreamManager
+ @Mock
+ private lateinit var mEnterAnimator: Animator
+ @Mock
+ private lateinit var mExitAnimator: Animator
+
+ private lateinit var mTransitionCoordinator: LowLightTransitionCoordinator
+ private lateinit var mLowLightDreamManager: LowLightDreamManager
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testScope = TestScope(StandardTestDispatcher())
+
+ mTransitionCoordinator = LowLightTransitionCoordinator()
+ mTransitionCoordinator.setLowLightEnterListener(
+ object : LowLightTransitionCoordinator.LowLightEnterListener {
+ override fun onBeforeEnterLowLight() = mEnterAnimator
+ })
+ mTransitionCoordinator.setLowLightExitListener(
+ object : LowLightTransitionCoordinator.LowLightExitListener {
+ override fun onBeforeExitLowLight() = mExitAnimator
+ })
+
+ mLowLightDreamManager = LowLightDreamManager(
+ coroutineScope = testScope,
+ dreamManager = mDreamManager,
+ lowLightTransitionCoordinator = mTransitionCoordinator,
+ lowLightDreamComponent = DREAM_COMPONENT,
+ lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS
+ )
+ }
+
+ @Test
+ fun setAmbientLightMode_lowLight_setSystemDream() = testScope.runTest {
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ runCurrent()
+ verify(mDreamManager, never()).setSystemDreamComponent(DREAM_COMPONENT)
+ completeEnterAnimations()
+ runCurrent()
+ verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
+ }
+
+ @Test
+ fun setAmbientLightMode_regularLight_clearSystemDream() = testScope.runTest {
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
+ runCurrent()
+ verify(mDreamManager, never()).setSystemDreamComponent(null)
+ completeExitAnimations()
+ runCurrent()
+ verify(mDreamManager).setSystemDreamComponent(null)
+ }
+
+ @Test
+ fun setAmbientLightMode_defaultUnknownMode_clearSystemDream() = testScope.runTest {
+ // Set to low light first.
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ runCurrent()
+ completeEnterAnimations()
+ runCurrent()
+ verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
+ clearInvocations(mDreamManager)
+
+ // Return to default unknown mode.
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN)
+ runCurrent()
+ completeExitAnimations()
+ runCurrent()
+ verify(mDreamManager).setSystemDreamComponent(null)
+ }
+
+ @Test
+ fun setAmbientLightMode_dreamComponentNotSet_doNothing() = testScope.runTest {
+ val lowLightDreamManager = LowLightDreamManager(
+ coroutineScope = testScope,
+ dreamManager = mDreamManager,
+ lowLightTransitionCoordinator = mTransitionCoordinator,
+ lowLightDreamComponent = null,
+ lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS
+ )
+ lowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ runCurrent()
+ verify(mEnterAnimator, never()).addListener(any())
+ verify(mDreamManager, never()).setSystemDreamComponent(any())
+ }
+
+ @Test
+ fun setAmbientLightMode_multipleTimesBeforeAnimationEnds_cancelsPrevious() = testScope.runTest {
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ runCurrent()
+ // If we reset the light mode back to regular before the previous animation finishes, it
+ // should be ignored.
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
+ runCurrent()
+ completeEnterAnimations()
+ completeExitAnimations()
+ runCurrent()
+ verify(mDreamManager, times(1)).setSystemDreamComponent(null)
+ }
+
+ @Test
+ fun setAmbientLightMode_animatorNeverFinishes_timesOut() = testScope.runTest {
+ mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ advanceTimeBy(delayTimeMillis = LOW_LIGHT_TIMEOUT_MS + 1)
+ // Animation never finishes, but we should still set the system dream
+ verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
+ }
+
+ private fun completeEnterAnimations() {
+ val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
+ listener.onAnimationEnd(mEnterAnimator)
+ }
+
+ private fun completeExitAnimations() {
+ val listener = withArgCaptor { verify(mExitAnimator).addListener(capture()) }
+ listener.onAnimationEnd(mExitAnimator)
+ }
+
+ companion object {
+ private val DREAM_COMPONENT = ComponentName("test_package", "test_dream")
+ private const val LOW_LIGHT_TIMEOUT_MS: Long = 1000
+ }
+} \ No newline at end of file
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java
deleted file mode 100644
index 81e1e33d6220..000000000000
--- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java
+++ /dev/null
@@ -1,113 +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.
- */
-
-package com.android.dream.lowlight;
-
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import android.animation.Animator;
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class LowLightTransitionCoordinatorTest {
- @Mock
- private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener;
-
- @Mock
- private LowLightTransitionCoordinator.LowLightExitListener mExitListener;
-
- @Mock
- private Animator mAnimator;
-
- @Captor
- private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor;
-
- @Mock
- private Runnable mRunnable;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- }
-
- @Test
- public void onEnterCalledOnListeners() {
- LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
-
- coordinator.setLowLightEnterListener(mEnterListener);
-
- coordinator.notifyBeforeLowLightTransition(true, mRunnable);
-
- verify(mEnterListener).onBeforeEnterLowLight();
- verify(mRunnable).run();
- }
-
- @Test
- public void onExitCalledOnListeners() {
- LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
-
- coordinator.setLowLightExitListener(mExitListener);
-
- coordinator.notifyBeforeLowLightTransition(false, mRunnable);
-
- verify(mExitListener).onBeforeExitLowLight();
- verify(mRunnable).run();
- }
-
- @Test
- public void listenerNotCalledAfterRemoval() {
- LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
-
- coordinator.setLowLightEnterListener(mEnterListener);
- coordinator.setLowLightEnterListener(null);
-
- coordinator.notifyBeforeLowLightTransition(true, mRunnable);
-
- verifyZeroInteractions(mEnterListener);
- verify(mRunnable).run();
- }
-
- @Test
- public void runnableCalledAfterAnimationEnds() {
- when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator);
-
- LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
- coordinator.setLowLightEnterListener(mEnterListener);
-
- coordinator.notifyBeforeLowLightTransition(true, mRunnable);
-
- // Animator listener is added and the runnable is not run yet.
- verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
- verifyZeroInteractions(mRunnable);
-
- // Runnable is run once the animation ends.
- mAnimatorListenerCaptor.getValue().onAnimationEnd(null);
- verify(mRunnable).run();
- }
-}
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
new file mode 100644
index 000000000000..4c526a6ac69d
--- /dev/null
+++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.dream.lowlight
+
+import android.animation.Animator
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightEnterListener
+import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightExitListener
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import src.com.android.dream.lowlight.utils.whenever
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class LowLightTransitionCoordinatorTest {
+ @Mock
+ private lateinit var mEnterListener: LowLightEnterListener
+
+ @Mock
+ private lateinit var mExitListener: LowLightExitListener
+
+ @Mock
+ private lateinit var mAnimator: Animator
+
+ @Captor
+ private lateinit var mAnimatorListenerCaptor: ArgumentCaptor<Animator.AnimatorListener>
+
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testScope = TestScope(StandardTestDispatcher())
+ }
+
+ @Test
+ fun onEnterCalledOnListeners() = testScope.runTest {
+ val coordinator = LowLightTransitionCoordinator()
+ coordinator.setLowLightEnterListener(mEnterListener)
+ val job = launch {
+ coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
+ }
+ runCurrent()
+ verify(mEnterListener).onBeforeEnterLowLight()
+ assertThat(job.isCompleted).isTrue()
+ }
+
+ @Test
+ fun onExitCalledOnListeners() = testScope.runTest {
+ val coordinator = LowLightTransitionCoordinator()
+ coordinator.setLowLightExitListener(mExitListener)
+ val job = launch {
+ coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = false)
+ }
+ runCurrent()
+ verify(mExitListener).onBeforeExitLowLight()
+ assertThat(job.isCompleted).isTrue()
+ }
+
+ @Test
+ fun listenerNotCalledAfterRemoval() = testScope.runTest {
+ val coordinator = LowLightTransitionCoordinator()
+ coordinator.setLowLightEnterListener(mEnterListener)
+ coordinator.setLowLightEnterListener(null)
+ val job = launch {
+ coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
+ }
+ runCurrent()
+ verify(mEnterListener, never()).onBeforeEnterLowLight()
+ assertThat(job.isCompleted).isTrue()
+ }
+
+ @Test
+ fun waitsForAnimationToEnd() = testScope.runTest {
+ whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
+ val coordinator = LowLightTransitionCoordinator()
+ coordinator.setLowLightEnterListener(mEnterListener)
+ val job = launch {
+ coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
+ }
+ runCurrent()
+ // Animator listener is added and the runnable is not run yet.
+ verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
+ assertThat(job.isCompleted).isFalse()
+
+ // Runnable is run once the animation ends.
+ mAnimatorListenerCaptor.value.onAnimationEnd(mAnimator)
+ runCurrent()
+ assertThat(job.isCompleted).isTrue()
+ assertThat(job.isCancelled).isFalse()
+ }
+
+ @Test
+ fun waitsForTimeoutIfAnimationNeverEnds() = testScope.runTest {
+ whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
+ val coordinator = LowLightTransitionCoordinator()
+ coordinator.setLowLightEnterListener(mEnterListener)
+ val job = launch {
+ coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
+ }
+ runCurrent()
+ assertThat(job.isCancelled).isFalse()
+ advanceTimeBy(delayTimeMillis = TIMEOUT.inWholeMilliseconds + 1)
+ // If animator doesn't complete within the timeout, we should cancel ourselves.
+ assertThat(job.isCancelled).isTrue()
+ }
+
+ @Test
+ fun shouldCancelIfAnimationIsCancelled() = testScope.runTest {
+ whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
+ val coordinator = LowLightTransitionCoordinator()
+ coordinator.setLowLightEnterListener(mEnterListener)
+ val job = launch {
+ coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
+ }
+ runCurrent()
+ // Animator listener is added and the runnable is not run yet.
+ verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
+ assertThat(job.isCompleted).isFalse()
+ assertThat(job.isCancelled).isFalse()
+
+ // Runnable is run once the animation ends.
+ mAnimatorListenerCaptor.value.onAnimationCancel(mAnimator)
+ runCurrent()
+ assertThat(job.isCompleted).isTrue()
+ assertThat(job.isCancelled).isTrue()
+ }
+
+ @Test
+ fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest {
+ whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
+ val coordinator = LowLightTransitionCoordinator()
+ coordinator.setLowLightEnterListener(mEnterListener)
+ val job = launch {
+ coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
+ }
+ runCurrent()
+ // Animator listener is added and the runnable is not run yet.
+ verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
+ verify(mAnimator, never()).cancel()
+ assertThat(job.isCompleted).isFalse()
+
+ job.cancel()
+ // We should have removed the listener and cancelled the animator
+ verify(mAnimator).removeListener(mAnimatorListenerCaptor.value)
+ verify(mAnimator).cancel()
+ }
+
+ companion object {
+ private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS)
+ }
+}
diff --git a/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt
new file mode 100644
index 000000000000..e5ec26ca4b41
--- /dev/null
+++ b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt
@@ -0,0 +1,125 @@
+package src.com.android.dream.lowlight.utils
+
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatcher
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.Stubber
+
+/**
+ * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
+
+/**
+ * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+inline fun <reified T> any(): T = any(T::class.java)
+
+/**
+ * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher)
+
+/**
+ * Kotlin type-inferred version of Mockito.nullable()
+ */
+inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java)
+
+/**
+ * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
+ * when null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
+ ArgumentCaptor.forClass(T::class.java)
+
+/**
+ * Helper function for creating new mocks, without the need to pass in a [Class] instance.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ *
+ * @param apply builder function to simplify stub configuration by improving type inference.
+ */
+inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java)
+ .apply(apply)
+
+/**
+ * Helper function for stubbing methods without the need to use backticks.
+ *
+ * @see Mockito.when
+ */
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
+fun <T> Stubber.whenever(mock: T): T = `when`(mock)
+
+/**
+ * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
+ * kotlin tests are mocking kotlin objects and the methods take non-null parameters:
+ *
+ * java.lang.NullPointerException: capture() must not be null
+ */
+class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
+ private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz)
+ fun capture(): T = wrapped.capture()
+ val value: T
+ get() = wrapped.value
+ val allValues: List<T>
+ get() = wrapped.allValues
+}
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> =
+ KotlinArgumentCaptor(T::class.java)
+
+/**
+ * Helper function for creating and using a single-use ArgumentCaptor in kotlin.
+ *
+ * val captor = argumentCaptor<Foo>()
+ * verify(...).someMethod(captor.capture())
+ * val captured = captor.value
+ *
+ * becomes:
+ *
+ * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
+ *
+ * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException.
+ */
+inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T =
+ kotlinArgumentCaptor<T>().apply { block() }.value
+
+/**
+ * Variant of [withArgCaptor] for capturing multiple arguments.
+ *
+ * val captor = argumentCaptor<Foo>()
+ * verify(...).someMethod(captor.capture())
+ * val captured: List<Foo> = captor.allValues
+ *
+ * becomes:
+ *
+ * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
+ */
+inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
+ kotlinArgumentCaptor<T>().apply{ block() }.allValues
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 8f022accaccb..db581471e2ca 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -77,7 +77,6 @@ cc_defaults {
target: {
android: {
include_dirs: [
- "external/skia/src/effects",
"external/skia/src/image",
"external/skia/src/utils",
"external/skia/src/gpu",
@@ -130,6 +129,7 @@ cc_defaults {
"libandroidfw",
"libcrypto",
"libsync",
+ "libui",
],
static_libs: [
"libEGL_blobCache",
@@ -337,11 +337,14 @@ cc_defaults {
"jni/android_graphics_Matrix.cpp",
"jni/android_graphics_Picture.cpp",
"jni/android_graphics_DisplayListCanvas.cpp",
+ "jni/android_graphics_Mesh.cpp",
"jni/android_graphics_RenderNode.cpp",
"jni/android_nio_utils.cpp",
"jni/android_util_PathParser.cpp",
"jni/Bitmap.cpp",
+ "jni/BufferUtils.cpp",
+ "jni/HardwareBufferHelpers.cpp",
"jni/BitmapFactory.cpp",
"jni/ByteBufferStreamAdaptor.cpp",
"jni/Camera.cpp",
@@ -350,9 +353,11 @@ cc_defaults {
"jni/CreateJavaOutputStreamAdaptor.cpp",
"jni/FontFamily.cpp",
"jni/FontUtils.cpp",
+ "jni/Gainmap.cpp",
"jni/Graphics.cpp",
"jni/ImageDecoder.cpp",
"jni/Interpolator.cpp",
+ "jni/MeshSpecification.cpp",
"jni/MaskFilter.cpp",
"jni/NinePatch.cpp",
"jni/NinePatchPeeker.cpp",
@@ -360,9 +365,11 @@ cc_defaults {
"jni/PaintFilter.cpp",
"jni/Path.cpp",
"jni/PathEffect.cpp",
+ "jni/PathIterator.cpp",
"jni/PathMeasure.cpp",
"jni/Picture.cpp",
"jni/Region.cpp",
+ "jni/ScopedParcel.cpp",
"jni/Shader.cpp",
"jni/RenderEffect.cpp",
"jni/Typeface.cpp",
@@ -373,27 +380,30 @@ cc_defaults {
"jni/text/LineBreaker.cpp",
"jni/text/MeasuredText.cpp",
"jni/text/TextShaper.cpp",
+ "jni/text/GraphemeBreak.cpp",
],
- header_libs: ["android_graphics_jni_headers"],
+ header_libs: [
+ "android_graphics_jni_headers",
+ "libnativewindow_headers",
+ ],
include_dirs: [
"external/skia/include/private",
"external/skia/src/codec",
"external/skia/src/core",
- "external/skia/src/effects",
- "external/skia/src/image",
- "external/skia/src/images",
],
shared_libs: [
"libbase",
"libcutils",
"libharfbuzz_ng",
+ "libimage_io",
+ "libjpeg",
+ "libultrahdr",
"liblog",
"libminikin",
"libz",
- "libjpeg",
],
static_libs: [
@@ -407,6 +417,7 @@ cc_defaults {
"jni/AnimatedImageDrawable.cpp",
"jni/android_graphics_TextureLayer.cpp",
"jni/android_graphics_HardwareRenderer.cpp",
+ "jni/android_graphics_HardwareBufferRenderer.cpp",
"jni/BitmapRegionDecoder.cpp",
"jni/GIFMovie.cpp",
"jni/GraphicsStatsService.cpp",
@@ -502,6 +513,7 @@ cc_defaults {
"canvas/CanvasOpBuffer.cpp",
"canvas/CanvasOpRasterizer.cpp",
"effects/StretchEffect.cpp",
+ "effects/GainmapRenderer.cpp",
"pipeline/skia/HolePunch.cpp",
"pipeline/skia/SkiaDisplayList.cpp",
"pipeline/skia/SkiaRecordingCanvas.cpp",
@@ -530,9 +542,12 @@ cc_defaults {
"AnimatorManager.cpp",
"CanvasTransform.cpp",
"DamageAccumulator.cpp",
+ "Gainmap.cpp",
"Interpolator.cpp",
"LightingInfo.cpp",
"Matrix.cpp",
+ "Mesh.cpp",
+ "MemoryPolicy.cpp",
"PathParser.cpp",
"Properties.cpp",
"PropertyValuesAnimatorSet.cpp",
@@ -543,6 +558,7 @@ cc_defaults {
"RootRenderNode.cpp",
"SkiaCanvas.cpp",
"SkiaInterpolator.cpp",
+ "Tonemapper.cpp",
"VectorDrawable.cpp",
],
@@ -581,6 +597,7 @@ cc_defaults {
"renderthread/VulkanSurface.cpp",
"renderthread/RenderProxy.cpp",
"renderthread/RenderThread.cpp",
+ "renderthread/HintSessionWrapper.cpp",
"service/GraphicsStatsService.cpp",
"thread/CommonPool.cpp",
"utils/GLUtils.cpp",
diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp
index d0d24a8738f4..cd4fae86aa52 100644
--- a/libs/hwui/CanvasTransform.cpp
+++ b/libs/hwui/CanvasTransform.cpp
@@ -15,19 +15,21 @@
*/
#include "CanvasTransform.h"
-#include "Properties.h"
-#include "utils/Color.h"
+#include <SkAndroidFrameworkUtils.h>
+#include <SkBlendMode.h>
#include <SkColorFilter.h>
#include <SkGradientShader.h>
+#include <SkHighContrastFilter.h>
#include <SkPaint.h>
#include <SkShader.h>
+#include <log/log.h>
#include <algorithm>
#include <cmath>
-#include <log/log.h>
-#include <SkHighContrastFilter.h>
+#include "Properties.h"
+#include "utils/Color.h"
namespace android::uirenderer {
@@ -82,27 +84,21 @@ static void applyColorTransform(ColorTransform transform, SkPaint& paint) {
paint.setColor(newColor);
if (paint.getShader()) {
- SkShader::GradientInfo info;
+ SkAndroidFrameworkUtils::LinearGradientInfo info;
std::array<SkColor, 10> _colorStorage;
std::array<SkScalar, _colorStorage.size()> _offsetStorage;
info.fColorCount = _colorStorage.size();
info.fColors = _colorStorage.data();
info.fColorOffsets = _offsetStorage.data();
- SkShader::GradientType type = paint.getShader()->asAGradient(&info);
-
- if (info.fColorCount <= 10) {
- switch (type) {
- case SkShader::kLinear_GradientType:
- for (int i = 0; i < info.fColorCount; i++) {
- info.fColors[i] = transformColor(transform, info.fColors[i]);
- }
- paint.setShader(SkGradientShader::MakeLinear(info.fPoint, info.fColors,
- info.fColorOffsets, info.fColorCount,
- info.fTileMode, info.fGradientFlags, nullptr));
- break;
- default:break;
- }
+ if (SkAndroidFrameworkUtils::ShaderAsALinearGradient(paint.getShader(), &info) &&
+ info.fColorCount <= _colorStorage.size()) {
+ for (int i = 0; i < info.fColorCount; i++) {
+ info.fColors[i] = transformColor(transform, info.fColors[i]);
+ }
+ paint.setShader(SkGradientShader::MakeLinear(
+ info.fPoints, info.fColors, info.fColorOffsets, info.fColorCount,
+ info.fTileMode, info.fGradientFlags, nullptr));
}
}
diff --git a/libs/hwui/CanvasTransform.h b/libs/hwui/CanvasTransform.h
index c46a2d369974..291f4cf7193b 100644
--- a/libs/hwui/CanvasTransform.h
+++ b/libs/hwui/CanvasTransform.h
@@ -45,4 +45,4 @@ bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette pale
SkColor transformColor(ColorTransform transform, SkColor color);
SkColor transformColorInverse(ColorTransform transform, SkColor color);
-} // namespace android::uirenderer; \ No newline at end of file
+} // namespace android::uirenderer
diff --git a/libs/hwui/ColorMode.h b/libs/hwui/ColorMode.h
index 3df5c3c9caed..959cf742c8e4 100644
--- a/libs/hwui/ColorMode.h
+++ b/libs/hwui/ColorMode.h
@@ -25,9 +25,10 @@ enum class ColorMode {
// WideColorGamut selects the most optimal colorspace & format for the device's display
// Most commonly DisplayP3 + RGBA_8888 currently.
WideColorGamut = 1,
- // HDR Rec2020 + F16
+ // Extended range Display P3
Hdr = 2,
- // HDR Rec2020 + 1010102
+ // Extended range Display P3 10-bit
+ // for test purposes only, not shippable due to insuffient alpha
Hdr10 = 3,
// Alpha 8
A8 = 4,
diff --git a/libs/hwui/CopyRequest.h b/libs/hwui/CopyRequest.h
new file mode 100644
index 000000000000..5fbd5f900716
--- /dev/null
+++ b/libs/hwui/CopyRequest.h
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "Rect.h"
+#include "hwui/Bitmap.h"
+
+namespace android::uirenderer {
+
+// Keep in sync with PixelCopy.java codes
+enum class CopyResult {
+ Success = 0,
+ UnknownError = 1,
+ Timeout = 2,
+ SourceEmpty = 3,
+ SourceInvalid = 4,
+ DestinationInvalid = 5,
+};
+
+struct CopyRequest {
+ Rect srcRect;
+ CopyRequest(Rect srcRect) : srcRect(srcRect) {}
+ virtual ~CopyRequest() {}
+ virtual SkBitmap getDestinationBitmap(int srcWidth, int srcHeight) = 0;
+ virtual void onCopyFinished(CopyResult result) = 0;
+};
+
+} // namespace android::uirenderer
diff --git a/libs/hwui/DamageAccumulator.cpp b/libs/hwui/DamageAccumulator.cpp
index 9db47c3ba090..a8d170d00ef7 100644
--- a/libs/hwui/DamageAccumulator.cpp
+++ b/libs/hwui/DamageAccumulator.cpp
@@ -207,27 +207,6 @@ static void applyTransforms(DirtyStack* frame, DirtyStack* end) {
}
}
-static void computeTransformImpl(const DirtyStack* frame, const DirtyStack* end,
- Matrix4* outMatrix) {
- while (frame != end) {
- switch (frame->type) {
- case TransformRenderNode:
- frame->renderNode->applyViewPropertyTransforms(*outMatrix);
- break;
- case TransformMatrix4:
- outMatrix->multiply(*frame->matrix4);
- break;
- case TransformNone:
- // nothing to be done
- break;
- default:
- LOG_ALWAYS_FATAL("Tried to compute transform with an invalid type: %d",
- frame->type);
- }
- frame = frame->prev;
- }
-}
-
void DamageAccumulator::applyRenderNodeTransform(DirtyStack* frame) {
if (frame->pendingDirty.isEmpty()) {
return;
@@ -282,9 +261,6 @@ void DamageAccumulator::finish(SkRect* totalDirty) {
DamageAccumulator::StretchResult DamageAccumulator::findNearestStretchEffect() const {
DirtyStack* frame = mHead;
- const auto& headProperties = mHead->renderNode->properties();
- float startWidth = headProperties.getWidth();
- float startHeight = headProperties.getHeight();
while (frame->prev != frame) {
if (frame->type == TransformRenderNode) {
const auto& renderNode = frame->renderNode;
@@ -295,21 +271,16 @@ DamageAccumulator::StretchResult DamageAccumulator::findNearestStretchEffect() c
const float height = (float) frameRenderNodeProperties.getHeight();
if (!effect.isEmpty()) {
Matrix4 stretchMatrix;
- computeTransformImpl(mHead, frame, &stretchMatrix);
- Rect stretchRect = Rect(0.f, 0.f, startWidth, startHeight);
+ computeTransformImpl(frame, &stretchMatrix);
+ Rect stretchRect = Rect(0.f, 0.f, width, height);
stretchMatrix.mapRect(stretchRect);
return StretchResult{
- .stretchEffect = &effect,
- .childRelativeBounds = SkRect::MakeLTRB(
- stretchRect.left,
- stretchRect.top,
- stretchRect.right,
- stretchRect.bottom
- ),
- .width = width,
- .height = height
- };
+ .stretchEffect = &effect,
+ .parentBounds = SkRect::MakeLTRB(stretchRect.left, stretchRect.top,
+ stretchRect.right, stretchRect.bottom),
+ .width = width,
+ .height = height};
}
}
frame = frame->prev;
diff --git a/libs/hwui/DamageAccumulator.h b/libs/hwui/DamageAccumulator.h
index 90a35174d929..c4249af392d3 100644
--- a/libs/hwui/DamageAccumulator.h
+++ b/libs/hwui/DamageAccumulator.h
@@ -70,9 +70,9 @@ public:
const StretchEffect* stretchEffect;
/**
- * Bounds of the child relative to the stretch container
+ * Bounds of the stretching container
*/
- const SkRect childRelativeBounds;
+ const SkRect parentBounds;
/**
* Width of the stretch container
diff --git a/libs/hwui/DeferredLayerUpdater.h b/libs/hwui/DeferredLayerUpdater.h
index 9a4c5505fa35..a7f8f6189a8e 100644
--- a/libs/hwui/DeferredLayerUpdater.h
+++ b/libs/hwui/DeferredLayerUpdater.h
@@ -16,6 +16,7 @@
#pragma once
+#include <SkBlendMode.h>
#include <SkColorFilter.h>
#include <SkImage.h>
#include <SkMatrix.h>
diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp
index 07594715a84c..32bc122fdc58 100644
--- a/libs/hwui/DeviceInfo.cpp
+++ b/libs/hwui/DeviceInfo.cpp
@@ -93,15 +93,25 @@ void DeviceInfo::setWideColorDataspace(ADataSpace dataspace) {
case ADATASPACE_SCRGB:
get()->mWideColorSpace = SkColorSpace::MakeSRGB();
break;
+ default:
+ ALOGW("Unknown dataspace %d", dataspace);
+ // Treat unknown dataspaces as sRGB, so fall through
+ [[fallthrough]];
case ADATASPACE_SRGB:
// when sRGB is returned, it means wide color gamut is not supported.
get()->mWideColorSpace = SkColorSpace::MakeSRGB();
break;
- default:
- LOG_ALWAYS_FATAL("Unreachable: unsupported wide color space.");
}
}
+void DeviceInfo::setSupportFp16ForHdr(bool supportFp16ForHdr) {
+ get()->mSupportFp16ForHdr = supportFp16ForHdr;
+}
+
+void DeviceInfo::setSupportMixedColorSpaces(bool supportMixedColorSpaces) {
+ get()->mSupportMixedColorSpaces = supportMixedColorSpaces;
+}
+
void DeviceInfo::onRefreshRateChanged(int64_t vsyncPeriod) {
mVsyncPeriod = vsyncPeriod;
}
diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h
index d5fee3f667a9..d4af0872e31e 100644
--- a/libs/hwui/DeviceInfo.h
+++ b/libs/hwui/DeviceInfo.h
@@ -16,7 +16,9 @@
#ifndef DEVICEINFO_H
#define DEVICEINFO_H
+#include <SkColorSpace.h>
#include <SkImageInfo.h>
+#include <SkRefCnt.h>
#include <android/data_space.h>
#include <mutex>
@@ -57,6 +59,12 @@ public:
}
static void setWideColorDataspace(ADataSpace dataspace);
+ static void setSupportFp16ForHdr(bool supportFp16ForHdr);
+ static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; };
+
+ static void setSupportMixedColorSpaces(bool supportMixedColorSpaces);
+ static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; };
+
// this value is only valid after the GPU has been initialized and there is a valid graphics
// context or if you are using the HWUI_NULL_GPU
int maxTextureSize() const;
@@ -86,6 +94,8 @@ private:
int mMaxTextureSize;
sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB();
+ bool mSupportFp16ForHdr = false;
+ bool mSupportMixedColorSpaces = false;
SkColorType mWideColorType = SkColorType::kN32_SkColorType;
int mDisplaysSize = 0;
int mPhysicalDisplayIndex = -1;
diff --git a/libs/hwui/DisplayListOps.in b/libs/hwui/DisplayListOps.in
index 4ec782f6fec0..a18ba1c633b9 100644
--- a/libs/hwui/DisplayListOps.in
+++ b/libs/hwui/DisplayListOps.in
@@ -52,3 +52,5 @@ X(DrawShadowRec)
X(DrawVectorDrawable)
X(DrawRippleDrawable)
X(DrawWebView)
+X(DrawSkMesh)
+X(DrawMesh) \ No newline at end of file
diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h
index 564ee4f53a54..b15b6cb9a9ec 100644
--- a/libs/hwui/FrameInfo.h
+++ b/libs/hwui/FrameInfo.h
@@ -104,6 +104,7 @@ public:
set(FrameInfoIndex::AnimationStart) = vsyncTime;
set(FrameInfoIndex::PerformTraversalsStart) = vsyncTime;
set(FrameInfoIndex::DrawStart) = vsyncTime;
+ set(FrameInfoIndex::FrameStartTime) = vsyncTime;
set(FrameInfoIndex::FrameDeadline) = frameDeadline;
set(FrameInfoIndex::FrameInterval) = frameInterval;
return *this;
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt b/libs/hwui/Gainmap.cpp
index 6695c17ed514..30f401ef5f01 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt
+++ b/libs/hwui/Gainmap.cpp
@@ -1,5 +1,5 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
+/**
+ * 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.
@@ -13,21 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+#include "Gainmap.h"
-package com.android.wm.shell.flicker.helpers
+namespace android::uirenderer {
-import android.app.Instrumentation
-import com.android.server.wm.traces.parser.toFlickerComponent
-import com.android.wm.shell.flicker.testapp.Components
-
-class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper(
- instrumentation,
- Components.LaunchBubbleActivity.LABEL,
- Components.LaunchBubbleActivity.COMPONENT.toFlickerComponent()
-) {
-
- companion object {
- const val TEST_REPETITIONS = 1
- const val TIMEOUT_MS = 3_000L
+sp<Gainmap> Gainmap::allocateHardwareGainmap(const sp<Gainmap>& srcGainmap) {
+ auto gainmap = sp<Gainmap>::make();
+ gainmap->info = srcGainmap->info;
+ const SkBitmap skSrcBitmap = srcGainmap->bitmap->getSkBitmap();
+ sk_sp<Bitmap> skBitmap(Bitmap::allocateHardwareBitmap(skSrcBitmap));
+ if (!skBitmap.get()) {
+ return nullptr;
}
+ gainmap->bitmap = std::move(skBitmap);
+ return gainmap;
}
+
+} // namespace android::uirenderer \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java b/libs/hwui/Gainmap.h
index baa1e6fdd1e9..3bc183ab854f 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SplitScreenSecondaryActivity.java
+++ b/libs/hwui/Gainmap.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,15 +14,20 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker.testapp;
+#pragma once
-import android.app.Activity;
-import android.os.Bundle;
+#include <SkGainmapInfo.h>
+#include <SkImage.h>
+#include <hwui/Bitmap.h>
+#include <utils/LightRefBase.h>
-public class SplitScreenSecondaryActivity extends Activity {
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- setContentView(R.layout.activity_splitscreen_secondary);
- }
-}
+namespace android::uirenderer {
+
+class Gainmap : public LightRefBase<Gainmap> {
+public:
+ SkGainmapInfo info;
+ sk_sp<Bitmap> bitmap;
+ static sp<Gainmap> allocateHardwareGainmap(const sp<Gainmap>& srcGainmap);
+};
+
+} // namespace android::uirenderer
diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp
index c24cabb287de..b7e99994355c 100644
--- a/libs/hwui/HardwareBitmapUploader.cpp
+++ b/libs/hwui/HardwareBitmapUploader.cpp
@@ -22,8 +22,11 @@
#include <GLES2/gl2ext.h>
#include <GLES3/gl3.h>
#include <GrDirectContext.h>
+#include <SkBitmap.h>
#include <SkCanvas.h>
#include <SkImage.h>
+#include <SkImageInfo.h>
+#include <SkRefCnt.h>
#include <gui/TraceUtils.h>
#include <utils/GLUtils.h>
#include <utils/NdkUtils.h>
@@ -39,6 +42,8 @@
namespace android::uirenderer {
+static constexpr auto kThreadTimeout = 60000_ms;
+
class AHBUploader;
// This helper uploader classes allows us to upload using either EGL or Vulkan using the same
// interface.
@@ -77,7 +82,7 @@ public:
}
void postIdleTimeoutCheck() {
- mUploadThread->queue().postDelayed(5000_ms, [this](){ this->idleTimeoutCheck(); });
+ mUploadThread->queue().postDelayed(kThreadTimeout, [this]() { this->idleTimeoutCheck(); });
}
protected:
@@ -94,7 +99,7 @@ private:
bool shouldTimeOutLocked() {
nsecs_t durationSince = systemTime() - mLastUpload;
- return durationSince > 2000_ms;
+ return durationSince > kThreadTimeout;
}
void idleTimeoutCheck() {
diff --git a/libs/hwui/HardwareBitmapUploader.h b/libs/hwui/HardwareBitmapUploader.h
index 81057a24c29c..00ee99648889 100644
--- a/libs/hwui/HardwareBitmapUploader.h
+++ b/libs/hwui/HardwareBitmapUploader.h
@@ -17,6 +17,9 @@
#pragma once
#include <hwui/Bitmap.h>
+#include <SkRefCnt.h>
+
+class SkBitmap;
namespace android::uirenderer {
diff --git a/libs/hwui/Layer.cpp b/libs/hwui/Layer.cpp
index 9053c1240957..fc3118ae32dd 100644
--- a/libs/hwui/Layer.cpp
+++ b/libs/hwui/Layer.cpp
@@ -20,6 +20,8 @@
#include "utils/Color.h"
#include "utils/MathUtils.h"
+#include <SkBlendMode.h>
+
#include <log/log.h>
namespace android {
diff --git a/libs/hwui/MemoryPolicy.cpp b/libs/hwui/MemoryPolicy.cpp
new file mode 100644
index 000000000000..ca1312e75f4c
--- /dev/null
+++ b/libs/hwui/MemoryPolicy.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+#include "MemoryPolicy.h"
+
+#include <android-base/properties.h>
+
+#include <optional>
+#include <string_view>
+
+#include "Properties.h"
+
+namespace android::uirenderer {
+
+constexpr static MemoryPolicy sDefaultMemoryPolicy;
+constexpr static MemoryPolicy sPersistentOrSystemPolicy{
+ .contextTimeout = 10_s,
+ .useAlternativeUiHidden = true,
+};
+constexpr static MemoryPolicy sLowRamPolicy{
+ .useAlternativeUiHidden = true,
+ .purgeScratchOnly = false,
+};
+constexpr static MemoryPolicy sExtremeLowRam{
+ .initialMaxSurfaceAreaScale = 0.2f,
+ .surfaceSizeMultiplier = 5 * 4.0f,
+ .backgroundRetentionPercent = 0.2f,
+ .contextTimeout = 5_s,
+ .minimumResourceRetention = 1_s,
+ .useAlternativeUiHidden = true,
+ .purgeScratchOnly = false,
+ .releaseContextOnStoppedOnly = true,
+};
+
+const MemoryPolicy& loadMemoryPolicy() {
+ if (Properties::isSystemOrPersistent) {
+ return sPersistentOrSystemPolicy;
+ }
+ std::string memoryPolicy = base::GetProperty(PROPERTY_MEMORY_POLICY, "");
+ if (memoryPolicy == "default") {
+ return sDefaultMemoryPolicy;
+ }
+ if (memoryPolicy == "lowram") {
+ return sLowRamPolicy;
+ }
+ if (memoryPolicy == "extremelowram") {
+ return sExtremeLowRam;
+ }
+
+ if (Properties::isLowRam) {
+ return sLowRamPolicy;
+ }
+ return sDefaultMemoryPolicy;
+}
+
+} // namespace android::uirenderer \ No newline at end of file
diff --git a/libs/hwui/MemoryPolicy.h b/libs/hwui/MemoryPolicy.h
new file mode 100644
index 000000000000..347daf34f52a
--- /dev/null
+++ b/libs/hwui/MemoryPolicy.h
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "utils/TimeUtils.h"
+
+namespace android::uirenderer {
+
+// Values mirror those from ComponentCallbacks2.java
+enum class TrimLevel {
+ COMPLETE = 80,
+ MODERATE = 60,
+ BACKGROUND = 40,
+ UI_HIDDEN = 20,
+ RUNNING_CRITICAL = 15,
+ RUNNING_LOW = 10,
+ RUNNING_MODERATE = 5,
+};
+
+enum class CacheTrimLevel {
+ ALL_CACHES = 0,
+ FONT_CACHE = 1,
+ RESOURCE_CACHE = 2,
+};
+
+struct MemoryPolicy {
+ // The initial scale factor applied to the display resolution. The default is 1, but
+ // lower values may be used to start with a smaller initial cache size. The cache will
+ // be adjusted if larger frames are actually rendered
+ float initialMaxSurfaceAreaScale = 1.0f;
+ // The foreground cache size multiplier. The surface area of the screen will be multiplied
+ // by this
+ float surfaceSizeMultiplier = 12.0f * 4.0f;
+ // How much of the foreground cache size should be preserved when going into the background
+ float backgroundRetentionPercent = 0.5f;
+ // How long after the last renderer goes away before the GPU context is released. A value
+ // of 0 means only drop the context on background TRIM signals
+ nsecs_t contextTimeout = 10_s;
+ // The minimum amount of time to hold onto items in the resource cache
+ // The actual time used will be the max of this & when frames were actually rendered
+ nsecs_t minimumResourceRetention = 10_s;
+ // If false, use only TRIM_UI_HIDDEN to drive background cache limits;
+ // If true, use all signals (such as all contexts are stopped) to drive the limits
+ bool useAlternativeUiHidden = true;
+ // Whether or not to only purge scratch resources when triggering UI Hidden or background
+ // collection
+ bool purgeScratchOnly = true;
+ // EXPERIMENTAL: Whether or not to trigger releasing GPU context when all contexts are stopped
+ // WARNING: Enabling this option can lead to instability, see b/266626090
+ bool releaseContextOnStoppedOnly = false;
+};
+
+const MemoryPolicy& loadMemoryPolicy();
+
+} // namespace android::uirenderer \ No newline at end of file
diff --git a/libs/hwui/Mesh.cpp b/libs/hwui/Mesh.cpp
new file mode 100644
index 000000000000..e59bc9565a59
--- /dev/null
+++ b/libs/hwui/Mesh.cpp
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+#include "Mesh.h"
+
+#include <GLES/gl.h>
+#include <SkMesh.h>
+
+#include "SafeMath.h"
+
+static size_t min_vcount_for_mode(SkMesh::Mode mode) {
+ switch (mode) {
+ case SkMesh::Mode::kTriangles:
+ return 3;
+ case SkMesh::Mode::kTriangleStrip:
+ return 3;
+ }
+}
+
+// Re-implementation of SkMesh::validate to validate user side that their mesh is valid.
+std::tuple<bool, SkString> Mesh::validate() {
+#define FAIL_MESH_VALIDATE(...) return std::make_tuple(false, SkStringPrintf(__VA_ARGS__))
+ if (!mMeshSpec) {
+ FAIL_MESH_VALIDATE("MeshSpecification is required.");
+ }
+ if (mVertexBufferData.empty()) {
+ FAIL_MESH_VALIDATE("VertexBuffer is required.");
+ }
+
+ auto meshStride = mMeshSpec->stride();
+ auto meshMode = SkMesh::Mode(mMode);
+ SafeMath sm;
+ size_t vsize = sm.mul(meshStride, mVertexCount);
+ if (sm.add(vsize, mVertexOffset) > mVertexBufferData.size()) {
+ FAIL_MESH_VALIDATE(
+ "The vertex buffer offset and vertex count reads beyond the end of the"
+ " vertex buffer.");
+ }
+
+ if (mVertexOffset % meshStride != 0) {
+ FAIL_MESH_VALIDATE("The vertex offset (%zu) must be a multiple of the vertex stride (%zu).",
+ mVertexOffset, meshStride);
+ }
+
+ if (size_t uniformSize = mMeshSpec->uniformSize()) {
+ if (!mBuilder->fUniforms || mBuilder->fUniforms->size() < uniformSize) {
+ FAIL_MESH_VALIDATE("The uniform data is %zu bytes but must be at least %zu.",
+ mBuilder->fUniforms->size(), uniformSize);
+ }
+ }
+
+ auto modeToStr = [](SkMesh::Mode m) {
+ switch (m) {
+ case SkMesh::Mode::kTriangles:
+ return "triangles";
+ case SkMesh::Mode::kTriangleStrip:
+ return "triangle-strip";
+ }
+ };
+ if (!mIndexBufferData.empty()) {
+ if (mIndexCount < min_vcount_for_mode(meshMode)) {
+ FAIL_MESH_VALIDATE("%s mode requires at least %zu indices but index count is %zu.",
+ modeToStr(meshMode), min_vcount_for_mode(meshMode), mIndexCount);
+ }
+ size_t isize = sm.mul(sizeof(uint16_t), mIndexCount);
+ if (sm.add(isize, mIndexOffset) > mIndexBufferData.size()) {
+ FAIL_MESH_VALIDATE(
+ "The index buffer offset and index count reads beyond the end of the"
+ " index buffer.");
+ }
+ // If we allow 32 bit indices then this should enforce 4 byte alignment in that case.
+ if (!SkIsAlign2(mIndexOffset)) {
+ FAIL_MESH_VALIDATE("The index offset must be a multiple of 2.");
+ }
+ } else {
+ if (mVertexCount < min_vcount_for_mode(meshMode)) {
+ 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);
+ }
+
+ if (!sm.ok()) {
+ FAIL_MESH_VALIDATE("Overflow");
+ }
+#undef FAIL_MESH_VALIDATE
+ return {true, {}};
+}
diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h
new file mode 100644
index 000000000000..13e3c8e7bf77
--- /dev/null
+++ b/libs/hwui/Mesh.h
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+#ifndef MESH_H_
+#define MESH_H_
+
+#include <GrDirectContext.h>
+#include <SkMesh.h>
+#include <jni.h>
+#include <log/log.h>
+
+#include <utility>
+
+class MeshUniformBuilder {
+public:
+ struct MeshUniform {
+ template <typename T>
+ std::enable_if_t<std::is_trivially_copyable<T>::value, MeshUniform> operator=(
+ const T& val) {
+ if (!fVar) {
+ LOG_FATAL("Assigning to missing variable");
+ } else if (sizeof(val) != fVar->sizeInBytes()) {
+ LOG_FATAL("Incorrect value size");
+ } else {
+ void* dst = reinterpret_cast<void*>(
+ reinterpret_cast<uint8_t*>(fOwner->writableUniformData()) + fVar->offset);
+ memcpy(dst, &val, sizeof(val));
+ }
+ }
+
+ MeshUniform& operator=(const SkMatrix& val) {
+ if (!fVar) {
+ LOG_FATAL("Assigning to missing variable");
+ } else if (fVar->sizeInBytes() != 9 * sizeof(float)) {
+ LOG_FATAL("Incorrect value size");
+ } else {
+ float* data = reinterpret_cast<float*>(
+ reinterpret_cast<uint8_t*>(fOwner->writableUniformData()) + fVar->offset);
+ data[0] = val.get(0);
+ data[1] = val.get(3);
+ data[2] = val.get(6);
+ data[3] = val.get(1);
+ data[4] = val.get(4);
+ data[5] = val.get(7);
+ data[6] = val.get(2);
+ data[7] = val.get(5);
+ data[8] = val.get(8);
+ }
+ return *this;
+ }
+
+ template <typename T>
+ bool set(const T val[], const int count) {
+ static_assert(std::is_trivially_copyable<T>::value, "Value must be trivial copyable");
+ if (!fVar) {
+ LOG_FATAL("Assigning to missing variable");
+ return false;
+ } else if (sizeof(T) * count != fVar->sizeInBytes()) {
+ LOG_FATAL("Incorrect value size");
+ return false;
+ } else {
+ void* dst = reinterpret_cast<void*>(
+ reinterpret_cast<uint8_t*>(fOwner->writableUniformData()) + fVar->offset);
+ memcpy(dst, val, sizeof(T) * count);
+ }
+ return true;
+ }
+
+ MeshUniformBuilder* fOwner;
+ const SkRuntimeEffect::Uniform* fVar;
+ };
+ MeshUniform uniform(std::string_view name) { return {this, fMeshSpec->findUniform(name)}; }
+
+ explicit MeshUniformBuilder(sk_sp<SkMeshSpecification> meshSpec) {
+ fMeshSpec = sk_sp(meshSpec);
+ fUniforms = (SkData::MakeZeroInitialized(meshSpec->uniformSize()));
+ }
+
+ sk_sp<SkData> fUniforms;
+
+private:
+ void* writableUniformData() {
+ if (!fUniforms->unique()) {
+ fUniforms = SkData::MakeWithCopy(fUniforms->data(), fUniforms->size());
+ }
+ return fUniforms->writable_data();
+ }
+
+ sk_sp<SkMeshSpecification> fMeshSpec;
+};
+
+class Mesh {
+public:
+ Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode,
+ std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset,
+ std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds)
+ : mMeshSpec(meshSpec)
+ , mMode(mode)
+ , mVertexBufferData(std::move(vertexBufferData))
+ , mVertexCount(vertexCount)
+ , mVertexOffset(vertexOffset)
+ , mBuilder(std::move(builder))
+ , mBounds(bounds) {}
+
+ Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode,
+ std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset,
+ std::vector<uint8_t>&& indexBuffer, jint indexCount, jint indexOffset,
+ std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds)
+ : mMeshSpec(meshSpec)
+ , mMode(mode)
+ , mVertexBufferData(std::move(vertexBufferData))
+ , mVertexCount(vertexCount)
+ , mVertexOffset(vertexOffset)
+ , mIndexBufferData(std::move(indexBuffer))
+ , mIndexCount(indexCount)
+ , mIndexOffset(indexOffset)
+ , mBuilder(std::move(builder))
+ , mBounds(bounds) {}
+
+ Mesh(Mesh&&) = default;
+
+ Mesh& operator=(Mesh&&) = default;
+
+ [[nodiscard]] std::tuple<bool, SkString> validate();
+
+ void updateSkMesh(GrDirectContext* context) const {
+ GrDirectContext::DirectContextID genId = GrDirectContext::DirectContextID();
+ if (context) {
+ genId = context->directContextID();
+ }
+
+ if (mIsDirty || genId != mGenerationId) {
+ auto vb = SkMesh::MakeVertexBuffer(
+ context, reinterpret_cast<const void*>(mVertexBufferData.data()),
+ mVertexBufferData.size());
+ auto meshMode = SkMesh::Mode(mMode);
+ if (!mIndexBufferData.empty()) {
+ auto ib = SkMesh::MakeIndexBuffer(
+ context, reinterpret_cast<const void*>(mIndexBufferData.data()),
+ mIndexBufferData.size());
+ mMesh = SkMesh::MakeIndexed(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset,
+ ib, mIndexCount, mIndexOffset, mBuilder->fUniforms,
+ mBounds)
+ .mesh;
+ } else {
+ mMesh = SkMesh::Make(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset,
+ mBuilder->fUniforms, mBounds)
+ .mesh;
+ }
+ mIsDirty = false;
+ mGenerationId = genId;
+ }
+ }
+
+ SkMesh& getSkMesh() const {
+ LOG_FATAL_IF(mIsDirty,
+ "Attempt to obtain SkMesh when Mesh is dirty, did you "
+ "forget to call updateSkMesh with a GrDirectContext? "
+ "Defensively creating a CPU mesh");
+ return mMesh;
+ }
+
+ void markDirty() { mIsDirty = true; }
+
+ MeshUniformBuilder* uniformBuilder() { return mBuilder.get(); }
+
+private:
+ sk_sp<SkMeshSpecification> mMeshSpec;
+ int mMode = 0;
+
+ std::vector<uint8_t> mVertexBufferData;
+ size_t mVertexCount = 0;
+ size_t mVertexOffset = 0;
+
+ std::vector<uint8_t> mIndexBufferData;
+ size_t mIndexCount = 0;
+ size_t mIndexOffset = 0;
+
+ std::unique_ptr<MeshUniformBuilder> mBuilder;
+ SkRect mBounds{};
+
+ mutable SkMesh mMesh{};
+ mutable bool mIsDirty = true;
+ mutable GrDirectContext::DirectContextID mGenerationId = GrDirectContext::DirectContextID();
+};
+#endif // MESH_H_
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 738f1ab0c18f..06aed63d8def 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -17,7 +17,6 @@
#include "Properties.h"
#include "Debug.h"
-#include "log/log_main.h"
#ifdef __ANDROID__
#include "HWUIProperties.sysprop.h"
#endif
@@ -82,11 +81,17 @@ bool Properties::isolatedProcess = false;
int Properties::contextPriority = 0;
float Properties::defaultSdrWhitePoint = 200.f;
-bool Properties::useHintManager = true;
+bool Properties::useHintManager = false;
int Properties::targetCpuTimePercentage = 70;
bool Properties::enableWebViewOverlays = true;
+bool Properties::isHighEndGfx = true;
+bool Properties::isLowRam = false;
+bool Properties::isSystemOrPersistent = false;
+
+float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number
+
StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI;
DrawingEnabled Properties::drawingEnabled = DrawingEnabled::NotInitialized;
@@ -134,16 +139,23 @@ bool Properties::load() {
skpCaptureEnabled = debuggingEnabled && base::GetBoolProperty(PROPERTY_CAPTURE_SKP_ENABLED, false);
SkAndroidFrameworkTraceUtil::setEnableTracing(
- base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, false));
+ base::GetBoolProperty(PROPERTY_SKIA_TRACING_ENABLED, false));
+ SkAndroidFrameworkTraceUtil::setUsePerfettoTrackEvents(
+ base::GetBoolProperty(PROPERTY_SKIA_USE_PERFETTO_TRACK_EVENTS, false));
runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false);
- useHintManager = base::GetBoolProperty(PROPERTY_USE_HINT_MANAGER, true);
+ useHintManager = base::GetBoolProperty(PROPERTY_USE_HINT_MANAGER, false);
targetCpuTimePercentage = base::GetIntProperty(PROPERTY_TARGET_CPU_TIME_PERCENTAGE, 70);
if (targetCpuTimePercentage <= 0 || targetCpuTimePercentage > 100) targetCpuTimePercentage = 70;
enableWebViewOverlays = base::GetBoolProperty(PROPERTY_WEBVIEW_OVERLAYS_ENABLED, true);
+ auto hdrHeadroom = (float)atof(base::GetProperty(PROPERTY_8BIT_HDR_HEADROOM, "").c_str());
+ if (hdrHeadroom >= 1.f) {
+ maxHdrHeadroomOn8bit = std::min(hdrHeadroom, 100.f);
+ }
+
// call isDrawingEnabled to force loading of the property
isDrawingEnabled();
@@ -207,12 +219,15 @@ RenderPipelineType Properties::getRenderPipelineType() {
return sRenderPipelineType;
}
-void Properties::overrideRenderPipelineType(RenderPipelineType type, bool inUnitTest) {
+void Properties::overrideRenderPipelineType(RenderPipelineType type) {
// If we're doing actual rendering then we can't change the renderer after it's been set.
- // Unit tests can freely change this as often as it wants.
- LOG_ALWAYS_FATAL_IF(sRenderPipelineType != RenderPipelineType::NotInitialized &&
- sRenderPipelineType != type && !inUnitTest,
- "Trying to change pipeline but it's already set.");
+ // Unit tests can freely change this as often as it wants, though, as there's no actual
+ // GL rendering happening
+ if (sRenderPipelineType != RenderPipelineType::NotInitialized) {
+ LOG_ALWAYS_FATAL_IF(sRenderPipelineType != type,
+ "Trying to change pipeline but it's already set");
+ return;
+ }
sRenderPipelineType = type;
}
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index 2f8c67903a8b..bb477449fff0 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -143,9 +143,32 @@ enum DebugLevel {
#define PROPERTY_CAPTURE_SKP_ENABLED "debug.hwui.capture_skp_enabled"
/**
- * Allows to record Skia drawing commands with systrace.
+ * Allows broad recording of Skia drawing commands.
+ *
+ * If disabled, a very minimal set of trace events *may* be recorded.
+ * If enabled, a much broader set of trace events *may* be recorded.
+ *
+ * In either case, trace events are only recorded if an appropriately configured tracing session is
+ * active.
+ *
+ * Use debug.hwui.skia_use_perfetto_track_events to determine if ATrace (default) or Perfetto is
+ * used as the tracing backend.
+ */
+#define PROPERTY_SKIA_TRACING_ENABLED "debug.hwui.skia_tracing_enabled"
+
+/**
+ * Switches Skia's tracing to use Perfetto's Track Event system instead of ATrace.
+ *
+ * If disabled, ATrace will be used by default, which will record trace events from any of Skia's
+ * tracing categories if overall system tracing is active and the "gfx" and "view" ATrace categories
+ * are enabled.
+ *
+ * If enabled, then Perfetto's Track Event system will be used instead, which will only record if an
+ * active Perfetto tracing session is targeting the correct apps and Skia tracing categories with
+ * the Track Event data source enabled. This approach may be used to selectively filter out
+ * undesired Skia tracing categories, and events will contain more data fields.
*/
-#define PROPERTY_SKIA_ATRACE_ENABLED "debug.hwui.skia_atrace_enabled"
+#define PROPERTY_SKIA_USE_PERFETTO_TRACK_EVENTS "debug.hwui.skia_use_perfetto_track_events"
/**
* Defines how many frames in a sequence to capture.
@@ -193,6 +216,10 @@ enum DebugLevel {
*/
#define PROPERTY_DRAWING_ENABLED "debug.hwui.drawing_enabled"
+#define PROPERTY_MEMORY_POLICY "debug.hwui.app_memory_policy"
+
+#define PROPERTY_8BIT_HDR_HEADROOM "debug.hwui.8bit_hdr_headroom"
+
///////////////////////////////////////////////////////////////////////////////
// Misc
///////////////////////////////////////////////////////////////////////////////
@@ -276,7 +303,7 @@ public:
static bool enableRTAnimations;
// Used for testing only to change the render pipeline.
- static void overrideRenderPipelineType(RenderPipelineType, bool inUnitTest = false);
+ static void overrideRenderPipelineType(RenderPipelineType);
static bool runningInEmulator;
@@ -292,16 +319,29 @@ public:
static bool enableWebViewOverlays;
+ static bool isHighEndGfx;
+ static bool isLowRam;
+ static bool isSystemOrPersistent;
+
+ static float maxHdrHeadroomOn8bit;
+
static StretchEffectBehavior getStretchEffectBehavior() {
return stretchEffectBehavior;
}
static void setIsHighEndGfx(bool isHighEndGfx) {
+ Properties::isHighEndGfx = isHighEndGfx;
stretchEffectBehavior = isHighEndGfx ?
StretchEffectBehavior::ShaderHWUI :
StretchEffectBehavior::UniformScale;
}
+ static void setIsLowRam(bool isLowRam) { Properties::isLowRam = isLowRam; }
+
+ static void setIsSystemOrPersistent(bool isSystemOrPersistent) {
+ Properties::isSystemOrPersistent = isSystemOrPersistent;
+ }
+
/**
* Used for testing. Typical configuration of stretch behavior is done
* through setIsHighEndGfx
diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp
index a3ba88e4ee8a..045de35c1d97 100644
--- a/libs/hwui/Readback.cpp
+++ b/libs/hwui/Readback.cpp
@@ -16,12 +16,28 @@
#include "Readback.h"
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkCanvas.h>
+#include <SkColorSpace.h>
+#include <SkImage.h>
+#include <SkImageInfo.h>
+#include <SkMatrix.h>
+#include <SkPaint.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
+#include <SkSamplingOptions.h>
+#include <SkSurface.h>
+#include "include/gpu/GpuTypes.h" // from Skia
+#include <gui/TraceUtils.h>
+#include <private/android/AHardwareBufferHelpers.h>
+#include <shaders/shaders.h>
#include <sync/sync.h>
#include <system/window.h>
-#include <gui/TraceUtils.h>
#include "DeferredLayerUpdater.h"
#include "Properties.h"
+#include "Tonemapper.h"
#include "hwui/Bitmap.h"
#include "pipeline/skia/LayerDrawable.h"
#include "renderthread/EglManager.h"
@@ -37,8 +53,7 @@ namespace uirenderer {
#define ARECT_ARGS(r) float((r).left), float((r).top), float((r).right), float((r).bottom)
-CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRect,
- SkBitmap* bitmap) {
+void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<CopyRequest>& request) {
ATRACE_CALL();
// Setup the source
AHardwareBuffer* rawSourceBuffer;
@@ -51,44 +66,57 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec
// Really this shouldn't ever happen, but better safe than sorry.
if (err == UNKNOWN_TRANSACTION) {
ALOGW("Readback failed to ANativeWindow_getLastQueuedBuffer2 - who are we talking to?");
- return copySurfaceIntoLegacy(window, inSrcRect, bitmap);
+ return request->onCopyFinished(CopyResult::SourceInvalid);
}
ALOGV("Using new path, cropRect=" RECT_STRING ", transform=%x", ARECT_ARGS(cropRect),
windowTransform);
if (err != NO_ERROR) {
ALOGW("Failed to get last queued buffer, error = %d", err);
- return CopyResult::UnknownError;
+ return request->onCopyFinished(CopyResult::SourceInvalid);
}
if (rawSourceBuffer == nullptr) {
ALOGW("Surface doesn't have any previously queued frames, nothing to readback from");
- return CopyResult::SourceEmpty;
+ return request->onCopyFinished(CopyResult::SourceEmpty);
}
UniqueAHardwareBuffer sourceBuffer{rawSourceBuffer};
AHardwareBuffer_Desc description;
AHardwareBuffer_describe(sourceBuffer.get(), &description);
if (description.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT) {
ALOGW("Surface is protected, unable to copy from it");
- return CopyResult::SourceInvalid;
+ return request->onCopyFinished(CopyResult::SourceInvalid);
}
- if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) {
- ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt");
- return CopyResult::Timeout;
+ {
+ ATRACE_NAME("sync_wait");
+ if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) {
+ ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt");
+ return request->onCopyFinished(CopyResult::Timeout);
+ }
}
- sk_sp<SkColorSpace> colorSpace = DataSpaceToColorSpace(
- static_cast<android_dataspace>(ANativeWindow_getBuffersDataSpace(window)));
+ int32_t dataspace = ANativeWindow_getBuffersDataSpace(window);
+
+ // If the application is not updating the Surface themselves, e.g., another
+ // process is producing buffers for the application to display, then
+ // ANativeWindow_getBuffersDataSpace will return an unknown answer, so grab
+ // the dataspace from buffer metadata instead, if it exists.
+ if (dataspace == 0) {
+ dataspace = AHardwareBuffer_getDataSpace(sourceBuffer.get());
+ }
+
+ sk_sp<SkColorSpace> colorSpace =
+ DataSpaceToColorSpace(static_cast<android_dataspace>(dataspace));
sk_sp<SkImage> image =
SkImage::MakeFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, colorSpace);
if (!image.get()) {
- return CopyResult::UnknownError;
+ return request->onCopyFinished(CopyResult::UnknownError);
}
sk_sp<GrDirectContext> grContext = mRenderThread.requireGrContext();
- SkRect srcRect = inSrcRect.toSkRect();
+ SkRect srcRect = request->srcRect.toSkRect();
SkRect imageSrcRect = SkRect::MakeIWH(description.width, description.height);
SkISize imageWH = SkISize::Make(description.width, description.height);
@@ -136,23 +164,26 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec
ALOGV("intersecting " RECT_STRING " with " RECT_STRING, SK_RECT_ARGS(srcRect),
SK_RECT_ARGS(textureRect));
if (!srcRect.intersect(textureRect)) {
- return CopyResult::UnknownError;
+ return request->onCopyFinished(CopyResult::UnknownError);
}
}
+ SkBitmap skBitmap = request->getDestinationBitmap(srcRect.width(), srcRect.height());
+ SkBitmap* bitmap = &skBitmap;
sk_sp<SkSurface> tmpSurface =
- SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes,
+ SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), skgpu::Budgeted::kYes,
bitmap->info(), 0, kTopLeft_GrSurfaceOrigin, nullptr);
// if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we
// attempt to do the intermediate rendering step in 8888
if (!tmpSurface.get()) {
SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType);
- tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes,
+ tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(),
+ skgpu::Budgeted::kYes,
tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr);
if (!tmpSurface.get()) {
ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap");
- return CopyResult::UnknownError;
+ return request->onCopyFinished(CopyResult::UnknownError);
}
}
@@ -211,6 +242,10 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec
const bool hasBufferCrop = cropRect.left < cropRect.right && cropRect.top < cropRect.bottom;
auto constraint =
hasBufferCrop ? SkCanvas::kStrict_SrcRectConstraint : SkCanvas::kFast_SrcRectConstraint;
+
+ static constexpr float kMaxLuminanceNits = 4000.f;
+ tonemapPaint(image->imageInfo(), canvas->imageInfo(), kMaxLuminanceNits, paint);
+
canvas->drawImageRect(image, imageSrcRect, imageDstRect, sampling, &paint, constraint);
canvas->restore();
@@ -223,52 +258,13 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec
!tmpBitmap.tryAllocPixels(tmpInfo) || !tmpSurface->readPixels(tmpBitmap, 0, 0) ||
!tmpBitmap.readPixels(bitmap->info(), bitmap->getPixels(), bitmap->rowBytes(), 0, 0)) {
ALOGW("Unable to convert content into the provided bitmap");
- return CopyResult::UnknownError;
+ return request->onCopyFinished(CopyResult::UnknownError);
}
}
bitmap->notifyPixelsChanged();
- return CopyResult::Success;
-}
-
-CopyResult Readback::copySurfaceIntoLegacy(ANativeWindow* window, const Rect& srcRect,
- SkBitmap* bitmap) {
- // Setup the source
- AHardwareBuffer* rawSourceBuffer;
- int rawSourceFence;
- Matrix4 texTransform;
- status_t err = ANativeWindow_getLastQueuedBuffer(window, &rawSourceBuffer, &rawSourceFence,
- texTransform.data);
- base::unique_fd sourceFence(rawSourceFence);
- texTransform.invalidateType();
- if (err != NO_ERROR) {
- ALOGW("Failed to get last queued buffer, error = %d", err);
- return CopyResult::UnknownError;
- }
- if (rawSourceBuffer == nullptr) {
- ALOGW("Surface doesn't have any previously queued frames, nothing to readback from");
- return CopyResult::SourceEmpty;
- }
-
- UniqueAHardwareBuffer sourceBuffer{rawSourceBuffer};
- AHardwareBuffer_Desc description;
- AHardwareBuffer_describe(sourceBuffer.get(), &description);
- if (description.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT) {
- ALOGW("Surface is protected, unable to copy from it");
- return CopyResult::SourceInvalid;
- }
-
- if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) {
- ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt");
- return CopyResult::Timeout;
- }
-
- sk_sp<SkColorSpace> colorSpace = DataSpaceToColorSpace(
- static_cast<android_dataspace>(ANativeWindow_getBuffersDataSpace(window)));
- sk_sp<SkImage> image =
- SkImage::MakeFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, colorSpace);
- return copyImageInto(image, srcRect, bitmap);
+ return request->onCopyFinished(CopyResult::Success);
}
CopyResult Readback::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) {
@@ -306,14 +302,14 @@ CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap
CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, const Rect& srcRect,
SkBitmap* bitmap) {
ATRACE_CALL();
+ if (!image.get()) {
+ return CopyResult::UnknownError;
+ }
if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) {
mRenderThread.requireGlContext();
} else {
mRenderThread.requireVkContext();
}
- if (!image.get()) {
- return CopyResult::UnknownError;
- }
int imgWidth = image->width();
int imgHeight = image->height();
sk_sp<GrDirectContext> grContext = sk_ref_sp(mRenderThread.getGrContext());
@@ -351,14 +347,17 @@ bool Readback::copyLayerInto(Layer* layer, const SkRect* srcRect, const SkRect*
* software buffer.
*/
sk_sp<SkSurface> tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(),
- SkBudgeted::kYes, bitmap->info(), 0,
+ skgpu::Budgeted::kYes,
+ bitmap->info(),
+ 0,
kTopLeft_GrSurfaceOrigin, nullptr);
// if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we
// attempt to do the intermediate rendering step in 8888
if (!tmpSurface.get()) {
SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType);
- tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes,
+ tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(),
+ skgpu::Budgeted::kYes,
tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr);
if (!tmpSurface.get()) {
ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap");
diff --git a/libs/hwui/Readback.h b/libs/hwui/Readback.h
index d0d748ff5c16..a092d472abf0 100644
--- a/libs/hwui/Readback.h
+++ b/libs/hwui/Readback.h
@@ -16,11 +16,16 @@
#pragma once
+#include <SkRefCnt.h>
+
+#include "CopyRequest.h"
#include "Matrix.h"
#include "Rect.h"
#include "renderthread/RenderThread.h"
-#include <SkBitmap.h>
+class SkBitmap;
+class SkImage;
+struct SkRect;
namespace android {
class Bitmap;
@@ -31,23 +36,13 @@ namespace uirenderer {
class DeferredLayerUpdater;
class Layer;
-// Keep in sync with PixelCopy.java codes
-enum class CopyResult {
- Success = 0,
- UnknownError = 1,
- Timeout = 2,
- SourceEmpty = 3,
- SourceInvalid = 4,
- DestinationInvalid = 5,
-};
-
class Readback {
public:
explicit Readback(renderthread::RenderThread& thread) : mRenderThread(thread) {}
/**
* Copies the surface's most recently queued buffer into the provided bitmap.
*/
- CopyResult copySurfaceInto(ANativeWindow* window, const Rect& srcRect, SkBitmap* bitmap);
+ void copySurfaceInto(ANativeWindow* window, const std::shared_ptr<CopyRequest>& request);
CopyResult copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap);
CopyResult copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap);
@@ -55,7 +50,6 @@ public:
CopyResult copyLayerInto(DeferredLayerUpdater* layer, SkBitmap* bitmap);
private:
- CopyResult copySurfaceIntoLegacy(ANativeWindow* window, const Rect& srcRect, SkBitmap* bitmap);
CopyResult copyImageInto(const sk_sp<SkImage>& image, const Rect& srcRect, SkBitmap* bitmap);
bool copyLayerInto(Layer* layer, const SkRect* srcRect, const SkRect* dstRect,
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index d101a1bdfb37..924fbd659824 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -15,13 +15,18 @@
*/
#include "RecordingCanvas.h"
-#include <hwui/Paint.h>
#include <GrRecordingContext.h>
+#include <SkMesh.h>
+#include <hwui/Paint.h>
+#include <log/log.h>
#include <experimental/type_traits>
+#include <utility>
+#include "Mesh.h"
#include "SkAndroidFrameworkUtils.h"
+#include "SkBlendMode.h"
#include "SkCanvas.h"
#include "SkCanvasPriv.h"
#include "SkColor.h"
@@ -29,14 +34,21 @@
#include "SkDrawShadowInfo.h"
#include "SkImage.h"
#include "SkImageFilter.h"
+#include "SkImageInfo.h"
#include "SkLatticeIter.h"
-#include "SkMath.h"
+#include "SkPaint.h"
#include "SkPicture.h"
+#include "SkRRect.h"
#include "SkRSXform.h"
+#include "SkRect.h"
#include "SkRegion.h"
#include "SkTextBlob.h"
#include "SkVertices.h"
+#include "Tonemapper.h"
#include "VectorDrawable.h"
+#include "effects/GainmapRenderer.h"
+#include "include/gpu/GpuTypes.h" // from Skia
+#include "include/gpu/GrDirectContext.h"
#include "pipeline/skia/AnimatedDrawables.h"
#include "pipeline/skia/FunctorDrawable.h"
#ifdef __ANDROID__
@@ -61,16 +73,24 @@ static void copy_v(void* dst) {}
template <typename S, typename... Rest>
static void copy_v(void* dst, const S* src, int n, Rest&&... rest) {
- SkASSERTF(((uintptr_t)dst & (alignof(S) - 1)) == 0,
- "Expected %p to be aligned for at least %zu bytes.", dst, alignof(S));
- sk_careful_memcpy(dst, src, n * sizeof(S));
- copy_v(SkTAddOffset<void>(dst, n * sizeof(S)), std::forward<Rest>(rest)...);
+ LOG_FATAL_IF(((uintptr_t)dst & (alignof(S) - 1)) != 0,
+ "Expected %p to be aligned for at least %zu bytes.",
+ dst, alignof(S));
+ // If n is 0, there is nothing to copy into dst from src.
+ if (n > 0) {
+ memcpy(dst, src, n * sizeof(S));
+ dst = reinterpret_cast<void*>(
+ reinterpret_cast<uint8_t*>(dst) + n * sizeof(S));
+ }
+ // Repeat for the next items, if any
+ copy_v(dst, std::forward<Rest>(rest)...);
}
// Helper for getting back at arrays which have been copy_v'd together after an Op.
template <typename D, typename T>
static const D* pod(const T* op, size_t offset = 0) {
- return SkTAddOffset<const D>(op + 1, offset);
+ return reinterpret_cast<const D*>(
+ reinterpret_cast<const uint8_t*>(op + 1) + offset);
}
namespace {
@@ -269,7 +289,6 @@ struct DrawDRRect final : Op {
SkPaint paint;
void draw(SkCanvas* c, const SkMatrix&) const { c->drawDRRect(outer, inner, paint); }
};
-
struct DrawAnnotation final : Op {
static const auto kType = Type::DrawAnnotation;
DrawAnnotation(const SkRect& rect, SkData* value) : rect(rect), value(sk_ref_sp(value)) {}
@@ -318,9 +337,15 @@ struct DrawPicture final : Op {
struct DrawImage final : Op {
static const auto kType = Type::DrawImage;
- DrawImage(sk_sp<const SkImage>&& image, SkScalar x, SkScalar y,
- const SkSamplingOptions& sampling, const SkPaint* paint, BitmapPalette palette)
- : image(std::move(image)), x(x), y(y), sampling(sampling), palette(palette) {
+ DrawImage(DrawImagePayload&& payload, SkScalar x, SkScalar y, const SkSamplingOptions& sampling,
+ const SkPaint* paint)
+ : image(std::move(payload.image))
+ , x(x)
+ , y(y)
+ , sampling(sampling)
+ , palette(payload.palette)
+ , gainmap(std::move(payload.gainmapImage))
+ , gainmapInfo(payload.gainmapInfo) {
if (paint) {
this->paint = *paint;
}
@@ -330,17 +355,34 @@ struct DrawImage final : Op {
SkSamplingOptions sampling;
SkPaint paint;
BitmapPalette palette;
+ sk_sp<const SkImage> gainmap;
+ SkGainmapInfo gainmapInfo;
+
void draw(SkCanvas* c, const SkMatrix&) const {
- c->drawImage(image.get(), x, y, sampling, &paint);
+ if (gainmap) {
+ SkRect src = SkRect::MakeWH(image->width(), image->height());
+ SkRect dst = SkRect::MakeXYWH(x, y, src.width(), src.height());
+ DrawGainmapBitmap(c, image, src, dst, sampling, &paint,
+ SkCanvas::kFast_SrcRectConstraint, gainmap, gainmapInfo);
+ } else {
+ SkPaint newPaint = paint;
+ tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint);
+ c->drawImage(image.get(), x, y, sampling, &newPaint);
+ }
}
};
struct DrawImageRect final : Op {
static const auto kType = Type::DrawImageRect;
- DrawImageRect(sk_sp<const SkImage>&& image, const SkRect* src, const SkRect& dst,
+ DrawImageRect(DrawImagePayload&& payload, const SkRect* src, const SkRect& dst,
const SkSamplingOptions& sampling, const SkPaint* paint,
- SkCanvas::SrcRectConstraint constraint, BitmapPalette palette)
- : image(std::move(image)), dst(dst), sampling(sampling), constraint(constraint)
- , palette(palette) {
+ SkCanvas::SrcRectConstraint constraint)
+ : image(std::move(payload.image))
+ , dst(dst)
+ , sampling(sampling)
+ , constraint(constraint)
+ , palette(payload.palette)
+ , gainmap(std::move(payload.gainmapImage))
+ , gainmapInfo(payload.gainmapInfo) {
this->src = src ? *src : SkRect::MakeIWH(this->image->width(), this->image->height());
if (paint) {
this->paint = *paint;
@@ -352,23 +394,32 @@ struct DrawImageRect final : Op {
SkPaint paint;
SkCanvas::SrcRectConstraint constraint;
BitmapPalette palette;
+ sk_sp<const SkImage> gainmap;
+ SkGainmapInfo gainmapInfo;
+
void draw(SkCanvas* c, const SkMatrix&) const {
- c->drawImageRect(image.get(), src, dst, sampling, &paint, constraint);
+ if (gainmap) {
+ DrawGainmapBitmap(c, image, src, dst, sampling, &paint, constraint, gainmap,
+ gainmapInfo);
+ } else {
+ SkPaint newPaint = paint;
+ tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint);
+ c->drawImageRect(image.get(), src, dst, sampling, &newPaint, constraint);
+ }
}
};
struct DrawImageLattice final : Op {
static const auto kType = Type::DrawImageLattice;
- DrawImageLattice(sk_sp<const SkImage>&& image, int xs, int ys, int fs, const SkIRect& src,
- const SkRect& dst, SkFilterMode filter, const SkPaint* paint,
- BitmapPalette palette)
- : image(std::move(image))
+ DrawImageLattice(DrawImagePayload&& payload, int xs, int ys, int fs, const SkIRect& src,
+ const SkRect& dst, SkFilterMode filter, const SkPaint* paint)
+ : image(std::move(payload.image))
, xs(xs)
, ys(ys)
, fs(fs)
, src(src)
, dst(dst)
, filter(filter)
- , palette(palette) {
+ , palette(payload.palette) {
if (paint) {
this->paint = *paint;
}
@@ -381,13 +432,17 @@ struct DrawImageLattice final : Op {
SkPaint paint;
BitmapPalette palette;
void draw(SkCanvas* c, const SkMatrix&) const {
+ // TODO: Support drawing a gainmap 9-patch?
+
auto xdivs = pod<int>(this, 0), ydivs = pod<int>(this, xs * sizeof(int));
auto colors = (0 == fs) ? nullptr : pod<SkColor>(this, (xs + ys) * sizeof(int));
auto flags =
(0 == fs) ? nullptr : pod<SkCanvas::Lattice::RectType>(
this, (xs + ys) * sizeof(int) + fs * sizeof(SkColor));
- c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst,
- filter, &paint);
+ SkPaint newPaint = paint;
+ tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint);
+ c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst, filter,
+ &newPaint);
}
};
@@ -463,6 +518,60 @@ struct DrawVertices final : Op {
c->drawVertices(vertices, mode, paint);
}
};
+struct DrawSkMesh final : Op {
+ static const auto kType = Type::DrawSkMesh;
+ DrawSkMesh(const SkMesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint)
+ : cpuMesh(mesh), blender(std::move(blender)), paint(paint) {
+ isGpuBased = false;
+ }
+
+ const SkMesh& cpuMesh;
+ mutable SkMesh gpuMesh;
+ sk_sp<SkBlender> blender;
+ SkPaint paint;
+ mutable bool isGpuBased;
+ mutable GrDirectContext::DirectContextID contextId;
+ void draw(SkCanvas* c, const SkMatrix&) const {
+ GrDirectContext* directContext = c->recordingContext()->asDirectContext();
+
+ GrDirectContext::DirectContextID id = directContext->directContextID();
+ if (!isGpuBased || contextId != id) {
+ sk_sp<SkMesh::VertexBuffer> vb =
+ SkMesh::CopyVertexBuffer(directContext, cpuMesh.refVertexBuffer());
+ if (!cpuMesh.indexBuffer()) {
+ gpuMesh = SkMesh::Make(cpuMesh.refSpec(), cpuMesh.mode(), vb, cpuMesh.vertexCount(),
+ cpuMesh.vertexOffset(), cpuMesh.refUniforms(),
+ cpuMesh.bounds())
+ .mesh;
+ } else {
+ sk_sp<SkMesh::IndexBuffer> ib =
+ SkMesh::CopyIndexBuffer(directContext, cpuMesh.refIndexBuffer());
+ gpuMesh = SkMesh::MakeIndexed(cpuMesh.refSpec(), cpuMesh.mode(), vb,
+ cpuMesh.vertexCount(), cpuMesh.vertexOffset(), ib,
+ cpuMesh.indexCount(), cpuMesh.indexOffset(),
+ cpuMesh.refUniforms(), cpuMesh.bounds())
+ .mesh;
+ }
+
+ isGpuBased = true;
+ contextId = id;
+ }
+
+ c->drawMesh(gpuMesh, blender, paint);
+ }
+};
+
+struct DrawMesh final : Op {
+ static const auto kType = Type::DrawMesh;
+ DrawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint)
+ : mesh(mesh), blender(std::move(blender)), paint(paint) {}
+
+ const Mesh& mesh;
+ sk_sp<SkBlender> blender;
+ SkPaint paint;
+
+ void draw(SkCanvas* c, const SkMatrix&) const { c->drawMesh(mesh.getSkMesh(), blender, paint); }
+};
struct DrawAtlas final : Op {
static const auto kType = Type::DrawAtlas;
DrawAtlas(const SkImage* atlas, int count, SkBlendMode mode, const SkSamplingOptions& sampling,
@@ -569,7 +678,7 @@ public:
GrRecordingContext* directContext = c->recordingContext();
mLayerImageInfo =
c->imageInfo().makeWH(deviceBounds.width(), deviceBounds.height());
- mLayerSurface = SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes,
+ mLayerSurface = SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes,
mLayerImageInfo, 0,
kTopLeft_GrSurfaceOrigin, nullptr);
}
@@ -604,18 +713,23 @@ public:
};
}
+static constexpr inline bool is_power_of_two(int value) {
+ return (value & (value - 1)) == 0;
+}
+
template <typename T, typename... Args>
void* DisplayListData::push(size_t pod, Args&&... args) {
size_t skip = SkAlignPtr(sizeof(T) + pod);
- SkASSERT(skip < (1 << 24));
+ LOG_FATAL_IF(skip >= (1 << 24));
if (fUsed + skip > fReserved) {
- static_assert(SkIsPow2(SKLITEDL_PAGE), "This math needs updating for non-pow2.");
+ static_assert(is_power_of_two(SKLITEDL_PAGE),
+ "This math needs updating for non-pow2.");
// Next greater multiple of SKLITEDL_PAGE.
fReserved = (fUsed + skip + SKLITEDL_PAGE) & ~(SKLITEDL_PAGE - 1);
fBytes.realloc(fReserved);
LOG_ALWAYS_FATAL_IF(fBytes.get() == nullptr, "realloc(%zd) failed", fReserved);
}
- SkASSERT(fUsed + skip <= fReserved);
+ LOG_FATAL_IF((fUsed + skip) > fReserved);
auto op = (T*)(fBytes.get() + fUsed);
fUsed += skip;
new (op) T{std::forward<Args>(args)...};
@@ -727,27 +841,25 @@ void DisplayListData::drawPicture(const SkPicture* picture, const SkMatrix* matr
const SkPaint* paint) {
this->push<DrawPicture>(0, picture, matrix, paint);
}
-void DisplayListData::drawImage(sk_sp<const SkImage> image, SkScalar x, SkScalar y,
- const SkSamplingOptions& sampling, const SkPaint* paint,
- BitmapPalette palette) {
- this->push<DrawImage>(0, std::move(image), x, y, sampling, paint, palette);
+void DisplayListData::drawImage(DrawImagePayload&& payload, SkScalar x, SkScalar y,
+ const SkSamplingOptions& sampling, const SkPaint* paint) {
+ this->push<DrawImage>(0, std::move(payload), x, y, sampling, paint);
}
-void DisplayListData::drawImageRect(sk_sp<const SkImage> image, const SkRect* src,
+void DisplayListData::drawImageRect(DrawImagePayload&& payload, const SkRect* src,
const SkRect& dst, const SkSamplingOptions& sampling,
- const SkPaint* paint, SkCanvas::SrcRectConstraint constraint,
- BitmapPalette palette) {
- this->push<DrawImageRect>(0, std::move(image), src, dst, sampling, paint, constraint, palette);
+ const SkPaint* paint, SkCanvas::SrcRectConstraint constraint) {
+ this->push<DrawImageRect>(0, std::move(payload), src, dst, sampling, paint, constraint);
}
-void DisplayListData::drawImageLattice(sk_sp<const SkImage> image, const SkCanvas::Lattice& lattice,
- const SkRect& dst, SkFilterMode filter, const SkPaint* paint,
- BitmapPalette palette) {
+void DisplayListData::drawImageLattice(DrawImagePayload&& payload, const SkCanvas::Lattice& lattice,
+ const SkRect& dst, SkFilterMode filter,
+ const SkPaint* paint) {
int xs = lattice.fXCount, ys = lattice.fYCount;
int fs = lattice.fRectTypes ? (xs + 1) * (ys + 1) : 0;
size_t bytes = (xs + ys) * sizeof(int) + fs * sizeof(SkCanvas::Lattice::RectType) +
fs * sizeof(SkColor);
- SkASSERT(lattice.fBounds);
- void* pod = this->push<DrawImageLattice>(bytes, std::move(image), xs, ys, fs, *lattice.fBounds,
- dst, filter, paint, palette);
+ LOG_FATAL_IF(!lattice.fBounds);
+ void* pod = this->push<DrawImageLattice>(bytes, std::move(payload), xs, ys, fs,
+ *lattice.fBounds, dst, filter, paint);
copy_v(pod, lattice.fXDivs, xs, lattice.fYDivs, ys, lattice.fColors, fs, lattice.fRectTypes,
fs);
}
@@ -774,6 +886,14 @@ void DisplayListData::drawPoints(SkCanvas::PointMode mode, size_t count, const S
void DisplayListData::drawVertices(const SkVertices* vert, SkBlendMode mode, const SkPaint& paint) {
this->push<DrawVertices>(0, vert, mode, paint);
}
+void DisplayListData::drawMesh(const SkMesh& mesh, const sk_sp<SkBlender>& blender,
+ const SkPaint& paint) {
+ this->push<DrawSkMesh>(0, mesh, blender, paint);
+}
+void DisplayListData::drawMesh(const Mesh& mesh, const sk_sp<SkBlender>& blender,
+ const SkPaint& paint) {
+ this->push<DrawMesh>(0, mesh, blender, paint);
+}
void DisplayListData::drawAtlas(const SkImage* atlas, const SkRSXform xforms[], const SkRect texs[],
const SkColor colors[], int count, SkBlendMode xfermode,
const SkSamplingOptions& sampling, const SkRect* cull,
@@ -1050,57 +1170,55 @@ void RecordingCanvas::drawRippleDrawable(const skiapipeline::RippleDrawableParam
fDL->drawRippleDrawable(params);
}
-void RecordingCanvas::drawImage(const sk_sp<SkImage>& image, SkScalar x, SkScalar y,
- const SkSamplingOptions& sampling, const SkPaint* paint,
- BitmapPalette palette) {
- fDL->drawImage(image, x, y, sampling, paint, palette);
+void RecordingCanvas::drawImage(DrawImagePayload&& payload, SkScalar x, SkScalar y,
+ const SkSamplingOptions& sampling, const SkPaint* paint) {
+ fDL->drawImage(std::move(payload), x, y, sampling, paint);
}
-void RecordingCanvas::drawImageRect(const sk_sp<SkImage>& image, const SkRect& src,
+void RecordingCanvas::drawImageRect(DrawImagePayload&& payload, const SkRect& src,
const SkRect& dst, const SkSamplingOptions& sampling,
- const SkPaint* paint, SrcRectConstraint constraint,
- BitmapPalette palette) {
- fDL->drawImageRect(image, &src, dst, sampling, paint, constraint, palette);
+ const SkPaint* paint, SrcRectConstraint constraint) {
+ fDL->drawImageRect(std::move(payload), &src, dst, sampling, paint, constraint);
}
-void RecordingCanvas::drawImageLattice(const sk_sp<SkImage>& image, const Lattice& lattice,
- const SkRect& dst, SkFilterMode filter, const SkPaint* paint,
- BitmapPalette palette) {
- if (!image || dst.isEmpty()) {
+void RecordingCanvas::drawImageLattice(DrawImagePayload&& payload, const Lattice& lattice,
+ const SkRect& dst, SkFilterMode filter,
+ const SkPaint* paint) {
+ if (!payload.image || dst.isEmpty()) {
return;
}
SkIRect bounds;
Lattice latticePlusBounds = lattice;
if (!latticePlusBounds.fBounds) {
- bounds = SkIRect::MakeWH(image->width(), image->height());
+ bounds = SkIRect::MakeWH(payload.image->width(), payload.image->height());
latticePlusBounds.fBounds = &bounds;
}
- if (SkLatticeIter::Valid(image->width(), image->height(), latticePlusBounds)) {
- fDL->drawImageLattice(image, latticePlusBounds, dst, filter, paint, palette);
+ if (SkLatticeIter::Valid(payload.image->width(), payload.image->height(), latticePlusBounds)) {
+ fDL->drawImageLattice(std::move(payload), latticePlusBounds, dst, filter, paint);
} else {
SkSamplingOptions sampling(filter, SkMipmapMode::kNone);
- fDL->drawImageRect(image, nullptr, dst, sampling, paint, kFast_SrcRectConstraint, palette);
+ fDL->drawImageRect(std::move(payload), nullptr, dst, sampling, paint,
+ kFast_SrcRectConstraint);
}
}
void RecordingCanvas::onDrawImage2(const SkImage* img, SkScalar x, SkScalar y,
const SkSamplingOptions& sampling, const SkPaint* paint) {
- fDL->drawImage(sk_ref_sp(img), x, y, sampling, paint, BitmapPalette::Unknown);
+ fDL->drawImage(DrawImagePayload(img), x, y, sampling, paint);
}
void RecordingCanvas::onDrawImageRect2(const SkImage* img, const SkRect& src, const SkRect& dst,
const SkSamplingOptions& sampling, const SkPaint* paint,
SrcRectConstraint constraint) {
- fDL->drawImageRect(sk_ref_sp(img), &src, dst, sampling, paint, constraint,
- BitmapPalette::Unknown);
+ fDL->drawImageRect(DrawImagePayload(img), &src, dst, sampling, paint, constraint);
}
void RecordingCanvas::onDrawImageLattice2(const SkImage* img, const SkCanvas::Lattice& lattice,
const SkRect& dst, SkFilterMode filter,
const SkPaint* paint) {
- fDL->drawImageLattice(sk_ref_sp(img), lattice, dst, filter, paint, BitmapPalette::Unknown);
+ fDL->drawImageLattice(DrawImagePayload(img), lattice, dst, filter, paint);
}
void RecordingCanvas::onDrawPatch(const SkPoint cubics[12], const SkColor colors[4],
@@ -1116,6 +1234,13 @@ void RecordingCanvas::onDrawVerticesObject(const SkVertices* vertices,
SkBlendMode mode, const SkPaint& paint) {
fDL->drawVertices(vertices, mode, paint);
}
+void RecordingCanvas::onDrawMesh(const SkMesh& mesh, sk_sp<SkBlender> blender,
+ const SkPaint& paint) {
+ fDL->drawMesh(mesh, blender, paint);
+}
+void RecordingCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint) {
+ fDL->drawMesh(mesh, blender, paint);
+}
void RecordingCanvas::onDrawAtlas2(const SkImage* atlas, const SkRSXform xforms[],
const SkRect texs[], const SkColor colors[], int count,
SkBlendMode bmode, const SkSamplingOptions& sampling,
@@ -1134,5 +1259,14 @@ void RecordingCanvas::drawWebView(skiapipeline::FunctorDrawable* drawable) {
fDL->drawWebView(drawable);
}
+[[nodiscard]] const SkMesh& DrawMeshPayload::getSkMesh() const {
+ LOG_FATAL_IF(!meshWrapper && !mesh, "One of Mesh or Mesh must be non-null");
+ if (meshWrapper) {
+ return meshWrapper->getSkMesh();
+ } else {
+ return *mesh;
+ }
+}
+
} // namespace uirenderer
} // namespace android
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 212b4e72dcb2..1f4ba5d6d557 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -16,23 +16,32 @@
#pragma once
+#include <SkCanvas.h>
+#include <SkCanvasVirtualEnforcer.h>
+#include <SkDrawable.h>
+#include <SkGainmapInfo.h>
+#include <SkNoDrawCanvas.h>
+#include <SkPaint.h>
+#include <SkPath.h>
+#include <SkRect.h>
+#include <SkRuntimeEffect.h>
+#include <log/log.h>
+
+#include <cstdlib>
+#include <utility>
+#include <vector>
+
#include "CanvasTransform.h"
+#include "Gainmap.h"
#include "hwui/Bitmap.h"
+#include "pipeline/skia/AnimatedDrawables.h"
+#include "utils/AutoMalloc.h"
#include "utils/Macros.h"
#include "utils/TypeLogic.h"
-#include "SkCanvas.h"
-#include "SkCanvasVirtualEnforcer.h"
-#include "SkDrawable.h"
-#include "SkNoDrawCanvas.h"
-#include "SkPaint.h"
-#include "SkPath.h"
-#include "SkRect.h"
-
-#include "pipeline/skia/AnimatedDrawables.h"
-
-#include <SkRuntimeEffect.h>
-#include <vector>
+enum class SkBlendMode;
+class SkRRect;
+class Mesh;
namespace android {
namespace uirenderer {
@@ -59,6 +68,44 @@ struct DisplayListOp {
static_assert(sizeof(DisplayListOp) == 4);
+class DrawMeshPayload {
+public:
+ explicit DrawMeshPayload(const SkMesh* mesh) : mesh(mesh) {}
+ explicit DrawMeshPayload(const Mesh* meshWrapper) : meshWrapper(meshWrapper) {}
+
+ [[nodiscard]] const SkMesh& getSkMesh() const;
+
+private:
+ const SkMesh* mesh = nullptr;
+ const Mesh* meshWrapper = nullptr;
+};
+
+struct DrawImagePayload {
+ explicit DrawImagePayload(Bitmap& bitmap)
+ : image(bitmap.makeImage()), palette(bitmap.palette()) {
+ if (bitmap.hasGainmap()) {
+ auto gainmap = bitmap.gainmap();
+ gainmapInfo = gainmap->info;
+ gainmapImage = gainmap->bitmap->makeImage();
+ }
+ }
+
+ explicit DrawImagePayload(const SkImage* image)
+ : image(sk_ref_sp(image)), palette(BitmapPalette::Unknown) {}
+
+ DrawImagePayload(const DrawImagePayload&) = default;
+ DrawImagePayload(DrawImagePayload&&) = default;
+ DrawImagePayload& operator=(const DrawImagePayload&) = default;
+ DrawImagePayload& operator=(DrawImagePayload&&) = default;
+ ~DrawImagePayload() = default;
+
+ sk_sp<SkImage> image;
+ BitmapPalette palette;
+
+ sk_sp<SkImage> gainmapImage;
+ SkGainmapInfo gainmapInfo;
+};
+
class RecordingCanvas;
class DisplayListData final {
@@ -109,19 +156,21 @@ private:
void drawRRect(const SkRRect&, const SkPaint&);
void drawDRRect(const SkRRect&, const SkRRect&, const SkPaint&);
+ void drawMesh(const SkMesh&, const sk_sp<SkBlender>&, const SkPaint&);
+ void drawMesh(const Mesh&, const sk_sp<SkBlender>&, const SkPaint&);
+
void drawAnnotation(const SkRect&, const char*, SkData*);
void drawDrawable(SkDrawable*, const SkMatrix*);
void drawPicture(const SkPicture*, const SkMatrix*, const SkPaint*);
void drawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&);
- void drawImage(sk_sp<const SkImage>, SkScalar, SkScalar, const SkSamplingOptions&,
- const SkPaint*, BitmapPalette palette);
- void drawImageNine(sk_sp<const SkImage>, const SkIRect&, const SkRect&, const SkPaint*);
- void drawImageRect(sk_sp<const SkImage>, const SkRect*, const SkRect&, const SkSamplingOptions&,
- const SkPaint*, SkCanvas::SrcRectConstraint, BitmapPalette palette);
- void drawImageLattice(sk_sp<const SkImage>, const SkCanvas::Lattice&, const SkRect&,
- SkFilterMode, const SkPaint*, BitmapPalette);
+ void drawImage(DrawImagePayload&&, SkScalar, SkScalar, const SkSamplingOptions&,
+ const SkPaint*);
+ void drawImageRect(DrawImagePayload&&, const SkRect*, const SkRect&, const SkSamplingOptions&,
+ const SkPaint*, SkCanvas::SrcRectConstraint);
+ void drawImageLattice(DrawImagePayload&&, const SkCanvas::Lattice&, const SkRect&, SkFilterMode,
+ const SkPaint*);
void drawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode,
const SkPaint&);
@@ -140,7 +189,7 @@ private:
template <typename Fn, typename... Args>
void map(const Fn[], Args...) const;
- SkAutoTMalloc<uint8_t> fBytes;
+ AutoTMalloc<uint8_t> fBytes;
size_t fUsed = 0;
size_t fReserved = 0;
@@ -188,14 +237,14 @@ public:
void onDrawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&) override;
- void drawImage(const sk_sp<SkImage>&, SkScalar left, SkScalar top, const SkSamplingOptions&,
- const SkPaint* paint, BitmapPalette pallete);
void drawRippleDrawable(const skiapipeline::RippleDrawableParams& params);
- void drawImageRect(const sk_sp<SkImage>& image, const SkRect& src, const SkRect& dst,
- const SkSamplingOptions&, const SkPaint*, SrcRectConstraint, BitmapPalette);
- void drawImageLattice(const sk_sp<SkImage>& image, const Lattice& lattice, const SkRect& dst,
- SkFilterMode, const SkPaint* paint, BitmapPalette palette);
+ void drawImage(DrawImagePayload&&, SkScalar, SkScalar, const SkSamplingOptions&,
+ const SkPaint*);
+ void drawImageRect(DrawImagePayload&&, const SkRect&, const SkRect&, const SkSamplingOptions&,
+ const SkPaint*, SrcRectConstraint);
+ void drawImageLattice(DrawImagePayload&&, const Lattice& lattice, const SkRect&, SkFilterMode,
+ const SkPaint*);
void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&,
const SkPaint*) override;
@@ -208,10 +257,12 @@ public:
const SkPaint&) override;
void onDrawPoints(PointMode, size_t count, const SkPoint pts[], const SkPaint&) override;
void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override;
+ void onDrawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) override;
void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int,
SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override;
void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override;
+ void drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint);
void drawVectorDrawable(VectorDrawableRoot* tree);
void drawWebView(skiapipeline::FunctorDrawable*);
diff --git a/libs/hwui/Rect.h b/libs/hwui/Rect.h
index 24443c8c9836..7170226037f9 100644
--- a/libs/hwui/Rect.h
+++ b/libs/hwui/Rect.h
@@ -71,9 +71,14 @@ public:
, right(rect.fRight)
, bottom(rect.fBottom) {}
- friend int operator==(const Rect& a, const Rect& b) { return !memcmp(&a, &b, sizeof(a)); }
+ friend int operator==(const Rect& a, const Rect& b) {
+ return a.left == b.left &&
+ a.top == b.top &&
+ a.right == b.right &&
+ a.bottom == b.bottom;
+ }
- friend int operator!=(const Rect& a, const Rect& b) { return memcmp(&a, &b, sizeof(a)); }
+ friend int operator!=(const Rect& a, const Rect& b) { return !(a == b); }
inline void clear() { left = top = right = bottom = 0.0f; }
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index b348a6ecaae4..1c39db3a31bb 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -152,6 +152,9 @@ void RenderNode::damageSelf(TreeInfo& info) {
// TODO: Get this from the display list ops or something
info.damageAccumulator->dirty(DIRTY_MIN, DIRTY_MIN, DIRTY_MAX, DIRTY_MAX);
}
+ if (!mIsTextureView) {
+ info.out.solelyTextureViewUpdates = false;
+ }
}
}
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 7092e1733570..d27c428ca8c3 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -16,7 +16,6 @@
#pragma once
-#include <SkCamera.h>
#include <SkMatrix.h>
#include <utils/LinearAllocator.h>
@@ -222,6 +221,8 @@ public:
int64_t uniqueId() const { return mUniqueId; }
+ void setIsTextureView() { mIsTextureView = true; }
+
void markDrawStart(SkCanvas& canvas);
void markDrawEnd(SkCanvas& canvas);
@@ -291,6 +292,8 @@ private:
bool mHasHolePunches;
StretchMask mStretchMask;
+ bool mIsTextureView = false;
+
// METHODS & FIELDS ONLY USED BY THE SKIA RENDERER
public:
/**
diff --git a/libs/hwui/SafeMath.h b/libs/hwui/SafeMath.h
new file mode 100644
index 000000000000..4d6adf55c0cb
--- /dev/null
+++ b/libs/hwui/SafeMath.h
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+#ifndef SkSafeMath_DEFINED
+#define SkSafeMath_DEFINED
+
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+
+// Copy of Skia's SafeMath API used to validate Mesh parameters to support
+// deferred creation of SkMesh instances on RenderThread.
+// SafeMath always check that a series of operations do not overflow.
+// This must be correct for all platforms, because this is a check for safety at runtime.
+
+class SafeMath {
+public:
+ SafeMath() = default;
+
+ bool ok() const { return fOK; }
+ explicit operator bool() const { return fOK; }
+
+ size_t mul(size_t x, size_t y) {
+ return sizeof(size_t) == sizeof(uint64_t) ? mul64(x, y) : mul32(x, y);
+ }
+
+ size_t add(size_t x, size_t y) {
+ size_t result = x + y;
+ fOK &= result >= x;
+ return result;
+ }
+
+ /**
+ * Return a + b, unless this result is an overflow/underflow. In those cases, fOK will
+ * be set to false, and it is undefined what this returns.
+ */
+ int addInt(int a, int b) {
+ if (b < 0 && a < std::numeric_limits<int>::min() - b) {
+ fOK = false;
+ return a;
+ } else if (b > 0 && a > std::numeric_limits<int>::max() - b) {
+ fOK = false;
+ return a;
+ }
+ return a + b;
+ }
+
+ // These saturate to their results
+ static size_t Add(size_t x, size_t y) {
+ SafeMath tmp;
+ size_t sum = tmp.add(x, y);
+ return tmp.ok() ? sum : SIZE_MAX;
+ }
+
+ static size_t Mul(size_t x, size_t y) {
+ SafeMath tmp;
+ size_t prod = tmp.mul(x, y);
+ return tmp.ok() ? prod : SIZE_MAX;
+ }
+
+private:
+ uint32_t mul32(uint32_t x, uint32_t y) {
+ uint64_t bx = x;
+ uint64_t by = y;
+ uint64_t result = bx * by;
+ fOK &= result >> 32 == 0;
+ // Overflow information is capture in fOK. Return the result modulo 2^32.
+ return (uint32_t)result;
+ }
+
+ uint64_t mul64(uint64_t x, uint64_t y) {
+ if (x <= std::numeric_limits<uint64_t>::max() >> 32 &&
+ y <= std::numeric_limits<uint64_t>::max() >> 32) {
+ return x * y;
+ } else {
+ auto hi = [](uint64_t x) { return x >> 32; };
+ auto lo = [](uint64_t x) { return x & 0xFFFFFFFF; };
+
+ uint64_t lx_ly = lo(x) * lo(y);
+ uint64_t hx_ly = hi(x) * lo(y);
+ uint64_t lx_hy = lo(x) * hi(y);
+ uint64_t hx_hy = hi(x) * hi(y);
+ uint64_t result = 0;
+ result = this->add(lx_ly, (hx_ly << 32));
+ result = this->add(result, (lx_hy << 32));
+ fOK &= (hx_hy + (hx_ly >> 32) + (lx_hy >> 32)) == 0;
+
+ return result;
+ }
+ }
+ bool fOK = true;
+};
+
+#endif // SkSafeMath_DEFINED
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index f9b3a8c12b2e..8394c3cd4175 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -16,37 +16,47 @@
#include "SkiaCanvas.h"
-#include "CanvasProperty.h"
-#include "NinePatchUtils.h"
-#include "VectorDrawable.h"
-#include "hwui/Bitmap.h"
-#include "hwui/MinikinUtils.h"
-#include "hwui/PaintFilter.h"
-#include "pipeline/skia/AnimatedDrawables.h"
-#include "pipeline/skia/HolePunch.h"
-
#include <SkAndroidFrameworkUtils.h>
#include <SkAnimatedImage.h>
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkCanvas.h>
#include <SkCanvasPriv.h>
#include <SkCanvasStateUtils.h>
#include <SkColorFilter.h>
-#include <SkDeque.h>
#include <SkDrawable.h>
#include <SkFont.h>
#include <SkGraphics.h>
#include <SkImage.h>
#include <SkImagePriv.h>
+#include <SkMatrix.h>
+#include <SkPaint.h>
#include <SkPicture.h>
+#include <SkRRect.h>
#include <SkRSXform.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
#include <SkShader.h>
-#include <SkTemplates.h>
#include <SkTextBlob.h>
#include <SkVertices.h>
+#include <log/log.h>
+#include <ui/FatVector.h>
#include <memory>
#include <optional>
#include <utility>
+#include "CanvasProperty.h"
+#include "Mesh.h"
+#include "NinePatchUtils.h"
+#include "VectorDrawable.h"
+#include "effects/GainmapRenderer.h"
+#include "hwui/Bitmap.h"
+#include "hwui/MinikinUtils.h"
+#include "hwui/PaintFilter.h"
+#include "pipeline/skia/AnimatedDrawables.h"
+#include "pipeline/skia/HolePunch.h"
+
namespace android {
using uirenderer::PaintUtils;
@@ -169,7 +179,7 @@ int SkiaCanvas::save(SaveFlags::Flags flags) {
// operation. It does this by explicitly saving off the clip & matrix state
// when requested and playing it back after the SkCanvas::restore.
void SkiaCanvas::restore() {
- const auto* rec = this->currentSaveRec();
+ const SaveRec* rec = this->currentSaveRec();
if (!rec) {
// Fast path - no record for this frame.
mCanvas->restore();
@@ -238,17 +248,20 @@ void SkiaCanvas::restoreUnclippedLayer(int restoreCount, const Paint& paint) {
}
const SkiaCanvas::SaveRec* SkiaCanvas::currentSaveRec() const {
- const SaveRec* rec = mSaveStack ? static_cast<const SaveRec*>(mSaveStack->back()) : nullptr;
+ const SaveRec* rec = (mSaveStack && !mSaveStack->empty())
+ ? static_cast<const SaveRec*>(&mSaveStack->back())
+ : nullptr;
int currentSaveCount = mCanvas->getSaveCount();
- SkASSERT(!rec || currentSaveCount >= rec->saveCount);
+ LOG_FATAL_IF(!(!rec || currentSaveCount >= rec->saveCount));
return (rec && rec->saveCount == currentSaveCount) ? rec : nullptr;
}
-void SkiaCanvas::punchHole(const SkRRect& rect) {
+void SkiaCanvas::punchHole(const SkRRect& rect, float alpha) {
SkPaint paint = SkPaint();
- paint.setColor(0);
- paint.setBlendMode(SkBlendMode::kClear);
+ paint.setColor(SkColors::kBlack);
+ paint.setAlphaf(alpha);
+ paint.setBlendMode(SkBlendMode::kDstOut);
mCanvas->drawRRect(rect, paint);
}
@@ -269,13 +282,12 @@ void SkiaCanvas::recordPartialSave(SaveFlags::Flags flags) {
}
if (!mSaveStack) {
- mSaveStack.reset(new SkDeque(sizeof(struct SaveRec), 8));
+ mSaveStack.reset(new std::deque<SaveRec>());
}
- SaveRec* rec = static_cast<SaveRec*>(mSaveStack->push_back());
- rec->saveCount = mCanvas->getSaveCount();
- rec->saveFlags = flags;
- rec->clipIndex = mClipStack.size();
+ mSaveStack->emplace_back(mCanvas->getSaveCount(), // saveCount
+ flags, // saveFlags
+ mClipStack.size()); // clipIndex
}
template <typename T>
@@ -290,7 +302,7 @@ void SkiaCanvas::recordClip(const T& clip, SkClipOp op) {
// Applies and optionally removes all clips >= index.
void SkiaCanvas::applyPersistentClips(size_t clipStartIndex) {
- SkASSERT(clipStartIndex <= mClipStack.size());
+ LOG_FATAL_IF(clipStartIndex > mClipStack.size());
const auto begin = mClipStack.cbegin() + clipStartIndex;
const auto end = mClipStack.cend();
@@ -306,7 +318,7 @@ void SkiaCanvas::applyPersistentClips(size_t clipStartIndex) {
// If the current/post-restore save rec is also persisting clips, we
// leave them on the stack to be reapplied part of the next restore().
// Otherwise we're done and just pop them.
- const auto* rec = this->currentSaveRec();
+ const SaveRec* rec = this->currentSaveRec();
if (!rec || (rec->saveFlags & SaveFlags::Clip)) {
mClipStack.erase(begin, end);
}
@@ -562,24 +574,41 @@ void SkiaCanvas::drawVertices(const SkVertices* vertices, SkBlendMode mode, cons
applyLooper(&paint, [&](const SkPaint& p) { mCanvas->drawVertices(vertices, mode, p); });
}
+void SkiaCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) {
+ GrDirectContext* context = nullptr;
+ auto recordingContext = mCanvas->recordingContext();
+ if (recordingContext) {
+ context = recordingContext->asDirectContext();
+ }
+ mesh.updateSkMesh(context);
+ mCanvas->drawMesh(mesh.getSkMesh(), blender, paint);
+}
+
// ----------------------------------------------------------------------------
// Canvas draw operations: Bitmaps
// ----------------------------------------------------------------------------
void SkiaCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) {
auto image = bitmap.makeImage();
+
+ if (bitmap.hasGainmap()) {
+ Paint gainmapPaint = paint ? *paint : Paint();
+ sk_sp<SkShader> gainmapShader = uirenderer::MakeGainmapShader(
+ image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info,
+ SkTileMode::kClamp, SkTileMode::kClamp, gainmapPaint.sampling());
+ gainmapPaint.setShader(gainmapShader);
+ return drawRect(left, top, left + bitmap.width(), top + bitmap.height(), gainmapPaint);
+ }
+
applyLooper(paint, [&](const Paint& p) {
mCanvas->drawImage(image, left, top, p.sampling(), &p);
});
}
void SkiaCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* paint) {
- auto image = bitmap.makeImage();
SkAutoCanvasRestore acr(mCanvas, true);
mCanvas->concat(matrix);
- applyLooper(paint, [&](const Paint& p) {
- mCanvas->drawImage(image, 0, 0, p.sampling(), &p);
- });
+ drawBitmap(bitmap, 0, 0, paint);
}
void SkiaCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float srcRight,
@@ -589,6 +618,16 @@ 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()) {
+ Paint gainmapPaint = paint ? *paint : Paint();
+ sk_sp<SkShader> gainmapShader = uirenderer::MakeGainmapShader(
+ image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info,
+ SkTileMode::kClamp, SkTileMode::kClamp, gainmapPaint.sampling());
+ gainmapShader = gainmapShader->makeWithLocalMatrix(SkMatrix::RectToRect(srcRect, dstRect));
+ gainmapPaint.setShader(gainmapShader);
+ return drawRect(dstLeft, dstTop, dstRight, dstBottom, gainmapPaint);
+ }
+
applyLooper(paint, [&](const Paint& p) {
mCanvas->drawImageRect(image, srcRect, dstRect, p.sampling(), &p,
SkCanvas::kFast_SrcRectConstraint);
@@ -634,7 +673,7 @@ void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight,
texsPtr += 1;
y += dy;
}
- SkASSERT(texsPtr - texs == ptCount);
+ LOG_FATAL_IF((texsPtr - texs) != ptCount);
}
// cons up indices
@@ -657,14 +696,14 @@ void SkiaCanvas::drawBitmapMesh(Bitmap& bitmap, int meshWidth, int meshHeight,
// bump to the next row
index += 1;
}
- SkASSERT(indexPtr - indices == indexCount);
+ LOG_FATAL_IF((indexPtr - indices) != indexCount);
}
// double-check that we have legal indices
-#ifdef SK_DEBUG
+#if !defined(NDEBUG)
{
for (int i = 0; i < indexCount; i++) {
- SkASSERT((unsigned)indices[i] < (unsigned)ptCount);
+ LOG_FATAL_IF((unsigned)indices[i] >= (unsigned)ptCount);
}
}
#endif
@@ -706,10 +745,12 @@ void SkiaCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, floa
numFlags = (lattice.fXCount + 1) * (lattice.fYCount + 1);
}
- SkAutoSTMalloc<25, SkCanvas::Lattice::RectType> flags(numFlags);
- SkAutoSTMalloc<25, SkColor> colors(numFlags);
+ // Most times, we do not have very many flags/colors, so the stack allocated part of
+ // FatVector will save us a heap allocation.
+ FatVector<SkCanvas::Lattice::RectType, 25> flags(numFlags);
+ FatVector<SkColor, 25> colors(numFlags);
if (numFlags > 0) {
- NinePatchUtils::SetLatticeFlags(&lattice, flags.get(), numFlags, chunk, colors.get());
+ NinePatchUtils::SetLatticeFlags(&lattice, flags.data(), numFlags, chunk, colors.data());
}
lattice.fBounds = nullptr;
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index 715007cdcd3b..b785989f35cb 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -19,19 +19,21 @@
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
#include "DeferredLayerUpdater.h"
#endif
+#include <SkCanvas.h>
+
+#include <cassert>
+#include <deque>
+#include <optional>
+
#include "RenderNode.h"
#include "VectorDrawable.h"
+#include "hwui/BlurDrawLooper.h"
#include "hwui/Canvas.h"
#include "hwui/Paint.h"
-#include "hwui/BlurDrawLooper.h"
-
-#include <SkCanvas.h>
-#include <SkDeque.h>
#include "pipeline/skia/AnimatedDrawables.h"
-#include "src/core/SkArenaAlloc.h"
-#include <cassert>
-#include <optional>
+enum class SkBlendMode;
+class SkRRect;
namespace android {
@@ -63,7 +65,7 @@ public:
LOG_ALWAYS_FATAL("SkiaCanvas does not support enableZ");
}
- virtual void punchHole(const SkRRect& rect) override;
+ virtual void punchHole(const SkRRect& rect, float alpha) override;
virtual void setBitmap(const SkBitmap& bitmap) override;
@@ -117,8 +119,8 @@ public:
virtual void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
const Paint& paint) override;
- virtual void drawDoubleRoundRect(const SkRRect& outer, const SkRRect& inner,
- const Paint& paint) override;
+ virtual void drawDoubleRoundRect(const SkRRect& outer, const SkRRect& inner,
+ const Paint& paint) override;
virtual void drawCircle(float x, float y, float radius, const Paint& paint) override;
virtual void drawOval(float left, float top, float right, float bottom,
@@ -127,6 +129,7 @@ public:
float sweepAngle, bool useCenter, const Paint& paint) override;
virtual void drawPath(const SkPath& path, const Paint& paint) override;
virtual void drawVertices(const SkVertices*, SkBlendMode, const Paint& paint) override;
+ virtual void drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) override;
virtual void drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) override;
virtual void drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* paint) override;
@@ -206,6 +209,9 @@ private:
int saveCount;
SaveFlags::Flags saveFlags;
size_t clipIndex;
+
+ SaveRec(int saveCount, SaveFlags::Flags saveFlags, size_t clipIndex)
+ : saveCount(saveCount), saveFlags(saveFlags), clipIndex(clipIndex) {}
};
const SaveRec* currentSaveRec() const;
@@ -219,11 +225,11 @@ private:
class Clip;
- std::unique_ptr<SkCanvas> mCanvasOwned; // might own a canvas we allocated
- SkCanvas* mCanvas; // we do NOT own this canvas, it must survive us
- // unless it is the same as mCanvasOwned.get()
- std::unique_ptr<SkDeque> mSaveStack; // lazily allocated, tracks partial saves.
- std::vector<Clip> mClipStack; // tracks persistent clips.
+ std::unique_ptr<SkCanvas> mCanvasOwned; // Might own a canvas we allocated.
+ SkCanvas* mCanvas; // We do NOT own this canvas, it must survive us
+ // unless it is the same as mCanvasOwned.get().
+ std::unique_ptr<std::deque<SaveRec>> mSaveStack; // Lazily allocated, tracks partial saves.
+ std::vector<Clip> mClipStack; // Tracks persistent clips.
sk_sp<PaintFilter> mPaintFilter;
};
diff --git a/libs/hwui/SkiaInterpolator.cpp b/libs/hwui/SkiaInterpolator.cpp
index 0695dd1ab218..b58f517834a3 100644
--- a/libs/hwui/SkiaInterpolator.cpp
+++ b/libs/hwui/SkiaInterpolator.cpp
@@ -16,12 +16,13 @@
#include "SkiaInterpolator.h"
-#include "include/core/SkMath.h"
+#include "include/core/SkScalar.h"
+#include "include/core/SkTypes.h"
#include "include/private/SkFixed.h"
-#include "include/private/SkMalloc.h"
-#include "include/private/SkTo.h"
#include "src/core/SkTSearch.h"
+#include <log/log.h>
+
typedef int Dot14;
#define Dot14_ONE (1 << 14)
#define Dot14_HALF (1 << 13)
@@ -91,25 +92,23 @@ static float SkUnitCubicInterp(float value, float bx, float by, float cx, float
SkiaInterpolatorBase::SkiaInterpolatorBase() {
fStorage = nullptr;
fTimes = nullptr;
- SkDEBUGCODE(fTimesArray = nullptr;)
}
SkiaInterpolatorBase::~SkiaInterpolatorBase() {
if (fStorage) {
- sk_free(fStorage);
+ free(fStorage);
}
}
void SkiaInterpolatorBase::reset(int elemCount, int frameCount) {
fFlags = 0;
- fElemCount = SkToU8(elemCount);
- fFrameCount = SkToS16(frameCount);
+ fElemCount = static_cast<uint8_t>(elemCount);
+ fFrameCount = static_cast<int16_t>(frameCount);
fRepeat = SK_Scalar1;
if (fStorage) {
- sk_free(fStorage);
+ free(fStorage);
fStorage = nullptr;
fTimes = nullptr;
- SkDEBUGCODE(fTimesArray = nullptr);
}
}
@@ -205,7 +204,6 @@ SkiaInterpolatorBase::Result SkiaInterpolatorBase::timeToT(SkMSec time, float* T
SkiaInterpolator::SkiaInterpolator() {
INHERITED::reset(0, 0);
fValues = nullptr;
- SkDEBUGCODE(fScalarsArray = nullptr;)
}
SkiaInterpolator::SkiaInterpolator(int elemCount, int frameCount) {
@@ -215,13 +213,12 @@ SkiaInterpolator::SkiaInterpolator(int elemCount, int frameCount) {
void SkiaInterpolator::reset(int elemCount, int frameCount) {
INHERITED::reset(elemCount, frameCount);
- fStorage = sk_malloc_throw((sizeof(float) * elemCount + sizeof(SkTimeCode)) * frameCount);
+ size_t numBytes = (sizeof(float) * elemCount + sizeof(SkTimeCode)) * frameCount;
+ fStorage = malloc(numBytes);
+ LOG_ALWAYS_FATAL_IF(!fStorage, "Failed to allocate %zu bytes in %s",
+ numBytes, __func__);
fTimes = (SkTimeCode*)fStorage;
fValues = (float*)((char*)fStorage + sizeof(SkTimeCode) * frameCount);
-#ifdef SK_DEBUG
- fTimesArray = (SkTimeCode(*)[10])fTimes;
- fScalarsArray = (float(*)[10])fValues;
-#endif
}
#define SK_Fixed1Third (SK_Fixed1 / 3)
diff --git a/libs/hwui/SkiaInterpolator.h b/libs/hwui/SkiaInterpolator.h
index c03f502528be..9422cb526a8f 100644
--- a/libs/hwui/SkiaInterpolator.h
+++ b/libs/hwui/SkiaInterpolator.h
@@ -17,7 +17,8 @@
#ifndef SkiaInterpolator_DEFINED
#define SkiaInterpolator_DEFINED
-#include "include/private/SkTo.h"
+#include <cstddef>
+#include <cstdint>
class SkiaInterpolatorBase {
public:
@@ -46,7 +47,9 @@ public:
@param mirror If true, the odd repeats interpolate from the last key
frame and the first.
*/
- void setMirror(bool mirror) { fFlags = SkToU8((fFlags & ~kMirror) | (int)mirror); }
+ void setMirror(bool mirror) {
+ fFlags = static_cast<uint8_t>((fFlags & ~kMirror) | (int)mirror);
+ }
/** Set the repeat count. The repeat count may be fractional.
@param repeatCount Multiplies the total time by this scalar.
@@ -57,7 +60,7 @@ public:
@param reset If true, the odd repeats interpolate from the last key
frame and the first.
*/
- void setReset(bool reset) { fFlags = SkToU8((fFlags & ~kReset) | (int)reset); }
+ void setReset(bool reset) { fFlags = static_cast<uint8_t>((fFlags & ~kReset) | (int)reset); }
Result timeToT(uint32_t time, float* T, int* index, bool* exact) const;
@@ -75,9 +78,6 @@ protected:
};
SkTimeCode* fTimes; // pointer into fStorage
void* fStorage;
-#ifdef SK_DEBUG
- SkTimeCode (*fTimesArray)[10];
-#endif
};
class SkiaInterpolator : public SkiaInterpolatorBase {
diff --git a/libs/hwui/Tonemapper.cpp b/libs/hwui/Tonemapper.cpp
new file mode 100644
index 000000000000..974a5d05aa84
--- /dev/null
+++ b/libs/hwui/Tonemapper.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright 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.
+ */
+
+#include "Tonemapper.h"
+
+#include <SkRuntimeEffect.h>
+#include <log/log.h>
+// libshaders only exists on Android devices
+#ifdef __ANDROID__
+#include <shaders/shaders.h>
+#endif
+
+#include "utils/Color.h"
+
+namespace android::uirenderer {
+
+namespace {
+
+// custom tonemapping only exists on Android devices
+#ifdef __ANDROID__
+class ColorFilterRuntimeEffectBuilder : public SkRuntimeEffectBuilder {
+public:
+ explicit ColorFilterRuntimeEffectBuilder(sk_sp<SkRuntimeEffect> effect)
+ : SkRuntimeEffectBuilder(std::move(effect)) {}
+
+ sk_sp<SkColorFilter> makeColorFilter() {
+ return this->effect()->makeColorFilter(this->uniforms());
+ }
+};
+
+static sk_sp<SkColorFilter> createLinearEffectColorFilter(const shaders::LinearEffect& linearEffect,
+ float maxDisplayLuminance,
+ float currentDisplayLuminanceNits,
+ float maxLuminance) {
+ auto shaderString = SkString(shaders::buildLinearEffectSkSL(linearEffect));
+ auto [runtimeEffect, error] = SkRuntimeEffect::MakeForColorFilter(std::move(shaderString));
+ if (!runtimeEffect) {
+ LOG_ALWAYS_FATAL("LinearColorFilter construction error: %s", error.c_str());
+ }
+
+ ColorFilterRuntimeEffectBuilder effectBuilder(std::move(runtimeEffect));
+
+ const auto uniforms =
+ shaders::buildLinearEffectUniforms(linearEffect, android::mat4(), maxDisplayLuminance,
+ currentDisplayLuminanceNits, maxLuminance);
+
+ for (const auto& uniform : uniforms) {
+ effectBuilder.uniform(uniform.name.c_str()).set(uniform.value.data(), uniform.value.size());
+ }
+
+ return effectBuilder.makeColorFilter();
+}
+
+static ui::Dataspace extractTransfer(ui::Dataspace dataspace) {
+ return static_cast<ui::Dataspace>(dataspace & HAL_DATASPACE_TRANSFER_MASK);
+}
+
+static bool isHdrDataspace(ui::Dataspace dataspace) {
+ const auto transfer = extractTransfer(dataspace);
+
+ return transfer == ui::Dataspace::TRANSFER_ST2084 || transfer == ui::Dataspace::TRANSFER_HLG;
+}
+
+static ui::Dataspace getDataspace(const SkImageInfo& image) {
+ return static_cast<ui::Dataspace>(
+ ColorSpaceToADataSpace(image.colorSpace(), image.colorType()));
+}
+#endif
+
+} // namespace
+
+// Given a source and destination image info, and the max content luminance, generate a tonemaping
+// shader and tag it on the supplied paint.
+void tonemapPaint(const SkImageInfo& source, const SkImageInfo& destination, float maxLuminanceNits,
+ SkPaint& paint) {
+// custom tonemapping only exists on Android devices
+#ifdef __ANDROID__
+ const auto sourceDataspace = getDataspace(source);
+ const auto destinationDataspace = getDataspace(destination);
+
+ if (extractTransfer(sourceDataspace) != extractTransfer(destinationDataspace) &&
+ (isHdrDataspace(sourceDataspace) || isHdrDataspace(destinationDataspace))) {
+ const auto effect = shaders::LinearEffect{
+ .inputDataspace = sourceDataspace,
+ .outputDataspace = destinationDataspace,
+ .undoPremultipliedAlpha = source.alphaType() == kPremul_SkAlphaType,
+ .type = shaders::LinearEffect::SkSLType::ColorFilter};
+ constexpr float kMaxDisplayBrightnessNits = 1000.f;
+ constexpr float kCurrentDisplayBrightnessNits = 500.f;
+ sk_sp<SkColorFilter> colorFilter = createLinearEffectColorFilter(
+ effect, kMaxDisplayBrightnessNits, kCurrentDisplayBrightnessNits, maxLuminanceNits);
+
+ if (paint.getColorFilter()) {
+ paint.setColorFilter(SkColorFilters::Compose(paint.refColorFilter(), colorFilter));
+ } else {
+ paint.setColorFilter(colorFilter);
+ }
+ }
+#else
+ return;
+#endif
+}
+
+} // namespace android::uirenderer
diff --git a/libs/hwui/Tonemapper.h b/libs/hwui/Tonemapper.h
new file mode 100644
index 000000000000..c0d5325fa9f8
--- /dev/null
+++ b/libs/hwui/Tonemapper.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 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.
+ */
+
+#pragma once
+
+#include <SkCanvas.h>
+
+namespace android::uirenderer {
+
+// Given a source and destination image info, and the max content luminance, generate a tonemaping
+// shader and tag it on the supplied paint.
+void tonemapPaint(const SkImageInfo& source, const SkImageInfo& destination, float maxLuminanceNits,
+ SkPaint& paint);
+
+} // namespace android::uirenderer
diff --git a/libs/hwui/TreeInfo.h b/libs/hwui/TreeInfo.h
index 6b8f43946a74..2bff9cb74fa7 100644
--- a/libs/hwui/TreeInfo.h
+++ b/libs/hwui/TreeInfo.h
@@ -123,6 +123,10 @@ public:
// This is used to post a message to redraw when it is time to draw the
// next frame of an AnimatedImageDrawable.
nsecs_t animatedImageDelay = kNoAnimatedImageDelay;
+ // This is used to determine if there were only TextureView updates in this frame.
+ // This info is passed to SurfaceFlinger to determine whether it should use vsyncIds
+ // for refresh rate selection.
+ bool solelyTextureViewUpdates = true;
} out;
// This flag helps to disable projection for receiver nodes that do not have any backward
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index 983c7766273a..536ff781badc 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -21,9 +21,10 @@
#include <utils/Log.h>
#include "PathParser.h"
-#include "SkColorFilter.h"
+#include "SkImage.h"
#include "SkImageInfo.h"
-#include "SkShader.h"
+#include "SkSamplingOptions.h"
+#include "SkScalar.h"
#include "hwui/Paint.h"
#ifdef __ANDROID__
diff --git a/libs/hwui/VectorDrawable.h b/libs/hwui/VectorDrawable.h
index 30bb04ae8361..c92654c479c1 100644
--- a/libs/hwui/VectorDrawable.h
+++ b/libs/hwui/VectorDrawable.h
@@ -31,6 +31,7 @@
#include <SkPath.h>
#include <SkPathMeasure.h>
#include <SkRect.h>
+#include <SkRefCnt.h>
#include <SkShader.h>
#include <SkSurface.h>
diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp
index 942c0506321c..770822a049b7 100644
--- a/libs/hwui/apex/LayoutlibLoader.cpp
+++ b/libs/hwui/apex/LayoutlibLoader.cpp
@@ -53,6 +53,7 @@ extern int register_android_graphics_FontFamily(JNIEnv* env);
extern int register_android_graphics_Matrix(JNIEnv* env);
extern int register_android_graphics_Paint(JNIEnv* env);
extern int register_android_graphics_Path(JNIEnv* env);
+extern int register_android_graphics_PathIterator(JNIEnv* env);
extern int register_android_graphics_PathMeasure(JNIEnv* env);
extern int register_android_graphics_Picture(JNIEnv* env);
extern int register_android_graphics_Region(JNIEnv* env);
@@ -65,6 +66,7 @@ extern int register_android_graphics_fonts_FontFamily(JNIEnv* env);
extern int register_android_graphics_text_LineBreaker(JNIEnv* env);
extern int register_android_graphics_text_MeasuredText(JNIEnv* env);
extern int register_android_graphics_text_TextShaper(JNIEnv* env);
+extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env);
extern int register_android_util_PathParser(JNIEnv* env);
extern int register_android_view_DisplayListCanvas(JNIEnv* env);
@@ -100,6 +102,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = {
{"android.graphics.Paint", REG_JNI(register_android_graphics_Paint)},
{"android.graphics.Path", REG_JNI(register_android_graphics_Path)},
{"android.graphics.PathEffect", REG_JNI(register_android_graphics_PathEffect)},
+ {"android.graphics.PathIterator", REG_JNI(register_android_graphics_PathIterator)},
{"android.graphics.PathMeasure", REG_JNI(register_android_graphics_PathMeasure)},
{"android.graphics.Picture", REG_JNI(register_android_graphics_Picture)},
{"android.graphics.RecordingCanvas", REG_JNI(register_android_view_DisplayListCanvas)},
@@ -123,6 +126,8 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = {
{"android.graphics.text.MeasuredText",
REG_JNI(register_android_graphics_text_MeasuredText)},
{"android.graphics.text.TextRunShaper", REG_JNI(register_android_graphics_text_TextShaper)},
+ {"android.graphics.text.GraphemeBreak",
+ REG_JNI(register_android_graphics_text_GraphemeBreak)},
{"android.util.PathParser", REG_JNI(register_android_util_PathParser)},
};
diff --git a/libs/hwui/apex/android_bitmap.cpp b/libs/hwui/apex/android_bitmap.cpp
index bc6bc456ba5a..c442a7b1d17c 100644
--- a/libs/hwui/apex/android_bitmap.cpp
+++ b/libs/hwui/apex/android_bitmap.cpp
@@ -24,6 +24,11 @@
#include <GraphicsJNI.h>
#include <hwui/Bitmap.h>
+#include <SkBitmap.h>
+#include <SkColorSpace.h>
+#include <SkImageInfo.h>
+#include <SkRefCnt.h>
+#include <SkStream.h>
#include <utils/Color.h>
using namespace android;
diff --git a/libs/hwui/apex/android_canvas.cpp b/libs/hwui/apex/android_canvas.cpp
index 2a939efed9bb..905b123076a2 100644
--- a/libs/hwui/apex/android_canvas.cpp
+++ b/libs/hwui/apex/android_canvas.cpp
@@ -23,7 +23,9 @@
#include <utils/Color.h>
#include <SkBitmap.h>
+#include <SkColorSpace.h>
#include <SkSurface.h>
+#include <SkRefCnt.h>
using namespace android;
diff --git a/libs/hwui/apex/android_paint.cpp b/libs/hwui/apex/android_paint.cpp
index 70bd085343ce..cc79cba5e19c 100644
--- a/libs/hwui/apex/android_paint.cpp
+++ b/libs/hwui/apex/android_paint.cpp
@@ -19,6 +19,7 @@
#include "TypeCast.h"
#include <hwui/Paint.h>
+#include <SkBlendMode.h>
using namespace android;
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index e1f5abd786bf..09ae7e78fe23 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -55,10 +55,12 @@ extern int register_android_graphics_ColorFilter(JNIEnv* env);
extern int register_android_graphics_ColorSpace(JNIEnv* env);
extern int register_android_graphics_DrawFilter(JNIEnv* env);
extern int register_android_graphics_FontFamily(JNIEnv* env);
+extern int register_android_graphics_Gainmap(JNIEnv* env);
extern int register_android_graphics_HardwareRendererObserver(JNIEnv* env);
extern int register_android_graphics_Matrix(JNIEnv* env);
extern int register_android_graphics_Paint(JNIEnv* env);
extern int register_android_graphics_Path(JNIEnv* env);
+extern int register_android_graphics_PathIterator(JNIEnv* env);
extern int register_android_graphics_PathMeasure(JNIEnv* env);
extern int register_android_graphics_Picture(JNIEnv*);
extern int register_android_graphics_Region(JNIEnv* env);
@@ -75,11 +77,15 @@ extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env);
extern int register_android_graphics_text_MeasuredText(JNIEnv* env);
extern int register_android_graphics_text_LineBreaker(JNIEnv *env);
extern int register_android_graphics_text_TextShaper(JNIEnv *env);
+extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env);
+extern int register_android_graphics_MeshSpecification(JNIEnv* env);
+extern int register_android_graphics_Mesh(JNIEnv* env);
extern int register_android_util_PathParser(JNIEnv* env);
extern int register_android_view_DisplayListCanvas(JNIEnv* env);
extern int register_android_view_RenderNode(JNIEnv* env);
extern int register_android_view_ThreadedRenderer(JNIEnv* env);
+extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
#ifdef NDEBUG
#define REG_JNI(name) { name }
@@ -94,59 +100,66 @@ extern int register_android_view_ThreadedRenderer(JNIEnv* env);
};
#endif
-static const RegJNIRec gRegJNI[] = {
- REG_JNI(register_android_graphics_Canvas),
- // This needs to be before register_android_graphics_Graphics, or the latter
- // will not be able to find the jmethodID for ColorSpace.get().
- REG_JNI(register_android_graphics_ColorSpace),
- REG_JNI(register_android_graphics_Graphics),
- REG_JNI(register_android_graphics_Bitmap),
- REG_JNI(register_android_graphics_BitmapFactory),
- REG_JNI(register_android_graphics_BitmapRegionDecoder),
- REG_JNI(register_android_graphics_ByteBufferStreamAdaptor),
- REG_JNI(register_android_graphics_Camera),
- REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor),
- REG_JNI(register_android_graphics_CanvasProperty),
- REG_JNI(register_android_graphics_ColorFilter),
- REG_JNI(register_android_graphics_DrawFilter),
- REG_JNI(register_android_graphics_FontFamily),
- REG_JNI(register_android_graphics_HardwareRendererObserver),
- REG_JNI(register_android_graphics_ImageDecoder),
- REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable),
- REG_JNI(register_android_graphics_Interpolator),
- REG_JNI(register_android_graphics_MaskFilter),
- REG_JNI(register_android_graphics_Matrix),
- REG_JNI(register_android_graphics_Movie),
- REG_JNI(register_android_graphics_NinePatch),
- REG_JNI(register_android_graphics_Paint),
- REG_JNI(register_android_graphics_Path),
- REG_JNI(register_android_graphics_PathMeasure),
- REG_JNI(register_android_graphics_PathEffect),
- REG_JNI(register_android_graphics_Picture),
- REG_JNI(register_android_graphics_Region),
- REG_JNI(register_android_graphics_Shader),
- REG_JNI(register_android_graphics_RenderEffect),
- REG_JNI(register_android_graphics_TextureLayer),
- REG_JNI(register_android_graphics_Typeface),
- REG_JNI(register_android_graphics_YuvImage),
- REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory),
- REG_JNI(register_android_graphics_animation_RenderNodeAnimator),
- REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable),
- REG_JNI(register_android_graphics_drawable_VectorDrawable),
- REG_JNI(register_android_graphics_fonts_Font),
- REG_JNI(register_android_graphics_fonts_FontFamily),
- REG_JNI(register_android_graphics_pdf_PdfDocument),
- REG_JNI(register_android_graphics_pdf_PdfEditor),
- REG_JNI(register_android_graphics_pdf_PdfRenderer),
- REG_JNI(register_android_graphics_text_MeasuredText),
- REG_JNI(register_android_graphics_text_LineBreaker),
- REG_JNI(register_android_graphics_text_TextShaper),
-
- REG_JNI(register_android_util_PathParser),
- REG_JNI(register_android_view_RenderNode),
- REG_JNI(register_android_view_DisplayListCanvas),
- REG_JNI(register_android_view_ThreadedRenderer),
-};
+ static const RegJNIRec gRegJNI[] = {
+ REG_JNI(register_android_graphics_Canvas),
+ // This needs to be before register_android_graphics_Graphics, or the latter
+ // will not be able to find the jmethodID for ColorSpace.get().
+ REG_JNI(register_android_graphics_ColorSpace),
+ REG_JNI(register_android_graphics_Graphics),
+ REG_JNI(register_android_graphics_Bitmap),
+ REG_JNI(register_android_graphics_BitmapFactory),
+ REG_JNI(register_android_graphics_BitmapRegionDecoder),
+ REG_JNI(register_android_graphics_ByteBufferStreamAdaptor),
+ REG_JNI(register_android_graphics_Camera),
+ REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor),
+ REG_JNI(register_android_graphics_CanvasProperty),
+ REG_JNI(register_android_graphics_ColorFilter),
+ REG_JNI(register_android_graphics_DrawFilter),
+ REG_JNI(register_android_graphics_FontFamily),
+ REG_JNI(register_android_graphics_Gainmap),
+ REG_JNI(register_android_graphics_HardwareRendererObserver),
+ REG_JNI(register_android_graphics_ImageDecoder),
+ REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable),
+ REG_JNI(register_android_graphics_Interpolator),
+ REG_JNI(register_android_graphics_MaskFilter),
+ REG_JNI(register_android_graphics_Matrix),
+ REG_JNI(register_android_graphics_Movie),
+ REG_JNI(register_android_graphics_NinePatch),
+ REG_JNI(register_android_graphics_Paint),
+ REG_JNI(register_android_graphics_Path),
+ REG_JNI(register_android_graphics_PathIterator),
+ REG_JNI(register_android_graphics_PathMeasure),
+ REG_JNI(register_android_graphics_PathEffect),
+ REG_JNI(register_android_graphics_Picture),
+ REG_JNI(register_android_graphics_Region),
+ REG_JNI(register_android_graphics_Shader),
+ REG_JNI(register_android_graphics_RenderEffect),
+ REG_JNI(register_android_graphics_TextureLayer),
+ REG_JNI(register_android_graphics_Typeface),
+ REG_JNI(register_android_graphics_YuvImage),
+ REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory),
+ REG_JNI(register_android_graphics_animation_RenderNodeAnimator),
+ REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable),
+ REG_JNI(register_android_graphics_drawable_VectorDrawable),
+ REG_JNI(register_android_graphics_fonts_Font),
+ REG_JNI(register_android_graphics_fonts_FontFamily),
+ REG_JNI(register_android_graphics_pdf_PdfDocument),
+ REG_JNI(register_android_graphics_pdf_PdfEditor),
+ REG_JNI(register_android_graphics_pdf_PdfRenderer),
+ REG_JNI(register_android_graphics_text_MeasuredText),
+ REG_JNI(register_android_graphics_text_LineBreaker),
+ REG_JNI(register_android_graphics_text_TextShaper),
+ REG_JNI(register_android_graphics_text_GraphemeBreak),
+ REG_JNI(register_android_graphics_MeshSpecification),
+ REG_JNI(register_android_graphics_Mesh),
+
+ REG_JNI(register_android_util_PathParser),
+ REG_JNI(register_android_view_RenderNode),
+ REG_JNI(register_android_view_DisplayListCanvas),
+ REG_JNI(register_android_graphics_HardwareBufferRenderer),
+
+ REG_JNI(register_android_view_ThreadedRenderer),
+ };
} // namespace android
diff --git a/libs/hwui/canvas/CanvasOps.h b/libs/hwui/canvas/CanvasOps.h
index fdc97a4fd8ba..2dcbca8273e7 100644
--- a/libs/hwui/canvas/CanvasOps.h
+++ b/libs/hwui/canvas/CanvasOps.h
@@ -17,13 +17,19 @@
#pragma once
#include <SkAndroidFrameworkUtils.h>
+#include <SkBlendMode.h>
#include <SkCanvas.h>
-#include <SkPath.h>
-#include <SkRegion.h>
-#include <SkVertices.h>
+#include <SkClipOp.h>
#include <SkImage.h>
+#include <SkPaint.h>
+#include <SkPath.h>
#include <SkPicture.h>
+#include <SkRRect.h>
+#include <SkRect.h>
+#include <SkRegion.h>
#include <SkRuntimeEffect.h>
+#include <SkSamplingOptions.h>
+#include <SkVertices.h>
#include <log/log.h>
diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp
new file mode 100644
index 000000000000..613f52b32bea
--- /dev/null
+++ b/libs/hwui/effects/GainmapRenderer.cpp
@@ -0,0 +1,304 @@
+/*
+ * 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 "GainmapRenderer.h"
+
+#include <SkGainmapShader.h>
+
+#include "Gainmap.h"
+#include "Rect.h"
+#include "utils/Trace.h"
+
+#ifdef __ANDROID__
+#include "include/core/SkColorSpace.h"
+#include "include/core/SkImage.h"
+#include "include/core/SkShader.h"
+#include "include/effects/SkRuntimeEffect.h"
+#include "include/private/SkGainmapInfo.h"
+#include "renderthread/CanvasContext.h"
+#include "src/core/SkColorFilterPriv.h"
+#include "src/core/SkImageInfoPriv.h"
+#include "src/core/SkRuntimeEffectPriv.h"
+#endif
+
+namespace android::uirenderer {
+
+using namespace renderthread;
+
+float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) {
+ // We should always have a known destination colorspace. If we don't we must be in some
+ // legacy mode where we're lost and also definitely not going to HDR
+ if (destColorspace == nullptr) {
+ return 1.f;
+ }
+
+ constexpr float GenericSdrWhiteNits = 203.f;
+ constexpr float maxPQLux = 10000.f;
+ constexpr float maxHLGLux = 1000.f;
+ skcms_TransferFunction destTF;
+ destColorspace->transferFn(&destTF);
+ if (skcms_TransferFunction_isPQish(&destTF)) {
+ return maxPQLux / GenericSdrWhiteNits;
+ } else if (skcms_TransferFunction_isHLGish(&destTF)) {
+ return maxHLGLux / GenericSdrWhiteNits;
+ } else {
+#ifdef __ANDROID__
+ CanvasContext* context = CanvasContext::getActiveContext();
+ return context ? context->targetSdrHdrRatio() : 1.f;
+#else
+ return 1.f;
+#endif
+ }
+}
+
+void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src,
+ const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint,
+ SkCanvas::SrcRectConstraint constraint,
+ const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo) {
+ ATRACE_CALL();
+#ifdef __ANDROID__
+ auto destColorspace = c->imageInfo().refColorSpace();
+ float targetSdrHdrRatio = getTargetHdrSdrRatio(destColorspace.get());
+ if (targetSdrHdrRatio > 1.f && gainmapImage) {
+ SkPaint gainmapPaint = *paint;
+ float sX = gainmapImage->width() / (float)image->width();
+ float sY = gainmapImage->height() / (float)image->height();
+ SkRect gainmapSrc = src;
+ // TODO: Tweak rounding?
+ gainmapSrc.fLeft *= sX;
+ gainmapSrc.fRight *= sX;
+ gainmapSrc.fTop *= sY;
+ gainmapSrc.fBottom *= sY;
+ auto shader =
+ SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling,
+ gainmapInfo, dst, targetSdrHdrRatio, destColorspace);
+ gainmapPaint.setShader(shader);
+ c->drawRect(dst, gainmapPaint);
+ } else
+#endif
+ c->drawImageRect(image.get(), src, dst, sampling, paint, constraint);
+}
+
+#ifdef __ANDROID__
+
+static constexpr char gGainmapSKSL[] = R"SKSL(
+ uniform shader base;
+ uniform shader gainmap;
+ uniform colorFilter workingSpaceToLinearSrgb;
+ uniform half4 logRatioMin;
+ uniform half4 logRatioMax;
+ uniform half4 gainmapGamma;
+ uniform half4 epsilonSdr;
+ uniform half4 epsilonHdr;
+ uniform half W;
+ uniform int gainmapIsAlpha;
+ uniform int gainmapIsRed;
+ uniform int singleChannel;
+ uniform int noGamma;
+
+ half4 toDest(half4 working) {
+ half4 ls = workingSpaceToLinearSrgb.eval(working);
+ vec3 dest = fromLinearSrgb(ls.rgb);
+ return half4(dest.r, dest.g, dest.b, ls.a);
+ }
+
+ half4 main(float2 coord) {
+ half4 S = base.eval(coord);
+ half4 G = gainmap.eval(coord);
+ if (gainmapIsAlpha == 1) {
+ G = half4(G.a, G.a, G.a, 1.0);
+ }
+ if (gainmapIsRed == 1) {
+ G = half4(G.r, G.r, G.r, 1.0);
+ }
+ if (singleChannel == 1) {
+ half L;
+ if (noGamma == 1) {
+ L = mix(logRatioMin.r, logRatioMax.r, G.r);
+ } else {
+ L = mix(logRatioMin.r, logRatioMax.r, pow(G.r, gainmapGamma.r));
+ }
+ half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
+ return toDest(half4(H.r, H.g, H.b, S.a));
+ } else {
+ half3 L;
+ if (noGamma == 1) {
+ L = mix(logRatioMin.rgb, logRatioMax.rgb, G.rgb);
+ } else {
+ L = mix(logRatioMin.rgb, logRatioMax.rgb, pow(G.rgb, gainmapGamma.rgb));
+ }
+ half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
+ return toDest(half4(H.r, H.g, H.b, S.a));
+ }
+ }
+)SKSL";
+
+static sk_sp<SkRuntimeEffect> gainmap_apply_effect() {
+ static const SkRuntimeEffect* effect = []() -> SkRuntimeEffect* {
+ auto buildResult = SkRuntimeEffect::MakeForShader(SkString(gGainmapSKSL), {});
+ if (buildResult.effect) {
+ return buildResult.effect.release();
+ } else {
+ LOG_ALWAYS_FATAL("Failed to build gainmap shader: %s", buildResult.errorText.c_str());
+ }
+ }();
+ SkASSERT(effect);
+ return sk_ref_sp(effect);
+}
+
+static bool all_channels_equal(const SkColor4f& c) {
+ return c.fR == c.fG && c.fR == c.fB;
+}
+
+class DeferredGainmapShader {
+private:
+ sk_sp<SkRuntimeEffect> mShader{gainmap_apply_effect()};
+ SkRuntimeShaderBuilder mBuilder{mShader};
+ SkGainmapInfo mGainmapInfo;
+ std::mutex mUniformGuard;
+
+ void setupChildren(const sk_sp<const SkImage>& baseImage,
+ const sk_sp<const SkImage>& gainmapImage, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& samplingOptions) {
+ sk_sp<SkColorSpace> baseColorSpace =
+ baseImage->colorSpace() ? baseImage->refColorSpace() : SkColorSpace::MakeSRGB();
+
+ // Determine the color space in which the gainmap math is to be applied.
+ sk_sp<SkColorSpace> gainmapMathColorSpace = baseColorSpace->makeLinearGamma();
+
+ // Create a color filter to transform from the base image's color space to the color space
+ // in which the gainmap is to be applied.
+ auto colorXformSdrToGainmap =
+ SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace);
+
+ // The base image shader will convert into the color space in which the gainmap is applied.
+ auto baseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions)
+ ->makeWithColorFilter(colorXformSdrToGainmap);
+
+ // The gainmap image shader will ignore any color space that the gainmap has.
+ const SkMatrix gainmapRectToDstRect =
+ SkMatrix::RectToRect(SkRect::MakeWH(gainmapImage->width(), gainmapImage->height()),
+ SkRect::MakeWH(baseImage->width(), baseImage->height()));
+ auto gainmapImageShader = gainmapImage->makeRawShader(tileModeX, tileModeY, samplingOptions,
+ &gainmapRectToDstRect);
+
+ // Create a color filter to transform from the color space in which the gainmap is applied
+ // to the intermediate destination color space.
+ auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform(
+ gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear());
+
+ mBuilder.child("base") = std::move(baseImageShader);
+ mBuilder.child("gainmap") = std::move(gainmapImageShader);
+ mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst);
+ }
+
+ void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo) {
+ const SkColor4f logRatioMin({sk_float_log(gainmapInfo.fGainmapRatioMin.fR),
+ sk_float_log(gainmapInfo.fGainmapRatioMin.fG),
+ sk_float_log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
+ const SkColor4f logRatioMax({sk_float_log(gainmapInfo.fGainmapRatioMax.fR),
+ sk_float_log(gainmapInfo.fGainmapRatioMax.fG),
+ sk_float_log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
+ const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f &&
+ gainmapInfo.fGainmapGamma.fG == 1.f &&
+ gainmapInfo.fGainmapGamma.fB == 1.f;
+ const uint32_t colorTypeFlags = SkColorTypeChannelFlags(gainmapImage->colorType());
+ const int gainmapIsAlpha = colorTypeFlags == kAlpha_SkColorChannelFlag;
+ const int gainmapIsRed = colorTypeFlags == kRed_SkColorChannelFlag;
+ const int singleChannel = all_channels_equal(gainmapInfo.fGainmapGamma) &&
+ all_channels_equal(gainmapInfo.fGainmapRatioMin) &&
+ all_channels_equal(gainmapInfo.fGainmapRatioMax) &&
+ (colorTypeFlags == kGray_SkColorChannelFlag ||
+ colorTypeFlags == kAlpha_SkColorChannelFlag ||
+ colorTypeFlags == kRed_SkColorChannelFlag);
+ mBuilder.uniform("logRatioMin") = logRatioMin;
+ mBuilder.uniform("logRatioMax") = logRatioMax;
+ mBuilder.uniform("gainmapGamma") = gainmapInfo.fGainmapGamma;
+ mBuilder.uniform("epsilonSdr") = gainmapInfo.fEpsilonSdr;
+ mBuilder.uniform("epsilonHdr") = gainmapInfo.fEpsilonHdr;
+ mBuilder.uniform("noGamma") = noGamma;
+ mBuilder.uniform("singleChannel") = singleChannel;
+ mBuilder.uniform("gainmapIsAlpha") = gainmapIsAlpha;
+ mBuilder.uniform("gainmapIsRed") = gainmapIsRed;
+ }
+
+ sk_sp<const SkData> build(float targetHdrSdrRatio) {
+ sk_sp<const SkData> uniforms;
+ {
+ // If we are called concurrently from multiple threads, we need to guard the call
+ // to writableUniforms() which mutates mUniform. This is otherwise safe because
+ // writeableUniforms() will make a copy if it's not unique before mutating
+ // This can happen if a BitmapShader is used on multiple canvas', such as a
+ // software + hardware canvas, which is otherwise valid as SkShader is "immutable"
+ std::lock_guard _lock(mUniformGuard);
+ const float Wunclamped = (sk_float_log(targetHdrSdrRatio) -
+ sk_float_log(mGainmapInfo.fDisplayRatioSdr)) /
+ (sk_float_log(mGainmapInfo.fDisplayRatioHdr) -
+ sk_float_log(mGainmapInfo.fDisplayRatioSdr));
+ const float W = std::max(std::min(Wunclamped, 1.f), 0.f);
+ mBuilder.uniform("W") = W;
+ uniforms = mBuilder.uniforms();
+ }
+ return uniforms;
+ }
+
+public:
+ explicit DeferredGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ mGainmapInfo = gainmapInfo;
+ setupChildren(image, gainmapImage, tileModeX, tileModeY, sampling);
+ setupGenericUniforms(gainmapImage, gainmapInfo);
+ }
+
+ static sk_sp<SkShader> Make(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ auto deferredHandler = std::make_shared<DeferredGainmapShader>(
+ image, gainmapImage, gainmapInfo, tileModeX, tileModeY, sampling);
+ auto callback =
+ [deferredHandler](const SkRuntimeEffectPriv::UniformsCallbackContext& renderContext)
+ -> sk_sp<const SkData> {
+ return deferredHandler->build(getTargetHdrSdrRatio(renderContext.fDstColorSpace));
+ };
+ return SkRuntimeEffectPriv::MakeDeferredShader(deferredHandler->mShader.get(), callback,
+ deferredHandler->mBuilder.children());
+ }
+};
+
+sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ return DeferredGainmapShader::Make(image, gainmapImage, gainmapInfo, tileModeX, tileModeY,
+ sampling);
+}
+
+#else // __ANDROID__
+
+sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling) {
+ return nullptr;
+}
+
+#endif // __ANDROID__
+
+} // namespace android::uirenderer \ No newline at end of file
diff --git a/libs/hwui/effects/GainmapRenderer.h b/libs/hwui/effects/GainmapRenderer.h
new file mode 100644
index 000000000000..0ab03f0b571a
--- /dev/null
+++ b/libs/hwui/effects/GainmapRenderer.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <SkCanvas.h>
+#include <SkGainmapInfo.h>
+#include <SkImage.h>
+#include <SkPaint.h>
+
+#include "hwui/Bitmap.h"
+
+namespace android::uirenderer {
+
+float getTargetHdrSdrRatio(const SkColorSpace* destColorspace);
+
+void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src,
+ const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint,
+ SkCanvas::SrcRectConstraint constraint,
+ const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo);
+
+sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
+ const sk_sp<const SkImage>& gainmapImage,
+ const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
+ SkTileMode tileModeY, const SkSamplingOptions& sampling);
+
+} // namespace android::uirenderer
diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp
index d08bc5c583c2..8049dc946c9e 100644
--- a/libs/hwui/hwui/AnimatedImageDrawable.cpp
+++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp
@@ -29,9 +29,10 @@
namespace android {
-AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed)
- : mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed) {
- mTimeToShowNextSnapshot = ms2ns(mSkAnimatedImage->currentFrameDuration());
+AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed,
+ SkEncodedImageFormat format)
+ : mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed), mFormat(format) {
+ mTimeToShowNextSnapshot = ms2ns(currentFrameDuration());
setStagingBounds(mSkAnimatedImage->getBounds());
}
@@ -92,7 +93,7 @@ bool AnimatedImageDrawable::isDirty(nsecs_t* outDelay) {
// directly from mSkAnimatedImage.
lock.unlock();
std::unique_lock imageLock{mImageLock};
- *outDelay = ms2ns(mSkAnimatedImage->currentFrameDuration());
+ *outDelay = ms2ns(currentFrameDuration());
return true;
} else {
// The next snapshot has not yet been decoded, but we've already passed
@@ -109,7 +110,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::decodeNextFrame() {
Snapshot snap;
{
std::unique_lock lock{mImageLock};
- snap.mDurationMS = mSkAnimatedImage->decodeNextFrame();
+ snap.mDurationMS = adjustFrameDuration(mSkAnimatedImage->decodeNextFrame());
snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot());
}
@@ -123,7 +124,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::reset() {
std::unique_lock lock{mImageLock};
mSkAnimatedImage->reset();
snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot());
- snap.mDurationMS = mSkAnimatedImage->currentFrameDuration();
+ snap.mDurationMS = currentFrameDuration();
}
return snap;
@@ -274,7 +275,7 @@ int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) {
{
std::unique_lock lock{mImageLock};
mSkAnimatedImage->reset();
- durationMS = mSkAnimatedImage->currentFrameDuration();
+ durationMS = currentFrameDuration();
}
{
std::unique_lock lock{mSwapLock};
@@ -306,7 +307,7 @@ int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) {
{
std::unique_lock lock{mImageLock};
if (update) {
- durationMS = mSkAnimatedImage->decodeNextFrame();
+ durationMS = adjustFrameDuration(mSkAnimatedImage->decodeNextFrame());
}
canvas->drawDrawable(mSkAnimatedImage.get());
@@ -336,4 +337,20 @@ SkRect AnimatedImageDrawable::onGetBounds() {
return SkRectMakeLargest();
}
+int AnimatedImageDrawable::adjustFrameDuration(int durationMs) {
+ if (durationMs == SkAnimatedImage::kFinished) {
+ return SkAnimatedImage::kFinished;
+ }
+
+ if (mFormat == SkEncodedImageFormat::kGIF) {
+ // Match Chrome & Firefox behavior that gifs with a duration <= 10ms is bumped to 100ms
+ return durationMs <= 10 ? 100 : durationMs;
+ }
+ return durationMs;
+}
+
+int AnimatedImageDrawable::currentFrameDuration() {
+ return adjustFrameDuration(mSkAnimatedImage->currentFrameDuration());
+}
+
} // namespace android
diff --git a/libs/hwui/hwui/AnimatedImageDrawable.h b/libs/hwui/hwui/AnimatedImageDrawable.h
index 8ca3c7e125f1..1e965abc82b5 100644
--- a/libs/hwui/hwui/AnimatedImageDrawable.h
+++ b/libs/hwui/hwui/AnimatedImageDrawable.h
@@ -16,16 +16,16 @@
#pragma once
-#include <cutils/compiler.h>
-#include <utils/Macros.h>
-#include <utils/RefBase.h>
-#include <utils/Timers.h>
-
#include <SkAnimatedImage.h>
#include <SkCanvas.h>
#include <SkColorFilter.h>
#include <SkDrawable.h>
+#include <SkEncodedImageFormat.h>
#include <SkPicture.h>
+#include <cutils/compiler.h>
+#include <utils/Macros.h>
+#include <utils/RefBase.h>
+#include <utils/Timers.h>
#include <future>
#include <mutex>
@@ -48,7 +48,8 @@ class AnimatedImageDrawable : public SkDrawable {
public:
// bytesUsed includes the approximate sizes of the SkAnimatedImage and the SkPictures in the
// Snapshots.
- AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed);
+ AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed,
+ SkEncodedImageFormat format);
/**
* This updates the internal time and returns true if the image needs
@@ -115,6 +116,7 @@ protected:
private:
sk_sp<SkAnimatedImage> mSkAnimatedImage;
const size_t mBytesUsed;
+ const SkEncodedImageFormat mFormat;
bool mRunning = false;
bool mStarting = false;
@@ -157,6 +159,9 @@ private:
Properties mProperties;
std::unique_ptr<OnAnimationEndListener> mEndListener;
+
+ int adjustFrameDuration(int);
+ int currentFrameDuration();
};
} // namespace android
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 67f47580a70f..92d875bf7f1e 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -18,6 +18,10 @@
#include "HardwareBitmapUploader.h"
#include "Properties.h"
#ifdef __ANDROID__ // Layoutlib does not support render thread
+#include <private/android/AHardwareBufferHelpers.h>
+#include <ui/GraphicBuffer.h>
+#include <ui/GraphicBufferMapper.h>
+
#include "renderthread/RenderProxy.h"
#endif
#include "utils/Color.h"
@@ -34,14 +38,51 @@
#include <binder/IServiceManager.h>
#endif
+#include <Gainmap.h>
#include <SkCanvas.h>
+#include <SkColor.h>
+#include <SkEncodedImageFormat.h>
+#include <SkHighContrastFilter.h>
+#include <SkImageEncoder.h>
#include <SkImagePriv.h>
+#include <SkJpegGainmapEncoder.h>
+#include <SkPixmap.h>
+#include <SkRect.h>
+#include <SkStream.h>
#include <SkWebpEncoder.h>
-#include <SkHighContrastFilter.h>
+
#include <limits>
namespace android {
+#ifdef __ANDROID__
+static uint64_t AHardwareBuffer_getAllocationSize(AHardwareBuffer* aHardwareBuffer) {
+ GraphicBuffer* buffer = AHardwareBuffer_to_GraphicBuffer(aHardwareBuffer);
+ auto& mapper = GraphicBufferMapper::get();
+ uint64_t size = 0;
+ auto err = mapper.getAllocationSize(buffer->handle, &size);
+ if (err == OK) {
+ if (size > 0) {
+ return size;
+ } else {
+ ALOGW("Mapper returned size = 0 for buffer format: 0x%x size: %d x %d", buffer->format,
+ buffer->width, buffer->height);
+ // Fall-through to estimate
+ }
+ }
+
+ // Estimation time!
+ // Stride could be = 0 if it's ill-defined (eg, compressed buffer), in which case we use the
+ // width of the buffer instead
+ size = std::max(buffer->width, buffer->stride) * buffer->height;
+ // Require bpp to be at least 1. This is too low for many formats, but it's better than 0
+ // Also while we could make increasingly better estimates, the reality is that mapper@4
+ // should be common enough at this point that we won't ever hit this anyway
+ size *= std::max(1u, bytesPerPixel(buffer->format));
+ return size;
+}
+#endif
+
bool Bitmap::computeAllocationSize(size_t rowBytes, int height, size_t* size) {
return 0 <= height && height <= std::numeric_limits<size_t>::max() &&
!__builtin_mul_overflow(rowBytes, (size_t)height, size) &&
@@ -252,6 +293,7 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes
, mPalette(palette)
, mPaletteGenerationId(getGenerationID()) {
mPixelStorage.hardware.buffer = buffer;
+ mPixelStorage.hardware.size = AHardwareBuffer_getAllocationSize(buffer);
AHardwareBuffer_acquire(buffer);
setImmutable(); // HW bitmaps are always immutable
mImage = SkImage::MakeFromAHardwareBuffer(buffer, mInfo.alphaType(), mInfo.refColorSpace());
@@ -308,6 +350,10 @@ size_t Bitmap::getAllocationByteCount() const {
return mPixelStorage.heap.size;
case PixelStorageType::Ashmem:
return mPixelStorage.ashmem.size;
+#ifdef __ANDROID__
+ case PixelStorageType::Hardware:
+ return mPixelStorage.hardware.size;
+#endif
default:
return rowBytes() * height();
}
@@ -450,6 +496,23 @@ BitmapPalette Bitmap::computePalette(const SkImageInfo& info, const void* addr,
}
bool Bitmap::compress(JavaCompressFormat format, int32_t quality, SkWStream* stream) {
+#ifdef __ANDROID__ // TODO: This isn't built for host for some reason?
+ if (hasGainmap() && format == JavaCompressFormat::Jpeg) {
+ SkBitmap baseBitmap = getSkBitmap();
+ SkBitmap gainmapBitmap = gainmap()->bitmap->getSkBitmap();
+ if (gainmapBitmap.colorType() == SkColorType::kAlpha_8_SkColorType) {
+ SkBitmap greyGainmap;
+ auto greyInfo = gainmapBitmap.info().makeColorType(SkColorType::kGray_8_SkColorType);
+ greyGainmap.setInfo(greyInfo, gainmapBitmap.rowBytes());
+ greyGainmap.setPixelRef(sk_ref_sp(gainmapBitmap.pixelRef()), 0, 0);
+ gainmapBitmap = std::move(greyGainmap);
+ }
+ SkJpegEncoder::Options options{.fQuality = quality};
+ return SkJpegGainmapEncoder::EncodeHDRGM(stream, baseBitmap.pixmap(), options,
+ gainmapBitmap.pixmap(), options, gainmap()->info);
+ }
+#endif
+
SkBitmap skbitmap;
getSkBitmap(&skbitmap);
return compress(skbitmap, format, quality, stream);
@@ -488,4 +551,14 @@ bool Bitmap::compress(const SkBitmap& bitmap, JavaCompressFormat format,
return SkEncodeImage(stream, bitmap, fm, quality);
}
+
+sp<uirenderer::Gainmap> Bitmap::gainmap() const {
+ LOG_ALWAYS_FATAL_IF(!hasGainmap(), "Bitmap doesn't have a gainmap");
+ return mGainmap;
+}
+
+void Bitmap::setGainmap(sp<uirenderer::Gainmap>&& gainmap) {
+ mGainmap = std::move(gainmap);
+}
+
} // namespace android
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index 94a047c06ced..dd344e2f5517 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -19,10 +19,14 @@
#include <SkColorFilter.h>
#include <SkColorSpace.h>
#include <SkImage.h>
-#include <SkImage.h>
#include <SkImageInfo.h>
#include <SkPixelRef.h>
+#include <SkRefCnt.h>
#include <cutils/compiler.h>
+#include <utils/StrongPointer.h>
+
+#include <optional>
+
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
#include <android/hardware_buffer.h>
#endif
@@ -47,6 +51,7 @@ enum class BitmapPalette {
};
namespace uirenderer {
+class Gainmap;
namespace renderthread {
class RenderThread;
}
@@ -119,6 +124,11 @@ public:
void getBounds(SkRect* bounds) const;
bool isHardware() const { return mPixelStorageType == PixelStorageType::Hardware; }
+ bool hasGainmap() const { return mGainmap.get() != nullptr; }
+
+ sp<uirenderer::Gainmap> gainmap() const;
+
+ void setGainmap(sp<uirenderer::Gainmap>&& gainmap);
PixelStorageType pixelStorageType() const { return mPixelStorageType; }
@@ -193,6 +203,8 @@ private:
bool mHasHardwareMipMap = false;
+ sp<uirenderer::Gainmap> mGainmap;
+
union {
struct {
SkPixelRef* pixelRef;
@@ -209,6 +221,7 @@ private:
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
struct {
AHardwareBuffer* buffer;
+ uint64_t size;
} hardware;
#endif
} mPixelStorage;
diff --git a/libs/hwui/hwui/BlurDrawLooper.cpp b/libs/hwui/hwui/BlurDrawLooper.cpp
index 270d24af99fd..8b52551fc107 100644
--- a/libs/hwui/hwui/BlurDrawLooper.cpp
+++ b/libs/hwui/hwui/BlurDrawLooper.cpp
@@ -15,6 +15,8 @@
*/
#include "BlurDrawLooper.h"
+#include <SkBlurTypes.h>
+#include <SkColorSpace.h>
#include <SkMaskFilter.h>
namespace android {
diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp
index b046f45d9c57..cd8af3d933b1 100644
--- a/libs/hwui/hwui/Canvas.cpp
+++ b/libs/hwui/hwui/Canvas.cpp
@@ -26,6 +26,7 @@
#include "hwui/PaintFilter.h"
#include <SkFontMetrics.h>
+#include <SkRRect.h>
namespace android {
diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h
index 82777646f3a2..44ee31d34d23 100644
--- a/libs/hwui/hwui/Canvas.h
+++ b/libs/hwui/hwui/Canvas.h
@@ -16,23 +16,25 @@
#pragma once
+#include <SaveFlags.h>
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkMatrix.h>
+#include <androidfw/ResourceTypes.h>
#include <cutils/compiler.h>
#include <utils/Functor.h>
-#include <SaveFlags.h>
-#include <androidfw/ResourceTypes.h>
#include "Properties.h"
#include "pipeline/skia/AnimatedDrawables.h"
#include "utils/Macros.h"
-#include <SkBitmap.h>
-#include <SkCanvas.h>
-#include <SkMatrix.h>
-
class SkAnimatedImage;
+enum class SkBlendMode;
class SkCanvasState;
+class SkRRect;
class SkRuntimeShaderBuilder;
class SkVertices;
+class Mesh;
namespace minikin {
class Font;
@@ -151,7 +153,7 @@ public:
LOG_ALWAYS_FATAL("Not supported");
}
- virtual void punchHole(const SkRRect& rect) = 0;
+ virtual void punchHole(const SkRRect& rect, float alpha) = 0;
// ----------------------------------------------------------------------------
// Canvas state operations
@@ -225,6 +227,7 @@ public:
float sweepAngle, bool useCenter, const Paint& paint) = 0;
virtual void drawPath(const SkPath& path, const Paint& paint) = 0;
virtual void drawVertices(const SkVertices*, SkBlendMode, const Paint& paint) = 0;
+ virtual void drawMesh(const Mesh& mesh, sk_sp<SkBlender>, const Paint& paint) = 0;
// Bitmap-based
virtual void drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) = 0;
diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp
index dd68f825b61d..701a87f0cce4 100644
--- a/libs/hwui/hwui/ImageDecoder.cpp
+++ b/libs/hwui/hwui/ImageDecoder.cpp
@@ -16,15 +16,32 @@
#include "ImageDecoder.h"
-#include <hwui/Bitmap.h>
-#include <log/log.h>
-
+#include <Gainmap.h>
+#include <SkAlphaType.h>
#include <SkAndroidCodec.h>
#include <SkBitmap.h>
#include <SkBlendMode.h>
#include <SkCanvas.h>
+#include <SkCodec.h>
+#include <SkCodecAnimation.h>
+#include <SkColorSpace.h>
+#include <SkColorType.h>
#include <SkEncodedOrigin.h>
+#include <SkImageInfo.h>
+#include <SkGainmapInfo.h>
+#include <SkMatrix.h>
#include <SkPaint.h>
+#include <SkPngChunkReader.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
+#include <SkSamplingOptions.h>
+#include <SkSize.h>
+#include <SkStream.h>
+#include <hwui/Bitmap.h>
+#include <log/log.h>
+#include <utils/Trace.h>
+
+#include <memory>
#undef LOG_TAG
#define LOG_TAG "ImageDecoder"
@@ -34,6 +51,9 @@ using namespace android;
sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const {
const skcms_ICCProfile* encodedProfile = mCodec->getICCProfile();
if (encodedProfile) {
+ if (encodedProfile->has_CICP) {
+ return mCodec->computeOutputColorSpace(kN32_SkColorType);
+ }
// If the profile maps directly to an SkColorSpace, that SkColorSpace
// will be returned. Otherwise, nullptr will be returned. In either
// case, using this SkColorSpace results in doing no color correction.
@@ -195,7 +215,7 @@ SkImageInfo ImageDecoder::getOutputInfo() const {
}
bool ImageDecoder::swapWidthHeight() const {
- return SkEncodedOriginSwapsWidthHeight(mCodec->codec()->getOrigin());
+ return SkEncodedOriginSwapsWidthHeight(getOrigin());
}
int ImageDecoder::width() const {
@@ -316,7 +336,7 @@ SkCodec::FrameInfo ImageDecoder::getCurrentFrameInfo() {
info.fFrameRect = SkIRect::MakeSize(dims);
}
- if (auto origin = mCodec->codec()->getOrigin(); origin != kDefault_SkEncodedOrigin) {
+ if (auto origin = getOrigin(); origin != kDefault_SkEncodedOrigin) {
if (SkEncodedOriginSwapsWidthHeight(origin)) {
dims = swapped(dims);
}
@@ -400,7 +420,7 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) {
// FIXME: Use scanline decoding on only a couple lines to save memory. b/70709380.
SkBitmap tmp;
const bool scale = mDecodeSize != mTargetSize;
- const auto origin = mCodec->codec()->getOrigin();
+ const auto origin = getOrigin();
const bool handleOrigin = origin != kDefault_SkEncodedOrigin;
SkMatrix outputMatrix;
if (scale || handleOrigin || mCropRect) {
@@ -455,12 +475,15 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) {
mOptions.fZeroInitialized = SkCodec::kYes_ZeroInitialized;
}
+ ATRACE_BEGIN("getAndroidPixels");
auto result = mCodec->getAndroidPixels(decodeInfo, decodePixels, decodeRowBytes, &mOptions);
+ ATRACE_END();
// The next call to decode() may not provide zero initialized memory.
mOptions.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
if (scale || handleOrigin || mCropRect) {
+ ATRACE_NAME("Handling scale/origin/crop");
SkBitmap scaledBm;
if (!scaledBm.installPixels(outputInfo, pixels, rowBytes)) {
return SkCodec::kInternalError;
@@ -478,3 +501,82 @@ SkCodec::Result ImageDecoder::decode(void* pixels, size_t rowBytes) {
return result;
}
+SkCodec::Result ImageDecoder::extractGainmap(Bitmap* destination, bool isShared) {
+ ATRACE_CALL();
+ SkGainmapInfo gainmapInfo;
+ std::unique_ptr<SkStream> gainmapStream;
+ {
+ ATRACE_NAME("getAndroidGainmap");
+ if (!mCodec->getAndroidGainmap(&gainmapInfo, &gainmapStream)) {
+ return SkCodec::kSuccess;
+ }
+ }
+ auto gainmapCodec = SkAndroidCodec::MakeFromStream(std::move(gainmapStream));
+ if (!gainmapCodec) {
+ ALOGW("Failed to create codec for gainmap stream");
+ return SkCodec::kInvalidInput;
+ }
+ ImageDecoder decoder{std::move(gainmapCodec)};
+ // Gainmap inherits the origin of the containing image
+ decoder.mOverrideOrigin.emplace(getOrigin());
+ // Update mDecodeSize / mTargetSize for the overridden origin
+ decoder.setTargetSize(decoder.width(), decoder.height());
+ if (decoder.gray()) {
+ decoder.setOutColorType(kGray_8_SkColorType);
+ }
+
+ const bool isScaled = width() != mTargetSize.width() || height() != mTargetSize.height();
+
+ if (isScaled) {
+ float scaleX = (float)mTargetSize.width() / width();
+ float scaleY = (float)mTargetSize.height() / height();
+ decoder.setTargetSize(decoder.width() * scaleX, decoder.height() * scaleY);
+ }
+
+ if (mCropRect) {
+ float sX = decoder.mTargetSize.width() / (float)mTargetSize.width();
+ float sY = decoder.mTargetSize.height() / (float)mTargetSize.height();
+ SkIRect crop = *mCropRect;
+ // TODO: Tweak rounding?
+ crop.fLeft *= sX;
+ crop.fRight *= sX;
+ crop.fTop *= sY;
+ crop.fBottom *= sY;
+ decoder.setCropRect(&crop);
+ }
+
+ SkImageInfo bitmapInfo = decoder.getOutputInfo();
+ if (bitmapInfo.colorType() == kGray_8_SkColorType) {
+ bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType);
+ }
+
+ SkBitmap bm;
+ if (!bm.setInfo(bitmapInfo)) {
+ ALOGE("Failed to setInfo properly");
+ return SkCodec::kInternalError;
+ }
+
+ sk_sp<Bitmap> nativeBitmap;
+ if (isShared) {
+ nativeBitmap = Bitmap::allocateAshmemBitmap(&bm);
+ } else {
+ nativeBitmap = Bitmap::allocateHeapBitmap(&bm);
+ }
+ if (!nativeBitmap) {
+ ALOGE("OOM allocating Bitmap with dimensions %i x %i", bitmapInfo.width(),
+ bitmapInfo.height());
+ return SkCodec::kInternalError;
+ }
+
+ SkCodec::Result result = decoder.decode(bm.getPixels(), bm.rowBytes());
+ bm.setImmutable();
+
+ if (result == SkCodec::kSuccess) {
+ auto gainmap = sp<uirenderer::Gainmap>::make();
+ gainmap->info = gainmapInfo;
+ gainmap->bitmap = std::move(nativeBitmap);
+ destination->setGainmap(std::move(gainmap));
+ }
+
+ return result;
+}
diff --git a/libs/hwui/hwui/ImageDecoder.h b/libs/hwui/hwui/ImageDecoder.h
index cef2233fc371..b3781b52a418 100644
--- a/libs/hwui/hwui/ImageDecoder.h
+++ b/libs/hwui/hwui/ImageDecoder.h
@@ -17,9 +17,11 @@
#include <SkAndroidCodec.h>
#include <SkCodec.h>
+#include <SkColorSpace.h>
#include <SkImageInfo.h>
#include <SkPngChunkReader.h>
#include <SkRect.h>
+#include <SkRefCnt.h>
#include <SkSize.h>
#include <cutils/compiler.h>
@@ -77,6 +79,8 @@ public:
// Set whether the ImageDecoder should handle RestorePrevious frames.
void setHandleRestorePrevious(bool handle);
+ SkCodec::Result extractGainmap(Bitmap* destination, bool isShared);
+
private:
// State machine for keeping track of how to handle RestorePrevious (RP)
// frames in decode().
@@ -113,6 +117,7 @@ private:
RestoreState mRestoreState;
sk_sp<Bitmap> mRestoreFrame;
std::optional<SkIRect> mCropRect;
+ std::optional<SkEncodedOrigin> mOverrideOrigin;
ImageDecoder(const ImageDecoder&) = delete;
ImageDecoder& operator=(const ImageDecoder&) = delete;
@@ -122,6 +127,10 @@ private:
bool swapWidthHeight() const;
// Store/restore a frame if necessary. Returns false on error.
bool handleRestorePrevious(const SkImageInfo&, void* pixels, size_t rowBytes);
+
+ SkEncodedOrigin getOrigin() const {
+ return mOverrideOrigin.has_value() ? *mOverrideOrigin : mCodec->codec()->getOrigin();
+ }
};
} // namespace android
diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp
index 2db3ace1cd43..34cb4aef70d9 100644
--- a/libs/hwui/hwui/MinikinSkia.cpp
+++ b/libs/hwui/hwui/MinikinSkia.cpp
@@ -16,10 +16,13 @@
#include "MinikinSkia.h"
-#include <SkFontDescriptor.h>
#include <SkFont.h>
+#include <SkFontDescriptor.h>
#include <SkFontMetrics.h>
#include <SkFontMgr.h>
+#include <SkRect.h>
+#include <SkScalar.h>
+#include <SkStream.h>
#include <SkTypeface.h>
#include <log/log.h>
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index 5a9d2508230e..3c67edc9a428 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -125,9 +125,14 @@ Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) {
}
Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families,
- int weight, int italic) {
+ int weight, int italic, const Typeface* fallback) {
Typeface* result = new Typeface;
- result->fFontCollection.reset(new minikin::FontCollection(families));
+ if (fallback == nullptr) {
+ result->fFontCollection = minikin::FontCollection::create(std::move(families));
+ } else {
+ result->fFontCollection =
+ fallback->fFontCollection->createCollectionWithFamilies(std::move(families));
+ }
if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) {
int weightFromFont;
@@ -191,8 +196,8 @@ void Typeface::setRobotoTypefaceForTest() {
std::vector<std::shared_ptr<minikin::Font>> fonts;
fonts.push_back(minikin::Font::Builder(font).build());
- std::shared_ptr<minikin::FontCollection> collection = std::make_shared<minikin::FontCollection>(
- std::make_shared<minikin::FontFamily>(std::move(fonts)));
+ std::shared_ptr<minikin::FontCollection> collection =
+ minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts)));
Typeface* hwTypeface = new Typeface();
hwTypeface->fFontCollection = collection;
diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h
index 0c3ef01ab26b..565136e53676 100644
--- a/libs/hwui/hwui/Typeface.h
+++ b/libs/hwui/hwui/Typeface.h
@@ -78,7 +78,8 @@ public:
Typeface* src, const std::vector<minikin::FontVariation>& variations);
static Typeface* createFromFamilies(
- std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic);
+ std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic,
+ const Typeface* fallback);
static void setDefault(const Typeface* face);
diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp
index c40b858268be..a7f5aa83e624 100644
--- a/libs/hwui/jni/AnimatedImageDrawable.cpp
+++ b/libs/hwui/jni/AnimatedImageDrawable.cpp
@@ -21,8 +21,11 @@
#include <SkAndroidCodec.h>
#include <SkAnimatedImage.h>
#include <SkColorFilter.h>
+#include <SkEncodedImageFormat.h>
#include <SkPicture.h>
#include <SkPictureRecorder.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
#include <hwui/AnimatedImageDrawable.h>
#include <hwui/ImageDecoder.h>
#include <hwui/Canvas.h>
@@ -94,7 +97,7 @@ static jlong AnimatedImageDrawable_nCreate(JNIEnv* env, jobject /*clazz*/,
bytesUsed += picture->approximateBytesUsed();
}
-
+ SkEncodedImageFormat format = imageDecoder->mCodec->getEncodedFormat();
sk_sp<SkAnimatedImage> animatedImg = SkAnimatedImage::Make(std::move(imageDecoder->mCodec),
info, subset,
std::move(picture));
@@ -105,8 +108,8 @@ static jlong AnimatedImageDrawable_nCreate(JNIEnv* env, jobject /*clazz*/,
bytesUsed += sizeof(animatedImg.get());
- sk_sp<AnimatedImageDrawable> drawable(new AnimatedImageDrawable(std::move(animatedImg),
- bytesUsed));
+ sk_sp<AnimatedImageDrawable> drawable(
+ new AnimatedImageDrawable(std::move(animatedImg), bytesUsed, format));
return reinterpret_cast<jlong>(drawable.release());
}
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index 5db0783cf83e..6ee7576651f2 100755..100644
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -1,41 +1,40 @@
#undef LOG_TAG
#define LOG_TAG "Bitmap"
+// #define LOG_NDEBUG 0
#include "Bitmap.h"
+#include <hwui/Bitmap.h>
+#include <hwui/Paint.h>
+
+#include "CreateJavaOutputStreamAdaptor.h"
+#include "Gainmap.h"
+#include "GraphicsJNI.h"
+#include "HardwareBufferHelpers.h"
+#include "ScopedParcel.h"
#include "SkBitmap.h"
+#include "SkBlendMode.h"
#include "SkCanvas.h"
#include "SkColor.h"
#include "SkColorSpace.h"
-#include "SkPixelRef.h"
-#include "SkImageEncoder.h"
+#include "SkData.h"
#include "SkImageInfo.h"
-#include "GraphicsJNI.h"
+#include "SkPaint.h"
+#include "SkPixmap.h"
+#include "SkPoint.h"
+#include "SkRefCnt.h"
#include "SkStream.h"
-#include "SkWebpEncoder.h"
-
+#include "SkTypes.h"
#include "android_nio_utils.h"
-#include "CreateJavaOutputStreamAdaptor.h"
-#include <hwui/Paint.h>
-#include <hwui/Bitmap.h>
-#include <utils/Color.h>
#ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread
#include <android-base/unique_fd.h>
-#include <android/binder_parcel.h>
-#include <android/binder_parcel_jni.h>
-#include <android/binder_parcel_platform.h>
-#include <android/binder_parcel_utils.h>
-#include <private/android/AHardwareBufferHelpers.h>
-#include <cutils/ashmem.h>
-#include <dlfcn.h>
#include <renderthread/RenderProxy.h>
-#include <sys/mman.h>
#endif
#include <inttypes.h>
#include <string.h>
+
#include <memory>
-#include <string>
#define DEBUG_PARCEL 0
@@ -46,6 +45,8 @@ static jmethodID gBitmap_reinitMethodID;
namespace android {
+jobject Gainmap_extractFromBitmap(JNIEnv* env, const Bitmap& bitmap);
+
class BitmapWrapper {
public:
explicit BitmapWrapper(Bitmap* bitmap)
@@ -368,15 +369,28 @@ static bool bitmapCopyTo(SkBitmap* dst, SkColorType dstCT, const SkBitmap& src,
return srcPM.readPixels(dstPM);
}
-static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle,
- jint dstConfigHandle, jboolean isMutable) {
+static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle, jint dstConfigHandle,
+ jboolean isMutable) {
+ LocalScopedBitmap bitmapHolder(srcHandle);
+ if (!bitmapHolder.valid()) {
+ return NULL;
+ }
+ const Bitmap& original = bitmapHolder->bitmap();
+ const bool hasGainmap = original.hasGainmap();
SkBitmap src;
- reinterpret_cast<BitmapWrapper*>(srcHandle)->getSkBitmap(&src);
+ bitmapHolder->getSkBitmap(&src);
+
if (dstConfigHandle == GraphicsJNI::hardwareLegacyBitmapConfig()) {
sk_sp<Bitmap> bitmap(Bitmap::allocateHardwareBitmap(src));
if (!bitmap.get()) {
return NULL;
}
+ if (hasGainmap) {
+ auto gm = uirenderer::Gainmap::allocateHardwareGainmap(original.gainmap());
+ if (gm) {
+ bitmap->setGainmap(std::move(gm));
+ }
+ }
return createBitmap(env, bitmap.release(), getPremulBitmapCreateFlags(isMutable));
}
@@ -388,6 +402,18 @@ static jobject Bitmap_copy(JNIEnv* env, jobject, jlong srcHandle,
return NULL;
}
auto bitmap = allocator.getStorageObjAndReset();
+ if (hasGainmap) {
+ auto gainmap = sp<uirenderer::Gainmap>::make();
+ gainmap->info = original.gainmap()->info;
+ const SkBitmap skSrcBitmap = original.gainmap()->bitmap->getSkBitmap();
+ SkBitmap skDestBitmap;
+ HeapAllocator destAllocator;
+ if (!bitmapCopyTo(&skDestBitmap, dstCT, skSrcBitmap, &destAllocator)) {
+ return NULL;
+ }
+ gainmap->bitmap = sk_sp<Bitmap>(destAllocator.getStorageObjAndReset());
+ bitmap->setGainmap(std::move(gainmap));
+ }
return createBitmap(env, bitmap, getPremulBitmapCreateFlags(isMutable));
}
@@ -421,6 +447,11 @@ static jobject Bitmap_copyAshmemConfig(JNIEnv* env, jobject, jlong srcHandle, ji
return ret;
}
+static jint Bitmap_getAshmemFd(JNIEnv* env, jobject, jlong bitmapHandle) {
+ LocalScopedBitmap bitmap(bitmapHandle);
+ return (bitmap.valid()) ? bitmap->bitmap().getAshmemFd() : -1;
+}
+
static void Bitmap_destruct(BitmapWrapper* bitmap) {
delete bitmap;
}
@@ -576,91 +607,7 @@ static void Bitmap_setHasMipMap(JNIEnv* env, jobject, jlong bitmapHandle,
///////////////////////////////////////////////////////////////////////////////
// TODO: Move somewhere else
-#ifdef __ANDROID__ // Layoutlib does not support parcel
-
-class ScopedParcel {
-public:
- explicit ScopedParcel(JNIEnv* env, jobject parcel) {
- mParcel = AParcel_fromJavaParcel(env, parcel);
- }
-
- ~ScopedParcel() { AParcel_delete(mParcel); }
-
- int32_t readInt32() {
- int32_t temp = 0;
- // TODO: This behavior-matches what android::Parcel does
- // but this should probably be better
- if (AParcel_readInt32(mParcel, &temp) != STATUS_OK) {
- temp = 0;
- }
- return temp;
- }
-
- uint32_t readUint32() {
- uint32_t temp = 0;
- // TODO: This behavior-matches what android::Parcel does
- // but this should probably be better
- if (AParcel_readUint32(mParcel, &temp) != STATUS_OK) {
- temp = 0;
- }
- return temp;
- }
-
- void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); }
-
- void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); }
-
- bool allowFds() const { return AParcel_getAllowFds(mParcel); }
-
- std::optional<sk_sp<SkData>> readData() {
- struct Data {
- void* ptr = nullptr;
- size_t size = 0;
- } data;
- auto error = AParcel_readByteArray(mParcel, &data,
- [](void* arrayData, int32_t length,
- int8_t** outBuffer) -> bool {
- Data* data = reinterpret_cast<Data*>(arrayData);
- if (length > 0) {
- data->ptr = sk_malloc_canfail(length);
- if (!data->ptr) {
- return false;
- }
- *outBuffer =
- reinterpret_cast<int8_t*>(data->ptr);
- data->size = length;
- }
- return true;
- });
- if (error != STATUS_OK || data.size <= 0) {
- sk_free(data.ptr);
- return std::nullopt;
- } else {
- return SkData::MakeFromMalloc(data.ptr, data.size);
- }
- }
-
- void writeData(const std::optional<sk_sp<SkData>>& optData) {
- if (optData) {
- const auto& data = *optData;
- AParcel_writeByteArray(mParcel, reinterpret_cast<const int8_t*>(data->data()),
- data->size());
- } else {
- AParcel_writeByteArray(mParcel, nullptr, -1);
- }
- }
-
- AParcel* get() { return mParcel; }
-
-private:
- AParcel* mParcel;
-};
-
-enum class BlobType : int32_t {
- IN_PLACE,
- ASHMEM,
-};
-
+#ifdef __ANDROID__ // Layoutlib does not support parcel
#define ON_ERROR_RETURN(X) \
if ((error = (X)) != STATUS_OK) return error
@@ -1189,18 +1136,11 @@ static jobject Bitmap_copyPreserveInternalConfig(JNIEnv* env, jobject, jlong bit
return createBitmap(env, bitmap.release(), getPremulBitmapCreateFlags(false));
}
-#ifdef __ANDROID__ // Layoutlib does not support graphic buffer
-typedef AHardwareBuffer* (*AHB_from_HB)(JNIEnv*, jobject);
-AHB_from_HB AHardwareBuffer_fromHardwareBuffer;
-
-typedef jobject (*AHB_to_HB)(JNIEnv*, AHardwareBuffer*);
-AHB_to_HB AHardwareBuffer_toHardwareBuffer;
-#endif
-
static jobject Bitmap_wrapHardwareBufferBitmap(JNIEnv* env, jobject, jobject hardwareBuffer,
jlong colorSpacePtr) {
#ifdef __ANDROID__ // Layoutlib does not support graphic buffer
- AHardwareBuffer* buffer = AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer);
+ AHardwareBuffer* buffer = uirenderer::HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(
+ env, hardwareBuffer);
sk_sp<Bitmap> bitmap = Bitmap::createFrom(buffer,
GraphicsJNI::getNativeColorSpace(colorSpacePtr));
if (!bitmap.get()) {
@@ -1223,7 +1163,8 @@ static jobject Bitmap_getHardwareBuffer(JNIEnv* env, jobject, jlong bitmapPtr) {
}
Bitmap& bitmap = bitmapHandle->bitmap();
- return AHardwareBuffer_toHardwareBuffer(env, bitmap.hardwareBuffer());
+ return uirenderer::HardwareBufferHelpers::AHardwareBuffer_toHardwareBuffer(
+ env, bitmap.hardwareBuffer());
#else
return nullptr;
#endif
@@ -1251,67 +1192,85 @@ static void Bitmap_setImmutable(JNIEnv* env, jobject, jlong bitmapHandle) {
return bitmapHolder->bitmap().setImmutable();
}
+static jboolean Bitmap_hasGainmap(CRITICAL_JNI_PARAMS_COMMA jlong bitmapHandle) {
+ LocalScopedBitmap bitmapHolder(bitmapHandle);
+ if (!bitmapHolder.valid()) return false;
+
+ return bitmapHolder->bitmap().hasGainmap();
+}
+
+static jobject Bitmap_extractGainmap(JNIEnv* env, jobject, jlong bitmapHandle) {
+ LocalScopedBitmap bitmapHolder(bitmapHandle);
+ if (!bitmapHolder.valid()) return nullptr;
+ if (!bitmapHolder->bitmap().hasGainmap()) return nullptr;
+
+ return Gainmap_extractFromBitmap(env, bitmapHolder->bitmap());
+}
+
+static void Bitmap_setGainmap(JNIEnv*, jobject, jlong bitmapHandle, jlong gainmapPtr) {
+ LocalScopedBitmap bitmapHolder(bitmapHandle);
+ if (!bitmapHolder.valid()) return;
+ uirenderer::Gainmap* gainmap = reinterpret_cast<uirenderer::Gainmap*>(gainmapPtr);
+ bitmapHolder->bitmap().setGainmap(sp<uirenderer::Gainmap>::fromExisting(gainmap));
+}
+
///////////////////////////////////////////////////////////////////////////////
static const JNINativeMethod gBitmapMethods[] = {
- { "nativeCreate", "([IIIIIIZJ)Landroid/graphics/Bitmap;",
- (void*)Bitmap_creator },
- { "nativeCopy", "(JIZ)Landroid/graphics/Bitmap;",
- (void*)Bitmap_copy },
- { "nativeCopyAshmem", "(J)Landroid/graphics/Bitmap;",
- (void*)Bitmap_copyAshmem },
- { "nativeCopyAshmemConfig", "(JI)Landroid/graphics/Bitmap;",
- (void*)Bitmap_copyAshmemConfig },
- { "nativeGetNativeFinalizer", "()J", (void*)Bitmap_getNativeFinalizer },
- { "nativeRecycle", "(J)V", (void*)Bitmap_recycle },
- { "nativeReconfigure", "(JIIIZ)V", (void*)Bitmap_reconfigure },
- { "nativeCompress", "(JIILjava/io/OutputStream;[B)Z",
- (void*)Bitmap_compress },
- { "nativeErase", "(JI)V", (void*)Bitmap_erase },
- { "nativeErase", "(JJJ)V", (void*)Bitmap_eraseLong },
- { "nativeRowBytes", "(J)I", (void*)Bitmap_rowBytes },
- { "nativeConfig", "(J)I", (void*)Bitmap_config },
- { "nativeHasAlpha", "(J)Z", (void*)Bitmap_hasAlpha },
- { "nativeIsPremultiplied", "(J)Z", (void*)Bitmap_isPremultiplied},
- { "nativeSetHasAlpha", "(JZZ)V", (void*)Bitmap_setHasAlpha},
- { "nativeSetPremultiplied", "(JZ)V", (void*)Bitmap_setPremultiplied},
- { "nativeHasMipMap", "(J)Z", (void*)Bitmap_hasMipMap },
- { "nativeSetHasMipMap", "(JZ)V", (void*)Bitmap_setHasMipMap },
- { "nativeCreateFromParcel",
- "(Landroid/os/Parcel;)Landroid/graphics/Bitmap;",
- (void*)Bitmap_createFromParcel },
- { "nativeWriteToParcel", "(JILandroid/os/Parcel;)Z",
- (void*)Bitmap_writeToParcel },
- { "nativeExtractAlpha", "(JJ[I)Landroid/graphics/Bitmap;",
- (void*)Bitmap_extractAlpha },
- { "nativeGenerationId", "(J)I", (void*)Bitmap_getGenerationId },
- { "nativeGetPixel", "(JII)I", (void*)Bitmap_getPixel },
- { "nativeGetColor", "(JII)J", (void*)Bitmap_getColor },
- { "nativeGetPixels", "(J[IIIIIII)V", (void*)Bitmap_getPixels },
- { "nativeSetPixel", "(JIII)V", (void*)Bitmap_setPixel },
- { "nativeSetPixels", "(J[IIIIIII)V", (void*)Bitmap_setPixels },
- { "nativeCopyPixelsToBuffer", "(JLjava/nio/Buffer;)V",
- (void*)Bitmap_copyPixelsToBuffer },
- { "nativeCopyPixelsFromBuffer", "(JLjava/nio/Buffer;)V",
- (void*)Bitmap_copyPixelsFromBuffer },
- { "nativeSameAs", "(JJ)Z", (void*)Bitmap_sameAs },
- { "nativePrepareToDraw", "(J)V", (void*)Bitmap_prepareToDraw },
- { "nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount },
- { "nativeCopyPreserveInternalConfig", "(J)Landroid/graphics/Bitmap;",
- (void*)Bitmap_copyPreserveInternalConfig },
- { "nativeWrapHardwareBufferBitmap", "(Landroid/hardware/HardwareBuffer;J)Landroid/graphics/Bitmap;",
- (void*) Bitmap_wrapHardwareBufferBitmap },
- { "nativeGetHardwareBuffer", "(J)Landroid/hardware/HardwareBuffer;",
- (void*) Bitmap_getHardwareBuffer },
- { "nativeComputeColorSpace", "(J)Landroid/graphics/ColorSpace;", (void*)Bitmap_computeColorSpace },
- { "nativeSetColorSpace", "(JJ)V", (void*)Bitmap_setColorSpace },
- { "nativeIsSRGB", "(J)Z", (void*)Bitmap_isSRGB },
- { "nativeIsSRGBLinear", "(J)Z", (void*)Bitmap_isSRGBLinear},
- { "nativeSetImmutable", "(J)V", (void*)Bitmap_setImmutable},
-
- // ------------ @CriticalNative ----------------
- { "nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable},
- { "nativeIsBackedByAshmem", "(J)Z", (void*)Bitmap_isBackedByAshmem}
+ {"nativeCreate", "([IIIIIIZJ)Landroid/graphics/Bitmap;", (void*)Bitmap_creator},
+ {"nativeCopy", "(JIZ)Landroid/graphics/Bitmap;", (void*)Bitmap_copy},
+ {"nativeCopyAshmem", "(J)Landroid/graphics/Bitmap;", (void*)Bitmap_copyAshmem},
+ {"nativeCopyAshmemConfig", "(JI)Landroid/graphics/Bitmap;", (void*)Bitmap_copyAshmemConfig},
+ {"nativeGetAshmemFD", "(J)I", (void*)Bitmap_getAshmemFd},
+ {"nativeGetNativeFinalizer", "()J", (void*)Bitmap_getNativeFinalizer},
+ {"nativeRecycle", "(J)V", (void*)Bitmap_recycle},
+ {"nativeReconfigure", "(JIIIZ)V", (void*)Bitmap_reconfigure},
+ {"nativeCompress", "(JIILjava/io/OutputStream;[B)Z", (void*)Bitmap_compress},
+ {"nativeErase", "(JI)V", (void*)Bitmap_erase},
+ {"nativeErase", "(JJJ)V", (void*)Bitmap_eraseLong},
+ {"nativeRowBytes", "(J)I", (void*)Bitmap_rowBytes},
+ {"nativeConfig", "(J)I", (void*)Bitmap_config},
+ {"nativeHasAlpha", "(J)Z", (void*)Bitmap_hasAlpha},
+ {"nativeIsPremultiplied", "(J)Z", (void*)Bitmap_isPremultiplied},
+ {"nativeSetHasAlpha", "(JZZ)V", (void*)Bitmap_setHasAlpha},
+ {"nativeSetPremultiplied", "(JZ)V", (void*)Bitmap_setPremultiplied},
+ {"nativeHasMipMap", "(J)Z", (void*)Bitmap_hasMipMap},
+ {"nativeSetHasMipMap", "(JZ)V", (void*)Bitmap_setHasMipMap},
+ {"nativeCreateFromParcel", "(Landroid/os/Parcel;)Landroid/graphics/Bitmap;",
+ (void*)Bitmap_createFromParcel},
+ {"nativeWriteToParcel", "(JILandroid/os/Parcel;)Z", (void*)Bitmap_writeToParcel},
+ {"nativeExtractAlpha", "(JJ[I)Landroid/graphics/Bitmap;", (void*)Bitmap_extractAlpha},
+ {"nativeGenerationId", "(J)I", (void*)Bitmap_getGenerationId},
+ {"nativeGetPixel", "(JII)I", (void*)Bitmap_getPixel},
+ {"nativeGetColor", "(JII)J", (void*)Bitmap_getColor},
+ {"nativeGetPixels", "(J[IIIIIII)V", (void*)Bitmap_getPixels},
+ {"nativeSetPixel", "(JIII)V", (void*)Bitmap_setPixel},
+ {"nativeSetPixels", "(J[IIIIIII)V", (void*)Bitmap_setPixels},
+ {"nativeCopyPixelsToBuffer", "(JLjava/nio/Buffer;)V", (void*)Bitmap_copyPixelsToBuffer},
+ {"nativeCopyPixelsFromBuffer", "(JLjava/nio/Buffer;)V", (void*)Bitmap_copyPixelsFromBuffer},
+ {"nativeSameAs", "(JJ)Z", (void*)Bitmap_sameAs},
+ {"nativePrepareToDraw", "(J)V", (void*)Bitmap_prepareToDraw},
+ {"nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount},
+ {"nativeCopyPreserveInternalConfig", "(J)Landroid/graphics/Bitmap;",
+ (void*)Bitmap_copyPreserveInternalConfig},
+ {"nativeWrapHardwareBufferBitmap",
+ "(Landroid/hardware/HardwareBuffer;J)Landroid/graphics/Bitmap;",
+ (void*)Bitmap_wrapHardwareBufferBitmap},
+ {"nativeGetHardwareBuffer", "(J)Landroid/hardware/HardwareBuffer;",
+ (void*)Bitmap_getHardwareBuffer},
+ {"nativeComputeColorSpace", "(J)Landroid/graphics/ColorSpace;",
+ (void*)Bitmap_computeColorSpace},
+ {"nativeSetColorSpace", "(JJ)V", (void*)Bitmap_setColorSpace},
+ {"nativeIsSRGB", "(J)Z", (void*)Bitmap_isSRGB},
+ {"nativeIsSRGBLinear", "(J)Z", (void*)Bitmap_isSRGBLinear},
+ {"nativeSetImmutable", "(J)V", (void*)Bitmap_setImmutable},
+ {"nativeExtractGainmap", "(J)Landroid/graphics/Gainmap;", (void*)Bitmap_extractGainmap},
+ {"nativeSetGainmap", "(JJ)V", (void*)Bitmap_setGainmap},
+
+ // ------------ @CriticalNative ----------------
+ {"nativeIsImmutable", "(J)Z", (void*)Bitmap_isImmutable},
+ {"nativeIsBackedByAshmem", "(J)Z", (void*)Bitmap_isBackedByAshmem},
+ {"nativeHasGainmap", "(J)Z", (void*)Bitmap_hasGainmap},
};
@@ -1321,18 +1280,7 @@ int register_android_graphics_Bitmap(JNIEnv* env)
gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J");
gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V");
gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V");
-
-#ifdef __ANDROID__ // Layoutlib does not support graphic buffer or parcel
- void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
- AHardwareBuffer_fromHardwareBuffer =
- (AHB_from_HB)dlsym(handle_, "AHardwareBuffer_fromHardwareBuffer");
- LOG_ALWAYS_FATAL_IF(AHardwareBuffer_fromHardwareBuffer == nullptr,
- "Failed to find required symbol AHardwareBuffer_fromHardwareBuffer!");
-
- AHardwareBuffer_toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer");
- LOG_ALWAYS_FATAL_IF(AHardwareBuffer_toHardwareBuffer == nullptr,
- " Failed to find required symbol AHardwareBuffer_toHardwareBuffer!");
-#endif
+ uirenderer::HardwareBufferHelpers::init();
return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods,
NELEM(gBitmapMethods));
}
diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h
index 73eca3aa8ef8..21a93f066d9b 100644
--- a/libs/hwui/jni/Bitmap.h
+++ b/libs/hwui/jni/Bitmap.h
@@ -19,7 +19,6 @@
#include <jni.h>
#include <android/bitmap.h>
-class SkBitmap;
struct SkImageInfo;
namespace android {
diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp
index 4e9daa4b0c16..8abcd9a59122 100644
--- a/libs/hwui/jni/BitmapFactory.cpp
+++ b/libs/hwui/jni/BitmapFactory.cpp
@@ -2,30 +2,43 @@
#define LOG_TAG "BitmapFactory"
#include "BitmapFactory.h"
+
+#include <Gainmap.h>
+#include <HardwareBitmapUploader.h>
+#include <androidfw/Asset.h>
+#include <androidfw/ResourceTypes.h>
+#include <cutils/compiler.h>
+#include <fcntl.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/stat.h>
+
+#include <memory>
+
#include "CreateJavaOutputStreamAdaptor.h"
#include "FrontBufferedStream.h"
#include "GraphicsJNI.h"
#include "MimeType.h"
#include "NinePatchPeeker.h"
#include "SkAndroidCodec.h"
+#include "SkBitmap.h"
+#include "SkBlendMode.h"
#include "SkCanvas.h"
-#include "SkMath.h"
+#include "SkColorSpace.h"
+#include "SkEncodedImageFormat.h"
+#include "SkGainmapInfo.h"
+#include "SkImageInfo.h"
+#include "SkPaint.h"
#include "SkPixelRef.h"
+#include "SkRect.h"
+#include "SkRefCnt.h"
+#include "SkSamplingOptions.h"
+#include "SkSize.h"
#include "SkStream.h"
#include "SkString.h"
-#include "SkUtils.h"
#include "Utils.h"
-#include <HardwareBitmapUploader.h>
-#include <nativehelper/JNIPlatformHelp.h>
-#include <androidfw/Asset.h>
-#include <androidfw/ResourceTypes.h>
-#include <cutils/compiler.h>
-#include <fcntl.h>
-#include <memory>
-#include <stdio.h>
-#include <sys/stat.h>
-
jfieldID gOptions_justBoundsFieldID;
jfieldID gOptions_sampleSizeFieldID;
jfieldID gOptions_configFieldID;
@@ -132,7 +145,7 @@ public:
}
const size_t size = info.computeByteSize(bitmap->rowBytes());
- if (size > SK_MaxS32) {
+ if (size > INT32_MAX) {
ALOGW("bitmap is too large");
return false;
}
@@ -173,6 +186,123 @@ static bool needsFineScale(const SkISize fullSize, const SkISize decodedSize,
needsFineScale(fullSize.height(), decodedSize.height(), sampleSize);
}
+static bool decodeGainmap(std::unique_ptr<SkStream> gainmapStream, const SkGainmapInfo& gainmapInfo,
+ sp<uirenderer::Gainmap>* outGainmap, const int sampleSize, float scale) {
+ std::unique_ptr<SkAndroidCodec> codec;
+ codec = SkAndroidCodec::MakeFromStream(std::move(gainmapStream), nullptr);
+ if (!codec) {
+ ALOGE("Can not create a codec for Gainmap.");
+ return false;
+ }
+ SkColorType decodeColorType = kN32_SkColorType;
+ if (codec->getInfo().colorType() == kGray_8_SkColorType) {
+ decodeColorType = kGray_8_SkColorType;
+ }
+ decodeColorType = codec->computeOutputColorType(decodeColorType);
+ sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(decodeColorType, nullptr);
+
+ SkISize size = codec->getSampledDimensions(sampleSize);
+
+ int scaledWidth = size.width();
+ int scaledHeight = size.height();
+ bool willScale = false;
+
+ // Apply a fine scaling step if necessary.
+ if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize) || scale != 1.0f) {
+ willScale = true;
+ // The operation below may loose precision (integer division), but it is put this way to
+ // mimic main image scale calculation
+ scaledWidth = static_cast<int>((codec->getInfo().width() / sampleSize) * scale + 0.5f);
+ scaledHeight = static_cast<int>((codec->getInfo().height() / sampleSize) * scale + 0.5f);
+ }
+
+ SkAlphaType alphaType = codec->computeOutputAlphaType(false);
+
+ const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType,
+ alphaType, decodeColorSpace);
+
+ SkImageInfo bitmapInfo = decodeInfo;
+ if (decodeColorType == kGray_8_SkColorType) {
+ // We treat gray8 as alpha8 in Bitmap's API surface
+ bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType);
+ }
+ SkBitmap decodeBitmap;
+ sk_sp<Bitmap> nativeBitmap = nullptr;
+
+ if (!decodeBitmap.setInfo(bitmapInfo)) {
+ ALOGE("Failed to setInfo.");
+ return false;
+ }
+
+ if (willScale) {
+ if (!decodeBitmap.tryAllocPixels(nullptr)) {
+ ALOGE("OOM allocating gainmap pixels.");
+ return false;
+ }
+ } else {
+ nativeBitmap = android::Bitmap::allocateHeapBitmap(&decodeBitmap);
+ if (!nativeBitmap) {
+ ALOGE("OOM allocating gainmap pixels.");
+ return false;
+ }
+ }
+
+ // Use SkAndroidCodec to perform the decode.
+ SkAndroidCodec::AndroidOptions codecOptions;
+ codecOptions.fZeroInitialized = SkCodec::kYes_ZeroInitialized;
+ codecOptions.fSampleSize = sampleSize;
+ SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodeBitmap.getPixels(),
+ decodeBitmap.rowBytes(), &codecOptions);
+ switch (result) {
+ case SkCodec::kSuccess:
+ case SkCodec::kIncompleteInput:
+ break;
+ default:
+ ALOGE("Error decoding gainmap.");
+ return false;
+ }
+
+ if (willScale) {
+ SkBitmap gainmapBitmap;
+ const float scaleX = scaledWidth / float(decodeBitmap.width());
+ const float scaleY = scaledHeight / float(decodeBitmap.height());
+
+ SkColorType scaledColorType = decodeBitmap.colorType();
+ gainmapBitmap.setInfo(
+ bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
+
+ nativeBitmap = android::Bitmap::allocateHeapBitmap(&gainmapBitmap);
+ if (!nativeBitmap) {
+ ALOGE("OOM allocating gainmap pixels.");
+ return false;
+ }
+
+ SkPaint paint;
+ // kSrc_Mode instructs us to overwrite the uninitialized pixels in
+ // outputBitmap. Otherwise we would blend by default, which is not
+ // what we want.
+ paint.setBlendMode(SkBlendMode::kSrc);
+
+ SkCanvas canvas(gainmapBitmap, SkCanvas::ColorBehavior::kLegacy);
+ canvas.scale(scaleX, scaleY);
+ decodeBitmap.setImmutable(); // so .asImage() doesn't make a copy
+ canvas.drawImage(decodeBitmap.asImage(), 0.0f, 0.0f,
+ SkSamplingOptions(SkFilterMode::kLinear), &paint);
+ }
+
+ auto gainmap = sp<uirenderer::Gainmap>::make();
+ if (!gainmap) {
+ ALOGE("OOM allocating Gainmap");
+ return false;
+ }
+
+ gainmap->info = gainmapInfo;
+ gainmap->bitmap = std::move(nativeBitmap);
+ *outGainmap = std::move(gainmap);
+
+ return true;
+}
+
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
jobject padding, jobject options, jlong inBitmapHandle,
jlong colorSpaceHandle) {
@@ -279,6 +409,14 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
decodeColorType = kN32_SkColorType;
}
+ // b/276879147, fallback to RGBA_8888 when decoding HEIF and P010 is not supported.
+ if (decodeColorType == kRGBA_1010102_SkColorType &&
+ codec->getEncodedFormat() == SkEncodedImageFormat::kHEIF &&
+ env->CallStaticBooleanMethod(gImageDecoder_class,
+ gImageDecoder_isP010SupportedForHEVCMethodID) == JNI_FALSE) {
+ decodeColorType = kN32_SkColorType;
+ }
+
sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(
decodeColorType, prefColorSpace);
@@ -476,6 +614,19 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
return nullObjectReturn("Got null SkPixelRef");
}
+ bool hasGainmap = false;
+ SkGainmapInfo gainmapInfo;
+ std::unique_ptr<SkStream> gainmapStream = nullptr;
+ sp<uirenderer::Gainmap> gainmap = nullptr;
+ if (result == SkCodec::kSuccess) {
+ hasGainmap = codec->getAndroidGainmap(&gainmapInfo, &gainmapStream);
+ }
+
+ if (hasGainmap) {
+ hasGainmap =
+ decodeGainmap(std::move(gainmapStream), gainmapInfo, &gainmap, sampleSize, scale);
+ }
+
if (!isMutable && javaBitmap == NULL) {
// promise we will never change our pixels (great for sharing and pictures)
outputBitmap.setImmutable();
@@ -483,6 +634,9 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
bool isPremultiplied = !requireUnpremultiplied;
if (javaBitmap != nullptr) {
+ if (hasGainmap) {
+ reuseBitmap->setGainmap(std::move(gainmap));
+ }
bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
outputBitmap.notifyPixelsChanged();
// If a java bitmap was passed in for reuse, pass it back
@@ -498,13 +652,25 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
if (!hardwareBitmap.get()) {
return nullObjectReturn("Failed to allocate a hardware bitmap");
}
+ if (hasGainmap) {
+ auto gm = uirenderer::Gainmap::allocateHardwareGainmap(gainmap);
+ if (gm) {
+ hardwareBitmap->setGainmap(std::move(gm));
+ }
+ }
+
return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,
ninePatchChunk, ninePatchInsets, -1);
}
+ Bitmap* heapBitmap = defaultAllocator.getStorageObjAndReset();
+ if (hasGainmap && heapBitmap != nullptr) {
+ heapBitmap->setGainmap(std::move(gainmap));
+ }
+
// now create the java bitmap
- return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
- bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
+ return bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags, ninePatchChunk, ninePatchInsets,
+ -1);
}
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
diff --git a/libs/hwui/jni/BitmapFactory.h b/libs/hwui/jni/BitmapFactory.h
index 45bffc44967d..a079cb4b513d 100644
--- a/libs/hwui/jni/BitmapFactory.h
+++ b/libs/hwui/jni/BitmapFactory.h
@@ -1,8 +1,9 @@
#ifndef _ANDROID_GRAPHICS_BITMAP_FACTORY_H_
#define _ANDROID_GRAPHICS_BITMAP_FACTORY_H_
+#include <SkEncodedImageFormat.h>
+
#include "GraphicsJNI.h"
-#include "SkEncodedImageFormat.h"
extern jclass gOptions_class;
extern jfieldID gOptions_justBoundsFieldID;
@@ -26,6 +27,9 @@ extern jfieldID gOptions_bitmapFieldID;
extern jclass gBitmapConfig_class;
extern jmethodID gBitmapConfig_nativeToConfigMethodID;
+extern jclass gImageDecoder_class;
+extern jmethodID gImageDecoder_isP010SupportedForHEVCMethodID;
+
jstring getMimeTypeAsJavaString(JNIEnv*, SkEncodedImageFormat);
#endif // _ANDROID_GRAPHICS_BITMAP_FACTORY_H_
diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp
index 1c20415dcc8f..740988f77270 100644
--- a/libs/hwui/jni/BitmapRegionDecoder.cpp
+++ b/libs/hwui/jni/BitmapRegionDecoder.cpp
@@ -17,27 +17,157 @@
#undef LOG_TAG
#define LOG_TAG "BitmapRegionDecoder"
+#include "BitmapRegionDecoder.h"
+
+#include <HardwareBitmapUploader.h>
+#include <androidfw/Asset.h>
+#include <sys/stat.h>
+
+#include <memory>
+
#include "BitmapFactory.h"
#include "CreateJavaOutputStreamAdaptor.h"
+#include "Gainmap.h"
#include "GraphicsJNI.h"
-#include "Utils.h"
-
-#include "BitmapRegionDecoder.h"
#include "SkBitmap.h"
#include "SkCodec.h"
+#include "SkColorSpace.h"
#include "SkData.h"
+#include "SkGainmapInfo.h"
#include "SkStream.h"
+#include "SkStreamPriv.h"
+#include "Utils.h"
-#include <HardwareBitmapUploader.h>
-#include <androidfw/Asset.h>
-#include <sys/stat.h>
+using namespace android;
-#include <memory>
+namespace android {
+class BitmapRegionDecoderWrapper {
+public:
+ static std::unique_ptr<BitmapRegionDecoderWrapper> Make(sk_sp<SkData> data) {
+ std::unique_ptr<skia::BitmapRegionDecoder> mainImageBRD =
+ skia::BitmapRegionDecoder::Make(std::move(data));
+ if (!mainImageBRD) {
+ return nullptr;
+ }
-using namespace android;
+ SkGainmapInfo gainmapInfo;
+ std::unique_ptr<SkStream> gainmapStream;
+ std::unique_ptr<skia::BitmapRegionDecoder> gainmapBRD = nullptr;
+ if (mainImageBRD->getAndroidGainmap(&gainmapInfo, &gainmapStream)) {
+ sk_sp<SkData> data = nullptr;
+ if (gainmapStream->getMemoryBase()) {
+ // It is safe to make without copy because we'll hold onto the stream.
+ data = SkData::MakeWithoutCopy(gainmapStream->getMemoryBase(),
+ gainmapStream->getLength());
+ } else {
+ data = SkCopyStreamToData(gainmapStream.get());
+ // We don't need to hold the stream anymore
+ gainmapStream = nullptr;
+ }
+ gainmapBRD = skia::BitmapRegionDecoder::Make(std::move(data));
+ }
+
+ return std::unique_ptr<BitmapRegionDecoderWrapper>(
+ new BitmapRegionDecoderWrapper(std::move(mainImageBRD), std::move(gainmapBRD),
+ gainmapInfo, std::move(gainmapStream)));
+ }
+
+ SkEncodedImageFormat getEncodedFormat() { return mMainImageBRD->getEncodedFormat(); }
+
+ SkColorType computeOutputColorType(SkColorType requestedColorType) {
+ return mMainImageBRD->computeOutputColorType(requestedColorType);
+ }
+
+ sk_sp<SkColorSpace> computeOutputColorSpace(SkColorType outputColorType,
+ sk_sp<SkColorSpace> prefColorSpace = nullptr) {
+ return mMainImageBRD->computeOutputColorSpace(outputColorType, prefColorSpace);
+ }
+
+ bool decodeRegion(SkBitmap* bitmap, skia::BRDAllocator* allocator, const SkIRect& desiredSubset,
+ int sampleSize, SkColorType colorType, bool requireUnpremul,
+ sk_sp<SkColorSpace> prefColorSpace) {
+ return mMainImageBRD->decodeRegion(bitmap, allocator, desiredSubset, sampleSize, colorType,
+ requireUnpremul, prefColorSpace);
+ }
+
+ bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, int outWidth, int outHeight,
+ const SkIRect& desiredSubset, int sampleSize, bool requireUnpremul) {
+ SkColorType decodeColorType = mGainmapBRD->computeOutputColorType(kN32_SkColorType);
+ sk_sp<SkColorSpace> decodeColorSpace =
+ mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr);
+ SkBitmap bm;
+ // Because we must match the dimensions of the base bitmap, we always use a
+ // recycling allocator even though we are allocating a new bitmap. This is to ensure
+ // that if a recycled bitmap was used for the base image that we match the relative
+ // dimensions of that base image. The behavior of BRD here is:
+ // if inBitmap is specified -> output dimensions are always equal to the inBitmap's
+ // if no bitmap is reused -> output dimensions are the intersect of the desiredSubset &
+ // the image bounds
+ // The handling of the above conditionals are baked into the desiredSubset, so we
+ // simply need to ensure that the resulting bitmap is the exact same width/height as
+ // the specified desiredSubset regardless of the intersection to the image bounds.
+ // kPremul_SkAlphaType is used just as a placeholder as it doesn't change the underlying
+ // allocation type. RecyclingClippingPixelAllocator will populate this with the
+ // actual alpha type in either allocPixelRef() or copyIfNecessary()
+ sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make(
+ outWidth, outHeight, decodeColorType, kPremul_SkAlphaType, decodeColorSpace));
+ if (!nativeBitmap) {
+ ALOGE("OOM allocating Bitmap for Gainmap");
+ return false;
+ }
+ RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false);
+ if (!mGainmapBRD->decodeRegion(&bm, &allocator, desiredSubset, sampleSize, decodeColorType,
+ requireUnpremul, decodeColorSpace)) {
+ ALOGE("Error decoding Gainmap region");
+ return false;
+ }
+ allocator.copyIfNecessary();
+ auto gainmap = sp<uirenderer::Gainmap>::make();
+ if (!gainmap) {
+ ALOGE("OOM allocating Gainmap");
+ return false;
+ }
+ gainmap->info = mGainmapInfo;
+ gainmap->bitmap = std::move(nativeBitmap);
+ *outGainmap = std::move(gainmap);
+ return true;
+ }
+
+ SkIRect calculateGainmapRegion(const SkIRect& mainImageRegion, int* inOutWidth,
+ int* inOutHeight) {
+ const float scaleX = ((float)mGainmapBRD->width()) / mMainImageBRD->width();
+ const float scaleY = ((float)mGainmapBRD->height()) / mMainImageBRD->height();
+ *inOutWidth *= scaleX;
+ *inOutHeight *= scaleY;
+ // TODO: Account for rounding error?
+ return SkIRect::MakeLTRB(mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
+ mainImageRegion.right() * scaleX,
+ mainImageRegion.bottom() * scaleY);
+ }
+
+ bool hasGainmap() { return mGainmapBRD != nullptr; }
+
+ int width() const { return mMainImageBRD->width(); }
+ int height() const { return mMainImageBRD->height(); }
+
+private:
+ BitmapRegionDecoderWrapper(std::unique_ptr<skia::BitmapRegionDecoder> mainImageBRD,
+ std::unique_ptr<skia::BitmapRegionDecoder> gainmapBRD,
+ SkGainmapInfo info, std::unique_ptr<SkStream> stream)
+ : mMainImageBRD(std::move(mainImageBRD))
+ , mGainmapBRD(std::move(gainmapBRD))
+ , mGainmapInfo(info)
+ , mGainmapStream(std::move(stream)) {}
+
+ std::unique_ptr<skia::BitmapRegionDecoder> mMainImageBRD;
+ std::unique_ptr<skia::BitmapRegionDecoder> mGainmapBRD;
+ SkGainmapInfo mGainmapInfo;
+ std::unique_ptr<SkStream> mGainmapStream;
+};
+} // namespace android
static jobject createBitmapRegionDecoder(JNIEnv* env, sk_sp<SkData> data) {
- auto brd = skia::BitmapRegionDecoder::Make(std::move(data));
+ auto brd = android::BitmapRegionDecoderWrapper::Make(std::move(data));
if (!brd) {
doThrowIOE(env, "Image format not supported");
return nullObjectReturn("CreateBitmapRegionDecoder returned null");
@@ -126,16 +256,14 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
// Recycle a bitmap if possible.
android::Bitmap* recycledBitmap = nullptr;
- size_t recycledBytes = 0;
if (javaBitmap) {
recycledBitmap = &bitmap::toBitmap(inBitmapHandle);
if (recycledBitmap->isImmutable()) {
ALOGW("Warning: Reusing an immutable bitmap as an image decoder target.");
}
- recycledBytes = recycledBitmap->getAllocationByteCount();
}
- auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
SkColorType decodeColorType = brd->computeOutputColorType(colorType);
if (isHardware) {
@@ -151,7 +279,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
// Set up the pixel allocator
skia::BRDAllocator* allocator = nullptr;
- RecyclingClippingPixelAllocator recycleAlloc(recycledBitmap, recycledBytes);
+ RecyclingClippingPixelAllocator recycleAlloc(recycledBitmap);
HeapAllocator heapAlloc;
if (javaBitmap) {
allocator = &recycleAlloc;
@@ -165,7 +293,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
decodeColorType, colorSpace);
// Decode the region.
- SkIRect subset = SkIRect::MakeXYWH(inputX, inputY, inputWidth, inputHeight);
+ const SkIRect subset = SkIRect::MakeXYWH(inputX, inputY, inputWidth, inputHeight);
SkBitmap bitmap;
if (!brd->decodeRegion(&bitmap, allocator, subset, sampleSize,
decodeColorType, requireUnpremul, decodeColorSpace)) {
@@ -195,9 +323,33 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
GraphicsJNI::getColorSpace(env, decodeColorSpace.get(), decodeColorType));
}
- // If we may have reused a bitmap, we need to indicate that the pixels have changed.
if (javaBitmap) {
recycleAlloc.copyIfNecessary();
+ }
+
+ sp<uirenderer::Gainmap> gainmap;
+ bool hasGainmap = brd->hasGainmap();
+ if (hasGainmap) {
+ int gainmapWidth = bitmap.width();
+ int gainmapHeight = bitmap.height();
+ if (javaBitmap) {
+ // If we are recycling we must match the inBitmap's relative dimensions
+ gainmapWidth = recycledBitmap->width();
+ gainmapHeight = recycledBitmap->height();
+ }
+ SkIRect gainmapSubset = brd->calculateGainmapRegion(subset, &gainmapWidth, &gainmapHeight);
+ if (!brd->decodeGainmapRegion(&gainmap, gainmapWidth, gainmapHeight, gainmapSubset,
+ sampleSize, requireUnpremul)) {
+ // If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap
+ hasGainmap = false;
+ }
+ }
+
+ // If we may have reused a bitmap, we need to indicate that the pixels have changed.
+ if (javaBitmap) {
+ if (hasGainmap) {
+ recycledBitmap->setGainmap(std::move(gainmap));
+ }
bitmap::reinitBitmap(env, javaBitmap, recycledBitmap->info(), !requireUnpremul);
return javaBitmap;
}
@@ -206,25 +358,36 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in
if (!requireUnpremul) {
bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Premultiplied;
}
+
if (isHardware) {
sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(bitmap);
+ if (hasGainmap) {
+ auto gm = uirenderer::Gainmap::allocateHardwareGainmap(gainmap);
+ if (gm) {
+ hardwareBitmap->setGainmap(std::move(gm));
+ }
+ }
return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags);
}
- return android::bitmap::createBitmap(env, heapAlloc.getStorageObjAndReset(), bitmapCreateFlags);
+ Bitmap* heapBitmap = heapAlloc.getStorageObjAndReset();
+ if (hasGainmap && heapBitmap != nullptr) {
+ heapBitmap->setGainmap(std::move(gainmap));
+ }
+ return android::bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags);
}
static jint nativeGetHeight(JNIEnv* env, jobject, jlong brdHandle) {
- auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
return static_cast<jint>(brd->height());
}
static jint nativeGetWidth(JNIEnv* env, jobject, jlong brdHandle) {
- auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
return static_cast<jint>(brd->width());
}
static void nativeClean(JNIEnv* env, jobject, jlong brdHandle) {
- auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+ auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
delete brd;
}
diff --git a/libs/hwui/jni/BufferUtils.cpp b/libs/hwui/jni/BufferUtils.cpp
new file mode 100644
index 000000000000..3eb08d7552da
--- /dev/null
+++ b/libs/hwui/jni/BufferUtils.cpp
@@ -0,0 +1,130 @@
+/*
+ * 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 "BufferUtils.h"
+
+#include "graphics_jni_helpers.h"
+
+static void copyToVector(std::vector<uint8_t>& dst, const void* src, size_t srcSize) {
+ if (src) {
+ dst.resize(srcSize);
+ memcpy(dst.data(), src, srcSize);
+ }
+}
+
+/**
+ * This code is taken and modified from com_google_android_gles_jni_GLImpl.cpp to extract data
+ * from a java.nio.Buffer.
+ */
+static void* getDirectBufferPointer(JNIEnv* env, jobject buffer) {
+ if (buffer == nullptr) {
+ return nullptr;
+ }
+
+ jint position;
+ jint limit;
+ jint elementSizeShift;
+ jlong pointer;
+ pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift);
+ if (pointer == 0) {
+ jniThrowException(env, "java/lang/IllegalArgumentException",
+ "Must use a native order direct Buffer");
+ return nullptr;
+ }
+ pointer += position << elementSizeShift;
+ return reinterpret_cast<void*>(pointer);
+}
+
+static void releasePointer(JNIEnv* env, jarray array, void* data, jboolean commit) {
+ env->ReleasePrimitiveArrayCritical(array, data, commit ? 0 : JNI_ABORT);
+}
+
+static void* getPointer(JNIEnv* env, jobject buffer, jarray* array, jint* remaining, jint* offset) {
+ jint position;
+ jint limit;
+ jint elementSizeShift;
+
+ jlong pointer;
+ pointer = jniGetNioBufferFields(env, buffer, &position, &limit, &elementSizeShift);
+ *remaining = (limit - position) << elementSizeShift;
+ if (pointer != 0L) {
+ *array = nullptr;
+ pointer += position << elementSizeShift;
+ return reinterpret_cast<void*>(pointer);
+ }
+
+ *array = jniGetNioBufferBaseArray(env, buffer);
+ *offset = jniGetNioBufferBaseArrayOffset(env, buffer);
+ return nullptr;
+}
+
+/**
+ * This is a copy of
+ * static void android_glBufferData__IILjava_nio_Buffer_2I
+ * from com_google_android_gles_jni_GLImpl.cpp
+ */
+static void setIndirectData(JNIEnv* env, size_t size, jobject data_buf,
+ std::vector<uint8_t>& result) {
+ jint exception = 0;
+ const char* exceptionType = nullptr;
+ const char* exceptionMessage = nullptr;
+ jarray array = nullptr;
+ jint bufferOffset = 0;
+ jint remaining;
+ void* data = 0;
+ char* dataBase = nullptr;
+
+ if (data_buf) {
+ data = getPointer(env, data_buf, (jarray*)&array, &remaining, &bufferOffset);
+ if (remaining < size) {
+ exception = 1;
+ exceptionType = "java/lang/IllegalArgumentException";
+ exceptionMessage = "remaining() < size < needed";
+ goto exit;
+ }
+ }
+ if (data_buf && data == nullptr) {
+ dataBase = (char*)env->GetPrimitiveArrayCritical(array, (jboolean*)0);
+ data = (void*)(dataBase + bufferOffset);
+ }
+
+ copyToVector(result, data, size);
+
+exit:
+ if (array) {
+ releasePointer(env, array, (void*)dataBase, JNI_FALSE);
+ }
+ if (exception) {
+ jniThrowException(env, exceptionType, exceptionMessage);
+ }
+}
+
+std::vector<uint8_t> copyJavaNioBufferToVector(JNIEnv* env, jobject buffer, size_t size,
+ jboolean isDirect) {
+ std::vector<uint8_t> data;
+ if (buffer == nullptr) {
+ jniThrowNullPointerException(env);
+ } else {
+ if (isDirect) {
+ void* directBufferPtr = getDirectBufferPointer(env, buffer);
+ if (directBufferPtr) {
+ copyToVector(data, directBufferPtr, size);
+ }
+ } else {
+ setIndirectData(env, size, buffer, data);
+ }
+ }
+ return data;
+}
diff --git a/libs/hwui/jni/BufferUtils.h b/libs/hwui/jni/BufferUtils.h
new file mode 100644
index 000000000000..b43c320b7771
--- /dev/null
+++ b/libs/hwui/jni/BufferUtils.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+#ifndef BUFFERUTILS_H_
+#define BUFFERUTILS_H_
+
+#include <jni.h>
+
+#include <vector>
+
+/**
+ * Helper method to load a java.nio.Buffer instance into a vector. This handles
+ * both direct and indirect buffers and promptly releases any critical arrays that
+ * have been retrieved in order to avoid potential jni exceptions due to interleaved
+ * jni calls between get/release primitive method invocations.
+ */
+std::vector<uint8_t> copyJavaNioBufferToVector(JNIEnv* env, jobject buffer, size_t size,
+ jboolean isDirect);
+
+#endif // BUFFERUTILS_H_
diff --git a/libs/hwui/jni/ByteBufferStreamAdaptor.cpp b/libs/hwui/jni/ByteBufferStreamAdaptor.cpp
index b10540cb3fbd..97dbc9ac171f 100644
--- a/libs/hwui/jni/ByteBufferStreamAdaptor.cpp
+++ b/libs/hwui/jni/ByteBufferStreamAdaptor.cpp
@@ -2,6 +2,7 @@
#include "GraphicsJNI.h"
#include "Utils.h"
+#include <SkData.h>
#include <SkStream.h>
using namespace android;
diff --git a/libs/hwui/jni/ColorFilter.cpp b/libs/hwui/jni/ColorFilter.cpp
index cef21f91f3c1..4bd7ef47b871 100644
--- a/libs/hwui/jni/ColorFilter.cpp
+++ b/libs/hwui/jni/ColorFilter.cpp
@@ -17,6 +17,7 @@
#include "GraphicsJNI.h"
+#include "SkBlendMode.h"
#include "SkColorFilter.h"
#include "SkColorMatrixFilter.h"
diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp
index f632d093d9f3..69774cc6e133 100644
--- a/libs/hwui/jni/FontFamily.cpp
+++ b/libs/hwui/jni/FontFamily.cpp
@@ -24,6 +24,7 @@
#include "SkData.h"
#include "SkFontMgr.h"
#include "SkRefCnt.h"
+#include "SkStream.h"
#include "SkTypeface.h"
#include "Utils.h"
#include "fonts/Font.h"
@@ -86,9 +87,9 @@ static jlong FontFamily_create(CRITICAL_JNI_PARAMS_COMMA jlong builderPtr) {
if (builder->fonts.empty()) {
return 0;
}
- std::shared_ptr<minikin::FontFamily> family = std::make_shared<minikin::FontFamily>(
+ std::shared_ptr<minikin::FontFamily> family = minikin::FontFamily::create(
builder->langId, builder->variant, std::move(builder->fonts),
- true /* isCustomFallback */);
+ true /* isCustomFallback */, false /* isDefaultFallback */);
if (family->getCoverage().length() == 0) {
return 0;
}
diff --git a/libs/hwui/jni/GIFMovie.cpp b/libs/hwui/jni/GIFMovie.cpp
index fef51b8d2f79..ae6ac4ce4ecc 100644
--- a/libs/hwui/jni/GIFMovie.cpp
+++ b/libs/hwui/jni/GIFMovie.cpp
@@ -7,9 +7,11 @@
#include "Movie.h"
+#include "SkBitmap.h"
#include "SkColor.h"
#include "SkColorPriv.h"
#include "SkStream.h"
+#include "SkTypes.h"
#include "gif_lib.h"
diff --git a/libs/hwui/jni/Gainmap.cpp b/libs/hwui/jni/Gainmap.cpp
new file mode 100644
index 000000000000..cec0ee7ee247
--- /dev/null
+++ b/libs/hwui/jni/Gainmap.cpp
@@ -0,0 +1,278 @@
+/*
+ * 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 <Gainmap.h>
+
+#ifdef __ANDROID__
+#include <binder/Parcel.h>
+#endif
+
+#include "Bitmap.h"
+#include "GraphicsJNI.h"
+#include "ScopedParcel.h"
+#include "graphics_jni_helpers.h"
+
+namespace android {
+
+static jclass gGainmap_class;
+static jmethodID gGainmap_constructorMethodID;
+
+using namespace uirenderer;
+
+static Gainmap* fromJava(jlong gainmap) {
+ return reinterpret_cast<Gainmap*>(gainmap);
+}
+
+static int getCreateFlags(const sk_sp<Bitmap>& bitmap) {
+ int flags = 0;
+ if (bitmap->info().alphaType() == kPremul_SkAlphaType) {
+ flags |= android::bitmap::kBitmapCreateFlag_Premultiplied;
+ }
+ if (!bitmap->isImmutable()) {
+ flags |= android::bitmap::kBitmapCreateFlag_Mutable;
+ }
+ return flags;
+}
+
+jobject Gainmap_extractFromBitmap(JNIEnv* env, const Bitmap& bitmap) {
+ auto gainmap = bitmap.gainmap();
+ jobject jGainmapImage;
+
+ {
+ // Scope to guard the release of nativeBitmap
+ auto nativeBitmap = gainmap->bitmap;
+ const int createFlags = getCreateFlags(nativeBitmap);
+ jGainmapImage = bitmap::createBitmap(env, nativeBitmap.release(), createFlags);
+ }
+
+ // Grab a ref for the jobject
+ gainmap->incStrong(0);
+ jobject obj = env->NewObject(gGainmap_class, gGainmap_constructorMethodID, jGainmapImage,
+ gainmap.get());
+
+ if (env->ExceptionCheck() != 0) {
+ // sadtrombone
+ gainmap->decStrong(0);
+ ALOGE("*** Uncaught exception returned from Java call!\n");
+ env->ExceptionDescribe();
+ }
+ return obj;
+}
+
+static void Gainmap_destructor(Gainmap* gainmap) {
+ gainmap->decStrong(0);
+}
+
+static jlong Gainmap_getNativeFinalizer(JNIEnv*, jobject) {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Gainmap_destructor));
+}
+
+jlong Gainmap_createEmpty(JNIEnv*, jobject) {
+ Gainmap* gainmap = new Gainmap();
+ gainmap->incStrong(0);
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(gainmap));
+}
+
+jlong Gainmap_createCopy(JNIEnv*, jobject, jlong sourcePtr) {
+ Gainmap* gainmap = new Gainmap();
+ gainmap->incStrong(0);
+ if (sourcePtr) {
+ Gainmap* src = fromJava(sourcePtr);
+ gainmap->info = src->info;
+ }
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(gainmap));
+}
+
+static void Gainmap_setBitmap(JNIEnv* env, jobject, jlong gainmapPtr, jobject jBitmap) {
+ android::Bitmap* bitmap = GraphicsJNI::getNativeBitmap(env, jBitmap);
+ fromJava(gainmapPtr)->bitmap = sk_ref_sp(bitmap);
+}
+
+static void Gainmap_setRatioMin(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, jfloat b) {
+ fromJava(gainmapPtr)->info.fGainmapRatioMin = {r, g, b, 1.f};
+}
+
+static void Gainmap_getRatioMin(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) {
+ const auto value = fromJava(gainmapPtr)->info.fGainmapRatioMin;
+ jfloat buf[3]{value.fR, value.fG, value.fB};
+ env->SetFloatArrayRegion(components, 0, 3, buf);
+}
+
+static void Gainmap_setRatioMax(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, jfloat b) {
+ fromJava(gainmapPtr)->info.fGainmapRatioMax = {r, g, b, 1.f};
+}
+
+static void Gainmap_getRatioMax(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) {
+ const auto value = fromJava(gainmapPtr)->info.fGainmapRatioMax;
+ jfloat buf[3]{value.fR, value.fG, value.fB};
+ env->SetFloatArrayRegion(components, 0, 3, buf);
+}
+
+static void Gainmap_setGamma(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g, jfloat b) {
+ fromJava(gainmapPtr)->info.fGainmapGamma = {r, g, b, 1.f};
+}
+
+static void Gainmap_getGamma(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) {
+ const auto value = fromJava(gainmapPtr)->info.fGainmapGamma;
+ jfloat buf[3]{value.fR, value.fG, value.fB};
+ env->SetFloatArrayRegion(components, 0, 3, buf);
+}
+
+static void Gainmap_setEpsilonSdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g,
+ jfloat b) {
+ fromJava(gainmapPtr)->info.fEpsilonSdr = {r, g, b, 1.f};
+}
+
+static void Gainmap_getEpsilonSdr(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) {
+ const auto value = fromJava(gainmapPtr)->info.fEpsilonSdr;
+ jfloat buf[3]{value.fR, value.fG, value.fB};
+ env->SetFloatArrayRegion(components, 0, 3, buf);
+}
+
+static void Gainmap_setEpsilonHdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat r, jfloat g,
+ jfloat b) {
+ fromJava(gainmapPtr)->info.fEpsilonHdr = {r, g, b, 1.f};
+}
+
+static void Gainmap_getEpsilonHdr(JNIEnv* env, jobject, jlong gainmapPtr, jfloatArray components) {
+ const auto value = fromJava(gainmapPtr)->info.fEpsilonHdr;
+ jfloat buf[3]{value.fR, value.fG, value.fB};
+ env->SetFloatArrayRegion(components, 0, 3, buf);
+}
+
+static void Gainmap_setDisplayRatioHdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat max) {
+ fromJava(gainmapPtr)->info.fDisplayRatioHdr = max;
+}
+
+static jfloat Gainmap_getDisplayRatioHdr(JNIEnv*, jobject, jlong gainmapPtr) {
+ return fromJava(gainmapPtr)->info.fDisplayRatioHdr;
+}
+
+static void Gainmap_setDisplayRatioSdr(JNIEnv*, jobject, jlong gainmapPtr, jfloat min) {
+ fromJava(gainmapPtr)->info.fDisplayRatioSdr = min;
+}
+
+static jfloat Gainmap_getDisplayRatioSdr(JNIEnv*, jobject, jlong gainmapPtr) {
+ return fromJava(gainmapPtr)->info.fDisplayRatioSdr;
+}
+
+// ----------------------------------------------------------------------------
+// Serialization
+// ----------------------------------------------------------------------------
+
+static void Gainmap_writeToParcel(JNIEnv* env, jobject, jlong nativeObject, jobject parcel) {
+#ifdef __ANDROID__ // Layoutlib does not support parcel
+ if (parcel == NULL) {
+ ALOGD("write null parcel\n");
+ return;
+ }
+ ScopedParcel p(env, parcel);
+ SkGainmapInfo info = fromJava(nativeObject)->info;
+ // write gainmap to parcel
+ // ratio min
+ p.writeFloat(info.fGainmapRatioMin.fR);
+ p.writeFloat(info.fGainmapRatioMin.fG);
+ p.writeFloat(info.fGainmapRatioMin.fB);
+ // ratio max
+ p.writeFloat(info.fGainmapRatioMax.fR);
+ p.writeFloat(info.fGainmapRatioMax.fG);
+ p.writeFloat(info.fGainmapRatioMax.fB);
+ // gamma
+ p.writeFloat(info.fGainmapGamma.fR);
+ p.writeFloat(info.fGainmapGamma.fG);
+ p.writeFloat(info.fGainmapGamma.fB);
+ // epsilonsdr
+ p.writeFloat(info.fEpsilonSdr.fR);
+ p.writeFloat(info.fEpsilonSdr.fG);
+ p.writeFloat(info.fEpsilonSdr.fB);
+ // epsilonhdr
+ p.writeFloat(info.fEpsilonHdr.fR);
+ p.writeFloat(info.fEpsilonHdr.fG);
+ p.writeFloat(info.fEpsilonHdr.fB);
+ // display ratio sdr
+ p.writeFloat(info.fDisplayRatioSdr);
+ // display ratio hdr
+ p.writeFloat(info.fDisplayRatioHdr);
+ // base image type
+ p.writeInt32(static_cast<int32_t>(info.fBaseImageType));
+ // type
+ p.writeInt32(static_cast<int32_t>(info.fType));
+#else
+ doThrowRE(env, "Cannot use parcels outside of Android!");
+#endif
+}
+
+static void Gainmap_readFromParcel(JNIEnv* env, jobject, jlong nativeObject, jobject parcel) {
+#ifdef __ANDROID__ // Layoutlib does not support parcel
+ if (parcel == NULL) {
+ jniThrowNullPointerException(env, "parcel cannot be null");
+ return;
+ }
+ ScopedParcel p(env, parcel);
+
+ SkGainmapInfo info;
+ info.fGainmapRatioMin = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f};
+ info.fGainmapRatioMax = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f};
+ info.fGainmapGamma = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f};
+ info.fEpsilonSdr = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f};
+ info.fEpsilonHdr = {p.readFloat(), p.readFloat(), p.readFloat(), 1.f};
+ info.fDisplayRatioSdr = p.readFloat();
+ info.fDisplayRatioHdr = p.readFloat();
+ info.fBaseImageType = static_cast<SkGainmapInfo::BaseImageType>(p.readInt32());
+ info.fType = static_cast<SkGainmapInfo::Type>(p.readInt32());
+
+ fromJava(nativeObject)->info = info;
+#else
+ jniThrowRuntimeException(env, "Cannot use parcels outside of Android");
+#endif
+}
+
+// ----------------------------------------------------------------------------
+// JNI Glue
+// ----------------------------------------------------------------------------
+
+static const JNINativeMethod gGainmapMethods[] = {
+ {"nGetFinalizer", "()J", (void*)Gainmap_getNativeFinalizer},
+ {"nCreateEmpty", "()J", (void*)Gainmap_createEmpty},
+ {"nCreateCopy", "(J)J", (void*)Gainmap_createCopy},
+ {"nSetBitmap", "(JLandroid/graphics/Bitmap;)V", (void*)Gainmap_setBitmap},
+ {"nSetRatioMin", "(JFFF)V", (void*)Gainmap_setRatioMin},
+ {"nGetRatioMin", "(J[F)V", (void*)Gainmap_getRatioMin},
+ {"nSetRatioMax", "(JFFF)V", (void*)Gainmap_setRatioMax},
+ {"nGetRatioMax", "(J[F)V", (void*)Gainmap_getRatioMax},
+ {"nSetGamma", "(JFFF)V", (void*)Gainmap_setGamma},
+ {"nGetGamma", "(J[F)V", (void*)Gainmap_getGamma},
+ {"nSetEpsilonSdr", "(JFFF)V", (void*)Gainmap_setEpsilonSdr},
+ {"nGetEpsilonSdr", "(J[F)V", (void*)Gainmap_getEpsilonSdr},
+ {"nSetEpsilonHdr", "(JFFF)V", (void*)Gainmap_setEpsilonHdr},
+ {"nGetEpsilonHdr", "(J[F)V", (void*)Gainmap_getEpsilonHdr},
+ {"nSetDisplayRatioHdr", "(JF)V", (void*)Gainmap_setDisplayRatioHdr},
+ {"nGetDisplayRatioHdr", "(J)F", (void*)Gainmap_getDisplayRatioHdr},
+ {"nSetDisplayRatioSdr", "(JF)V", (void*)Gainmap_setDisplayRatioSdr},
+ {"nGetDisplayRatioSdr", "(J)F", (void*)Gainmap_getDisplayRatioSdr},
+ {"nWriteGainmapToParcel", "(JLandroid/os/Parcel;)V", (void*)Gainmap_writeToParcel},
+ {"nReadGainmapFromParcel", "(JLandroid/os/Parcel;)V", (void*)Gainmap_readFromParcel},
+};
+
+int register_android_graphics_Gainmap(JNIEnv* env) {
+ gGainmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Gainmap"));
+ gGainmap_constructorMethodID =
+ GetMethodIDOrDie(env, gGainmap_class, "<init>", "(Landroid/graphics/Bitmap;J)V");
+ return android::RegisterMethodsOrDie(env, "android/graphics/Gainmap", gGainmapMethods,
+ NELEM(gGainmapMethods));
+}
+
+} // namespace android
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 33669ac0a34e..78b4f7b7654d 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -8,12 +8,19 @@
#include <nativehelper/JNIHelp.h>
#include "GraphicsJNI.h"
+#include "SkBitmap.h"
#include "SkCanvas.h"
+#include "SkColorSpace.h"
#include "SkFontMetrics.h"
-#include "SkMath.h"
+#include "SkImageInfo.h"
+#include "SkPixelRef.h"
+#include "SkPoint.h"
+#include "SkRect.h"
#include "SkRegion.h"
+#include "SkTypes.h"
#include <cutils/ashmem.h>
#include <hwui/Canvas.h>
+#include <log/log.h>
using namespace android;
@@ -483,7 +490,7 @@ SkRegion* GraphicsJNI::getNativeRegion(JNIEnv* env, jobject region)
void GraphicsJNI::set_metrics(JNIEnv* env, jobject metrics, const SkFontMetrics& skmetrics) {
if (metrics == nullptr) return;
- SkASSERT(env->IsInstanceOf(metrics, gFontMetrics_class));
+ LOG_FATAL_IF(!env->IsInstanceOf(metrics, gFontMetrics_class));
env->SetFloatField(metrics, gFontMetrics_top, SkScalarToFloat(skmetrics.fTop));
env->SetFloatField(metrics, gFontMetrics_ascent, SkScalarToFloat(skmetrics.fAscent));
env->SetFloatField(metrics, gFontMetrics_descent, SkScalarToFloat(skmetrics.fDescent));
@@ -497,7 +504,7 @@ int GraphicsJNI::set_metrics_int(JNIEnv* env, jobject metrics, const SkFontMetri
int leading = SkScalarRoundToInt(skmetrics.fLeading);
if (metrics) {
- SkASSERT(env->IsInstanceOf(metrics, gFontMetricsInt_class));
+ LOG_FATAL_IF(!env->IsInstanceOf(metrics, gFontMetricsInt_class));
env->SetIntField(metrics, gFontMetricsInt_top, SkScalarFloorToInt(skmetrics.fTop));
env->SetIntField(metrics, gFontMetricsInt_ascent, ascent);
env->SetIntField(metrics, gFontMetricsInt_descent, descent);
@@ -509,8 +516,7 @@ int GraphicsJNI::set_metrics_int(JNIEnv* env, jobject metrics, const SkFontMetri
///////////////////////////////////////////////////////////////////////////////////////////
-jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, skia::BitmapRegionDecoder* bitmap)
-{
+jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, BitmapRegionDecoderWrapper* bitmap) {
ALOG_ASSERT(bitmap != NULL);
jobject obj = env->NewObject(gBitmapRegionDecoder_class,
@@ -568,14 +574,14 @@ jobject GraphicsJNI::getColorSpace(JNIEnv* env, SkColorSpace* decodeColorSpace,
LOG_ALWAYS_FATAL_IF(!decodeColorSpace->toXYZD50(&xyzMatrix));
skcms_TransferFunction transferParams;
- // We can only handle numerical transfer functions at the moment
- LOG_ALWAYS_FATAL_IF(!decodeColorSpace->isNumericalTransferFn(&transferParams));
+ decodeColorSpace->transferFn(&transferParams);
+ auto res = skcms_TransferFunction_getType(&transferParams);
+ LOG_ALWAYS_FATAL_IF(res == skcms_TFType_HLGinvish || res == skcms_TFType_Invalid);
- jobject params = env->NewObject(gTransferParameters_class,
- gTransferParameters_constructorMethodID,
- transferParams.a, transferParams.b, transferParams.c,
- transferParams.d, transferParams.e, transferParams.f,
- transferParams.g);
+ jobject params;
+ params = env->NewObject(gTransferParameters_class, gTransferParameters_constructorMethodID,
+ transferParams.a, transferParams.b, transferParams.c, transferParams.d,
+ transferParams.e, transferParams.f, transferParams.g);
jfloatArray xyzArray = env->NewFloatArray(9);
jfloat xyz[9] = {
@@ -614,13 +620,13 @@ bool HeapAllocator::allocPixelRef(SkBitmap* bitmap) {
////////////////////////////////////////////////////////////////////////////////
-RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(
- android::Bitmap* recycledBitmap, size_t recycledBytes)
- : mRecycledBitmap(recycledBitmap)
- , mRecycledBytes(recycledBytes)
- , mSkiaBitmap(nullptr)
- , mNeedsCopy(false)
-{}
+RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
+ bool mustMatchColorType)
+ : mRecycledBitmap(recycledBitmap)
+ , mRecycledBytes(recycledBitmap ? recycledBitmap->getAllocationByteCount() : 0)
+ , mSkiaBitmap(nullptr)
+ , mNeedsCopy(false)
+ , mMustMatchColorType(mustMatchColorType) {}
RecyclingClippingPixelAllocator::~RecyclingClippingPixelAllocator() {}
@@ -631,10 +637,16 @@ bool RecyclingClippingPixelAllocator::allocPixelRef(SkBitmap* bitmap) {
LOG_ALWAYS_FATAL_IF(!bitmap);
mSkiaBitmap = bitmap;
- // This behaves differently than the RecyclingPixelAllocator. For backwards
- // compatibility, the original color type of the recycled bitmap must be maintained.
- if (mRecycledBitmap->info().colorType() != bitmap->colorType()) {
- return false;
+ if (mMustMatchColorType) {
+ // This behaves differently than the RecyclingPixelAllocator. For backwards
+ // compatibility, the original color type of the recycled bitmap must be maintained.
+ if (mRecycledBitmap->info().colorType() != bitmap->colorType()) {
+ ALOGW("recycled color type %d != bitmap color type %d",
+ mRecycledBitmap->info().colorType(), bitmap->colorType());
+ return false;
+ }
+ } else {
+ mRecycledBitmap->reconfigure(mRecycledBitmap->info().makeColorType(bitmap->colorType()));
}
// The Skia bitmap specifies the width and height needed by the decoder.
@@ -689,7 +701,7 @@ bool RecyclingClippingPixelAllocator::allocPixelRef(SkBitmap* bitmap) {
void RecyclingClippingPixelAllocator::copyIfNecessary() {
if (mNeedsCopy) {
mRecycledBitmap->ref();
- SkPixelRef* recycledPixels = mRecycledBitmap;
+ android::Bitmap* recycledPixels = mRecycledBitmap;
void* dst = recycledPixels->pixels();
const size_t dstRowBytes = mRecycledBitmap->rowBytes();
const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(),
@@ -698,8 +710,12 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() {
mSkiaBitmap->info().height());
for (int y = 0; y < rowsToCopy; y++) {
memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy);
- dst = SkTAddOffset<void>(dst, dstRowBytes);
+ // Cast to bytes in order to apply the dstRowBytes offset correctly.
+ dst = reinterpret_cast<void*>(
+ reinterpret_cast<uint8_t*>(dst) + dstRowBytes);
}
+ recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
+ recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
recycledPixels->notifyPixelsChanged();
recycledPixels->unref();
}
@@ -800,8 +816,8 @@ int register_android_graphics_Graphics(JNIEnv* env)
gTransferParameters_class = MakeGlobalRefOrDie(env, FindClassOrDie(env,
"android/graphics/ColorSpace$Rgb$TransferParameters"));
- gTransferParameters_constructorMethodID = GetMethodIDOrDie(env, gTransferParameters_class,
- "<init>", "(DDDDDDD)V");
+ gTransferParameters_constructorMethodID =
+ GetMethodIDOrDie(env, gTransferParameters_class, "<init>", "(DDDDDDD)V");
gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics");
gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class);
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index 085a905abaf8..23ab5dd38b1a 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -2,28 +2,25 @@
#define _ANDROID_GRAPHICS_GRAPHICS_JNI_H_
#include <cutils/compiler.h>
+#include <hwui/Bitmap.h>
+#include <hwui/Canvas.h>
-#include "Bitmap.h"
#include "BRDAllocator.h"
+#include "Bitmap.h"
#include "SkBitmap.h"
#include "SkCodec.h"
-#include "SkPixelRef.h"
+#include "SkColorSpace.h"
#include "SkMallocPixelRef.h"
+#include "SkPixelRef.h"
#include "SkPoint.h"
#include "SkRect.h"
-#include "SkColorSpace.h"
-#include <hwui/Canvas.h>
-#include <hwui/Bitmap.h>
-
#include "graphics_jni_helpers.h"
class SkCanvas;
struct SkFontMetrics;
namespace android {
-namespace skia {
- class BitmapRegionDecoder;
-}
+class BitmapRegionDecoderWrapper;
class Canvas;
class Paint;
struct Typeface;
@@ -49,10 +46,16 @@ public:
static void setJavaVM(JavaVM* javaVM);
- /** returns a pointer to the JavaVM provided when we initialized the module */
+ /**
+ * returns a pointer to the JavaVM provided when we initialized the module
+ * DEPRECATED: Objects should know the JavaVM that created them
+ */
static JavaVM* getJavaVM() { return mJavaVM; }
- /** return a pointer to the JNIEnv for this thread */
+ /**
+ * return a pointer to the JNIEnv for this thread
+ * DEPRECATED: Objects should know the JavaVM that created them
+ */
static JNIEnv* getJNIEnv();
/** create a JNIEnv* for this thread or assert if one already exists */
@@ -120,7 +123,7 @@ public:
static jobject createRegion(JNIEnv* env, SkRegion* region);
static jobject createBitmapRegionDecoder(JNIEnv* env,
- android::skia::BitmapRegionDecoder* bitmap);
+ android::BitmapRegionDecoderWrapper* bitmap);
/**
* Given a bitmap we natively allocate a memory block to store the contents
@@ -219,9 +222,8 @@ private:
*/
class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator {
public:
-
RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
- size_t recycledBytes);
+ bool mustMatchColorType = true);
~RecyclingClippingPixelAllocator();
@@ -249,6 +251,7 @@ private:
const size_t mRecycledBytes;
SkBitmap* mSkiaBitmap;
bool mNeedsCopy;
+ const bool mMustMatchColorType;
};
class AshmemPixelAllocator : public SkBitmap::Allocator {
@@ -335,6 +338,34 @@ private:
int fLen;
};
+class JGlobalRefHolder {
+public:
+ JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {}
+
+ virtual ~JGlobalRefHolder() {
+ env()->DeleteGlobalRef(mObject);
+ mObject = nullptr;
+ }
+
+ jobject object() { return mObject; }
+ JavaVM* vm() { return mVm; }
+
+ JNIEnv* env() {
+ JNIEnv* env;
+ if (mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", mVm);
+ }
+ return env;
+ }
+
+private:
+ JGlobalRefHolder(const JGlobalRefHolder&) = delete;
+ void operator=(const JGlobalRefHolder&) = delete;
+
+ JavaVM* mVm;
+ jobject mObject;
+};
+
void doThrowNPE(JNIEnv* env);
void doThrowAIOOBE(JNIEnv* env); // Array Index Out Of Bounds Exception
void doThrowIAE(JNIEnv* env, const char* msg = NULL); // Illegal Argument
diff --git a/libs/hwui/jni/HardwareBufferHelpers.cpp b/libs/hwui/jni/HardwareBufferHelpers.cpp
new file mode 100644
index 000000000000..7e3f771b6b3d
--- /dev/null
+++ b/libs/hwui/jni/HardwareBufferHelpers.cpp
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+#include "HardwareBufferHelpers.h"
+
+#include <dlfcn.h>
+#include <log/log.h>
+
+#ifdef __ANDROID__
+typedef AHardwareBuffer* (*AHB_from_HB)(JNIEnv*, jobject);
+typedef jobject (*AHB_to_HB)(JNIEnv*, AHardwareBuffer*);
+static AHB_from_HB fromHardwareBuffer = nullptr;
+static AHB_to_HB toHardwareBuffer = nullptr;
+#endif
+
+void android::uirenderer::HardwareBufferHelpers::init() {
+#ifdef __ANDROID__ // Layoutlib does not support graphic buffer or parcel
+ void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
+ fromHardwareBuffer = (AHB_from_HB)dlsym(handle_, "AHardwareBuffer_fromHardwareBuffer");
+ LOG_ALWAYS_FATAL_IF(fromHardwareBuffer == nullptr,
+ "Failed to find required symbol AHardwareBuffer_fromHardwareBuffer!");
+
+ toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer");
+ LOG_ALWAYS_FATAL_IF(toHardwareBuffer == nullptr,
+ " Failed to find required symbol AHardwareBuffer_toHardwareBuffer!");
+#endif
+}
+
+AHardwareBuffer* android::uirenderer::HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(
+ JNIEnv* env, jobject hardwarebuffer) {
+#ifdef __ANDROID__
+ LOG_ALWAYS_FATAL_IF(fromHardwareBuffer == nullptr,
+ "Failed to find symbol AHardwareBuffer_fromHardwareBuffer, did you forget "
+ "to call HardwareBufferHelpers::init?");
+ return fromHardwareBuffer(env, hardwarebuffer);
+#else
+ ALOGE("ERROR attempting to invoke AHardwareBuffer_fromHardwareBuffer on non Android "
+ "configuration");
+ return nullptr;
+#endif
+}
+
+jobject android::uirenderer::HardwareBufferHelpers::AHardwareBuffer_toHardwareBuffer(
+ JNIEnv* env, AHardwareBuffer* ahardwarebuffer) {
+#ifdef __ANDROID__
+ LOG_ALWAYS_FATAL_IF(toHardwareBuffer == nullptr,
+ "Failed to find symbol AHardwareBuffer_toHardwareBuffer, did you forget to "
+ "call HardwareBufferHelpers::init?");
+ return toHardwareBuffer(env, ahardwarebuffer);
+#else
+ ALOGE("ERROR attempting to invoke AHardwareBuffer_toHardwareBuffer on non Android "
+ "configuration");
+ return nullptr;
+#endif
+} \ No newline at end of file
diff --git a/libs/hwui/jni/HardwareBufferHelpers.h b/libs/hwui/jni/HardwareBufferHelpers.h
new file mode 100644
index 000000000000..326babfb0b34
--- /dev/null
+++ b/libs/hwui/jni/HardwareBufferHelpers.h
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+#ifndef HARDWAREBUFFER_JNI_HELPERS_H
+#define HARDWAREBUFFER_JNI_HELPERS_H
+
+#include <android/bitmap.h>
+#include <jni.h>
+
+namespace android {
+namespace uirenderer {
+
+class HardwareBufferHelpers {
+public:
+ static void init();
+ static AHardwareBuffer* AHardwareBuffer_fromHardwareBuffer(JNIEnv*, jobject);
+ static jobject AHardwareBuffer_toHardwareBuffer(JNIEnv*, AHardwareBuffer*);
+
+private:
+ HardwareBufferHelpers() = default; // not to be instantiated
+};
+
+} // namespace uirenderer
+} // namespace android
+
+#endif // HARDWAREBUFFER_JNI_HELPERS_H \ No newline at end of file
diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp
index f7b8c014be6e..6744c6c0b9ec 100644
--- a/libs/hwui/jni/ImageDecoder.cpp
+++ b/libs/hwui/jni/ImageDecoder.cpp
@@ -14,31 +14,43 @@
* limitations under the License.
*/
-#include "Bitmap.h"
-#include "BitmapFactory.h"
-#include "ByteBufferStreamAdaptor.h"
-#include "CreateJavaOutputStreamAdaptor.h"
-#include "GraphicsJNI.h"
#include "ImageDecoder.h"
-#include "NinePatchPeeker.h"
-#include "Utils.h"
-
-#include <hwui/Bitmap.h>
-#include <hwui/ImageDecoder.h>
-#include <HardwareBitmapUploader.h>
#include <FrontBufferedStream.h>
+#include <HardwareBitmapUploader.h>
+#include <SkAlphaType.h>
#include <SkAndroidCodec.h>
+#include <SkBitmap.h>
+#include <SkCodec.h>
+#include <SkCodecAnimation.h>
+#include <SkColorSpace.h>
+#include <SkColorType.h>
#include <SkEncodedImageFormat.h>
+#include <SkImageInfo.h>
+#include <SkRect.h>
+#include <SkSize.h>
#include <SkStream.h>
-
+#include <SkString.h>
#include <androidfw/Asset.h>
#include <fcntl.h>
+#include <gui/TraceUtils.h>
+#include <hwui/Bitmap.h>
+#include <hwui/ImageDecoder.h>
#include <sys/stat.h>
+#include "Bitmap.h"
+#include "BitmapFactory.h"
+#include "ByteBufferStreamAdaptor.h"
+#include "CreateJavaOutputStreamAdaptor.h"
+#include "Gainmap.h"
+#include "GraphicsJNI.h"
+#include "NinePatchPeeker.h"
+#include "Utils.h"
+
using namespace android;
-static jclass gImageDecoder_class;
+jclass gImageDecoder_class;
+jmethodID gImageDecoder_isP010SupportedForHEVCMethodID;
static jclass gSize_class;
static jclass gDecodeException_class;
static jclass gCanvas_class;
@@ -242,6 +254,7 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
jboolean requireUnpremul, jboolean preferRamOverQuality,
jboolean asAlphaMask, jlong colorSpaceHandle,
jboolean extended) {
+ ATRACE_CALL();
auto* decoder = reinterpret_cast<ImageDecoder*>(nativePtr);
if (!decoder->setTargetSize(targetWidth, targetHeight)) {
doThrowISE(env, "Could not scale to target size!");
@@ -287,6 +300,14 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
colorType = kN32_SkColorType;
}
+ // b/276879147, fallback to RGBA_8888 when decoding HEIF and P010 is not supported.
+ if (colorType == kRGBA_1010102_SkColorType &&
+ decoder->mCodec->getEncodedFormat() == SkEncodedImageFormat::kHEIF &&
+ env->CallStaticBooleanMethod(gImageDecoder_class,
+ gImageDecoder_isP010SupportedForHEVCMethodID) == JNI_FALSE) {
+ colorType = kN32_SkColorType;
+ }
+
if (!decoder->setOutColorType(colorType)) {
doThrowISE(env, "Failed to set out color type!");
return nullptr;
@@ -332,10 +353,22 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
return nullptr;
}
+ ATRACE_FORMAT("Decoding %dx%d bitmap", bitmapInfo.width(), bitmapInfo.height());
SkCodec::Result result = decoder->decode(bm.getPixels(), bm.rowBytes());
jthrowable jexception = get_and_clear_exception(env);
- int onPartialImageError = jexception ? kSourceException
- : 0; // No error.
+ int onPartialImageError = jexception ? kSourceException : 0; // No error.
+
+ // Only attempt to extract the gainmap if we're not post-processing, as we can't automatically
+ // mimic that to the gainmap and expect it to be meaningful. And also don't extract the gainmap
+ // if we're prioritizing RAM over quality, since the gainmap improves quality at the
+ // cost of RAM
+ if (result == SkCodec::kSuccess && !jpostProcess && !preferRamOverQuality) {
+ // The gainmap costs RAM to improve quality, so skip this if we're prioritizing RAM instead
+ result = decoder->extractGainmap(nativeBitmap.get(),
+ allocator == kSharedMemory_Allocator ? true : false);
+ jexception = get_and_clear_exception(env);
+ }
+
switch (result) {
case SkCodec::kSuccess:
// Ignore the exception, since the decode was successful anyway.
@@ -446,6 +479,12 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong
sk_sp<Bitmap> hwBitmap = Bitmap::allocateHardwareBitmap(bm);
if (hwBitmap) {
hwBitmap->setImmutable();
+ if (nativeBitmap->hasGainmap()) {
+ auto gm = uirenderer::Gainmap::allocateHardwareGainmap(nativeBitmap->gainmap());
+ if (gm) {
+ hwBitmap->setGainmap(std::move(gm));
+ }
+ }
return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags,
ninePatchChunk, ninePatchInsets);
}
@@ -511,6 +550,8 @@ int register_android_graphics_ImageDecoder(JNIEnv* env) {
gImageDecoder_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder"));
gImageDecoder_constructorMethodID = GetMethodIDOrDie(env, gImageDecoder_class, "<init>", "(JIIZZ)V");
gImageDecoder_postProcessMethodID = GetMethodIDOrDie(env, gImageDecoder_class, "postProcessAndRelease", "(Landroid/graphics/Canvas;)I");
+ gImageDecoder_isP010SupportedForHEVCMethodID =
+ GetStaticMethodIDOrDie(env, gImageDecoder_class, "isP010SupportedForHEVC", "()Z");
gSize_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/util/Size"));
gSize_constructorMethodID = GetMethodIDOrDie(env, gSize_class, "<init>", "(II)V");
diff --git a/libs/hwui/jni/Interpolator.cpp b/libs/hwui/jni/Interpolator.cpp
index fc3d70b87f5a..c71e3085caf5 100644
--- a/libs/hwui/jni/Interpolator.cpp
+++ b/libs/hwui/jni/Interpolator.cpp
@@ -24,12 +24,8 @@ static void Interpolator_setKeyFrame(JNIEnv* env, jobject clazz, jlong interpHan
AutoJavaFloatArray autoValues(env, valueArray);
AutoJavaFloatArray autoBlend(env, blendArray, 4);
-#ifdef SK_SCALAR_IS_FLOAT
SkScalar* scalars = autoValues.ptr();
SkScalar* blend = autoBlend.ptr();
-#else
- #error Need to convert float array to SkScalar array before calling the following function.
-#endif
interp->setKeyFrame(index, msec, scalars, blend);
}
diff --git a/libs/hwui/jni/JvmErrorReporter.h b/libs/hwui/jni/JvmErrorReporter.h
new file mode 100644
index 000000000000..3a3587572a1a
--- /dev/null
+++ b/libs/hwui/jni/JvmErrorReporter.h
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+#ifndef JVMERRORREPORTER_H
+#define JVMERRORREPORTER_H
+
+#include <TreeInfo.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include "GraphicsJNI.h"
+
+namespace android {
+namespace uirenderer {
+
+class JvmErrorReporter : public android::uirenderer::ErrorHandler {
+public:
+ JvmErrorReporter(JNIEnv* env) { env->GetJavaVM(&mVm); }
+
+ virtual void onError(const std::string& message) override {
+ JNIEnv* env;
+ if (mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", mVm);
+ }
+ jniThrowException(env, "java/lang/IllegalStateException", message.c_str());
+ }
+
+private:
+ JavaVM* mVm;
+};
+
+} // namespace uirenderer
+} // namespace android
+
+#endif // JVMERRORREPORTER_H
diff --git a/libs/hwui/jni/MaskFilter.cpp b/libs/hwui/jni/MaskFilter.cpp
index 5383032e0f77..048ce025ce27 100644
--- a/libs/hwui/jni/MaskFilter.cpp
+++ b/libs/hwui/jni/MaskFilter.cpp
@@ -2,6 +2,7 @@
#include "SkMaskFilter.h"
#include "SkBlurMask.h"
#include "SkBlurMaskFilter.h"
+#include "SkBlurTypes.h"
#include "SkTableMaskFilter.h"
static void ThrowIAE_IfNull(JNIEnv* env, void* ptr) {
diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp
new file mode 100644
index 000000000000..ae9792df3d82
--- /dev/null
+++ b/libs/hwui/jni/MeshSpecification.cpp
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+
+#include <SkMesh.h>
+
+#include "GraphicsJNI.h"
+#include "graphics_jni_helpers.h"
+
+namespace android {
+
+using Attribute = SkMeshSpecification::Attribute;
+using Varying = SkMeshSpecification::Varying;
+
+static struct {
+ jclass clazz{};
+ jfieldID type{};
+ jfieldID offset{};
+ jfieldID name{};
+} gAttributeInfo;
+
+static struct {
+ jclass clazz{};
+ jfieldID type{};
+ jfieldID name{};
+} gVaryingInfo;
+
+std::vector<Attribute> extractAttributes(JNIEnv* env, jobjectArray attributes) {
+ int size = env->GetArrayLength(attributes);
+ std::vector<Attribute> attVector;
+ attVector.reserve(size);
+ for (int i = 0; i < size; i++) {
+ jobject attribute = env->GetObjectArrayElement(attributes, i);
+ auto name = (jstring)env->GetObjectField(attribute, gAttributeInfo.name);
+ auto attName = ScopedUtfChars(env, name);
+ Attribute temp{Attribute::Type(env->GetIntField(attribute, gAttributeInfo.type)),
+ static_cast<size_t>(env->GetIntField(attribute, gAttributeInfo.offset)),
+ SkString(attName.c_str())};
+ attVector.push_back(std::move(temp));
+ }
+ return attVector;
+}
+
+std::vector<Varying> extractVaryings(JNIEnv* env, jobjectArray varyings) {
+ int size = env->GetArrayLength(varyings);
+ std::vector<Varying> varyVector;
+ varyVector.reserve(size);
+ for (int i = 0; i < size; i++) {
+ jobject varying = env->GetObjectArrayElement(varyings, i);
+ auto name = (jstring)env->GetObjectField(varying, gVaryingInfo.name);
+ auto varyName = ScopedUtfChars(env, name);
+ Varying temp{Varying::Type(env->GetIntField(varying, gVaryingInfo.type)),
+ SkString(varyName.c_str())};
+ varyVector.push_back(std::move(temp));
+ }
+
+ return varyVector;
+}
+
+static jlong Make(JNIEnv* env, jobject thiz, jobjectArray attributeArray, jint vertexStride,
+ jobjectArray varyingArray, jstring vertexShader, jstring fragmentShader) {
+ auto attributes = extractAttributes(env, attributeArray);
+ auto varyings = extractVaryings(env, varyingArray);
+ auto skVertexShader = ScopedUtfChars(env, vertexShader);
+ auto skFragmentShader = ScopedUtfChars(env, fragmentShader);
+ auto meshSpecResult = SkMeshSpecification::Make(attributes, vertexStride, varyings,
+ SkString(skVertexShader.c_str()),
+ SkString(skFragmentShader.c_str()));
+ if (meshSpecResult.specification.get() == nullptr) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str());
+ }
+
+ return reinterpret_cast<jlong>(meshSpecResult.specification.release());
+}
+
+static jlong MakeWithCS(JNIEnv* env, jobject thiz, jobjectArray attributeArray, jint vertexStride,
+ jobjectArray varyingArray, jstring vertexShader, jstring fragmentShader,
+ jlong colorSpace) {
+ auto attributes = extractAttributes(env, attributeArray);
+ auto varyings = extractVaryings(env, varyingArray);
+ auto skVertexShader = ScopedUtfChars(env, vertexShader);
+ auto skFragmentShader = ScopedUtfChars(env, fragmentShader);
+ auto meshSpecResult = SkMeshSpecification::Make(
+ attributes, vertexStride, varyings, SkString(skVertexShader.c_str()),
+ SkString(skFragmentShader.c_str()), GraphicsJNI::getNativeColorSpace(colorSpace));
+
+ if (meshSpecResult.specification.get() == nullptr) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str());
+ }
+
+ return reinterpret_cast<jlong>(meshSpecResult.specification.release());
+}
+
+static jlong MakeWithAlpha(JNIEnv* env, jobject thiz, jobjectArray attributeArray,
+ jint vertexStride, jobjectArray varyingArray, jstring vertexShader,
+ jstring fragmentShader, jlong colorSpace, jint alphaType) {
+ auto attributes = extractAttributes(env, attributeArray);
+ auto varyings = extractVaryings(env, varyingArray);
+ auto skVertexShader = ScopedUtfChars(env, vertexShader);
+ auto skFragmentShader = ScopedUtfChars(env, fragmentShader);
+ auto meshSpecResult = SkMeshSpecification::Make(
+ attributes, vertexStride, varyings, SkString(skVertexShader.c_str()),
+ SkString(skFragmentShader.c_str()), GraphicsJNI::getNativeColorSpace(colorSpace),
+ SkAlphaType(alphaType));
+
+ if (meshSpecResult.specification.get() == nullptr) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str());
+ }
+
+ return reinterpret_cast<jlong>(meshSpecResult.specification.release());
+}
+
+static void MeshSpecification_safeUnref(SkMeshSpecification* meshSpec) {
+ SkSafeUnref(meshSpec);
+}
+
+static jlong getMeshSpecificationFinalizer() {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshSpecification_safeUnref));
+}
+
+static const JNINativeMethod gMeshSpecificationMethods[] = {
+ {"nativeGetFinalizer", "()J", (void*)getMeshSpecificationFinalizer},
+ {"nativeMake",
+ "([Landroid/graphics/MeshSpecification$Attribute;I[Landroid/graphics/"
+ "MeshSpecification$Varying;"
+ "Ljava/lang/String;Ljava/lang/String;)J",
+ (void*)Make},
+ {"nativeMakeWithCS",
+ "([Landroid/graphics/MeshSpecification$Attribute;I"
+ "[Landroid/graphics/MeshSpecification$Varying;Ljava/lang/String;Ljava/lang/String;J)J",
+ (void*)MakeWithCS},
+ {"nativeMakeWithAlpha",
+ "([Landroid/graphics/MeshSpecification$Attribute;I"
+ "[Landroid/graphics/MeshSpecification$Varying;Ljava/lang/String;Ljava/lang/String;JI)J",
+ (void*)MakeWithAlpha}};
+
+int register_android_graphics_MeshSpecification(JNIEnv* env) {
+ android::RegisterMethodsOrDie(env, "android/graphics/MeshSpecification",
+ gMeshSpecificationMethods, NELEM(gMeshSpecificationMethods));
+
+ gAttributeInfo.clazz = env->FindClass("android/graphics/MeshSpecification$Attribute");
+ gAttributeInfo.type = env->GetFieldID(gAttributeInfo.clazz, "mType", "I");
+ gAttributeInfo.offset = env->GetFieldID(gAttributeInfo.clazz, "mOffset", "I");
+ gAttributeInfo.name = env->GetFieldID(gAttributeInfo.clazz, "mName", "Ljava/lang/String;");
+
+ gVaryingInfo.clazz = env->FindClass("android/graphics/MeshSpecification$Varying");
+ gVaryingInfo.type = env->GetFieldID(gVaryingInfo.clazz, "mType", "I");
+ gVaryingInfo.name = env->GetFieldID(gVaryingInfo.clazz, "mName", "Ljava/lang/String;");
+ return 0;
+}
+
+} // namespace android
diff --git a/libs/hwui/jni/Movie.cpp b/libs/hwui/jni/Movie.cpp
index bb8c99a73edf..7dfd8740b2d7 100644
--- a/libs/hwui/jni/Movie.cpp
+++ b/libs/hwui/jni/Movie.cpp
@@ -3,8 +3,8 @@
#include "GraphicsJNI.h"
#include <nativehelper/ScopedLocalRef.h>
#include "Movie.h"
+#include "SkRefCnt.h"
#include "SkStream.h"
-#include "SkUtils.h"
#include "Utils.h"
#include <androidfw/Asset.h>
diff --git a/libs/hwui/jni/Movie.h b/libs/hwui/jni/Movie.h
index 736890d5215e..02113dd58ec8 100644
--- a/libs/hwui/jni/Movie.h
+++ b/libs/hwui/jni/Movie.h
@@ -13,6 +13,7 @@
#include "SkBitmap.h"
#include "SkCanvas.h"
#include "SkRefCnt.h"
+#include "SkTypes.h"
class SkStreamRewindable;
diff --git a/libs/hwui/jni/MovieImpl.cpp b/libs/hwui/jni/MovieImpl.cpp
index ae9e04e617b0..abb75fa99c94 100644
--- a/libs/hwui/jni/MovieImpl.cpp
+++ b/libs/hwui/jni/MovieImpl.cpp
@@ -5,11 +5,12 @@
* found in the LICENSE file.
*/
#include "Movie.h"
-#include "SkCanvas.h"
-#include "SkPaint.h"
+#include "SkBitmap.h"
+#include "SkStream.h"
+#include "SkTypes.h"
// We should never see this in normal operation since our time values are
-// 0-based. So we use it as a sentinal.
+// 0-based. So we use it as a sentinel.
#define UNINITIALIZED_MSEC ((SkMSec)-1)
Movie::Movie()
@@ -81,8 +82,6 @@ const SkBitmap& Movie::bitmap()
////////////////////////////////////////////////////////////////////
-#include "SkStream.h"
-
Movie* Movie::DecodeMemory(const void* data, size_t length) {
SkMemoryStream stream(data, length, false);
return Movie::DecodeStream(&stream);
diff --git a/libs/hwui/jni/NinePatch.cpp b/libs/hwui/jni/NinePatch.cpp
index 08fc80fbdafd..d50a8a22b5cb 100644
--- a/libs/hwui/jni/NinePatch.cpp
+++ b/libs/hwui/jni/NinePatch.cpp
@@ -24,8 +24,10 @@
#include <hwui/Paint.h>
#include <utils/Log.h>
+#include "SkBitmap.h"
#include "SkCanvas.h"
#include "SkLatticeIter.h"
+#include "SkRect.h"
#include "SkRegion.h"
#include "GraphicsJNI.h"
#include "NinePatchPeeker.h"
diff --git a/libs/hwui/jni/NinePatchPeeker.cpp b/libs/hwui/jni/NinePatchPeeker.cpp
index 9171fc687276..d85ede5dc6d2 100644
--- a/libs/hwui/jni/NinePatchPeeker.cpp
+++ b/libs/hwui/jni/NinePatchPeeker.cpp
@@ -16,7 +16,7 @@
#include "NinePatchPeeker.h"
-#include <SkBitmap.h>
+#include <SkScalar.h>
#include <cutils/compiler.h>
using namespace android;
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 293e28022e47..13357fa25e8c 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -26,12 +26,14 @@
#include <nativehelper/ScopedPrimitiveArray.h>
#include "SkColorFilter.h"
+#include "SkColorSpace.h"
#include "SkFont.h"
#include "SkFontMetrics.h"
#include "SkFontTypes.h"
#include "SkMaskFilter.h"
#include "SkPath.h"
#include "SkPathEffect.h"
+#include "SkPathUtils.h"
#include "SkShader.h"
#include "SkBlendMode.h"
#include "unicode/uloc.h"
@@ -495,17 +497,32 @@ namespace PaintGlue {
return true;
}
- static jfloat doRunAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[],
- jint start, jint count, jint bufSize, jboolean isRtl, jint offset) {
+ static jfloat doRunAdvance(JNIEnv* env, const Paint* paint, const Typeface* typeface,
+ const jchar buf[], jint start, jint count, jint bufSize,
+ jboolean isRtl, jint offset, jfloatArray advances,
+ jint advancesIndex) {
+ if (advances) {
+ size_t advancesLength = env->GetArrayLength(advances);
+ if ((size_t)(count + advancesIndex) > advancesLength) {
+ doThrowAIOOBE(env);
+ return 0;
+ }
+ }
minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
- if (offset == start + count) {
+ if (offset == start + count && advances == nullptr) {
return MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
bufSize, nullptr);
}
std::unique_ptr<float[]> advancesArray(new float[count]);
MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
advancesArray.get());
- return minikin::getRunAdvance(advancesArray.get(), buf, start, count, offset);
+
+ float result = minikin::getRunAdvance(advancesArray.get(), buf, start, count, offset);
+ if (advances) {
+ minikin::distributeAdvances(advancesArray.get(), buf, start, count);
+ env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get());
+ }
+ return result;
}
static jfloat getRunAdvance___CIIIIZI_F(JNIEnv *env, jclass, jlong paintHandle, jcharArray text,
@@ -513,9 +530,23 @@ namespace PaintGlue {
const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
const Typeface* typeface = paint->getAndroidTypeface();
ScopedCharArrayRO textArray(env, text);
- jfloat result = doRunAdvance(paint, typeface, textArray.get() + contextStart,
- start - contextStart, end - start, contextEnd - contextStart, isRtl,
- offset - contextStart);
+ jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
+ start - contextStart, end - start, contextEnd - contextStart,
+ isRtl, offset - contextStart, nullptr, 0);
+ return result;
+ }
+
+ static jfloat getRunCharacterAdvance___CIIIIZI_FI_F(JNIEnv* env, jclass, jlong paintHandle,
+ jcharArray text, jint start, jint end,
+ jint contextStart, jint contextEnd,
+ jboolean isRtl, jint offset,
+ jfloatArray advances, jint advancesIndex) {
+ const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+ const Typeface* typeface = paint->getAndroidTypeface();
+ ScopedCharArrayRO textArray(env, text);
+ jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
+ start - contextStart, end - start, contextEnd - contextStart,
+ isRtl, offset - contextStart, advances, advancesIndex);
return result;
}
@@ -778,7 +809,7 @@ namespace PaintGlue {
Paint* obj = reinterpret_cast<Paint*>(objHandle);
SkPath* src = reinterpret_cast<SkPath*>(srcHandle);
SkPath* dst = reinterpret_cast<SkPath*>(dstHandle);
- return obj->getFillPath(*src, dst) ? JNI_TRUE : JNI_FALSE;
+ return skpathutils::FillPathWithPaint(*src, *obj, dst) ? JNI_TRUE : JNI_FALSE;
}
static jlong setShader(CRITICAL_JNI_PARAMS_COMMA jlong objHandle, jlong shaderHandle) {
@@ -1032,113 +1063,112 @@ namespace PaintGlue {
}; // namespace PaintGlue
static const JNINativeMethod methods[] = {
- {"nGetNativeFinalizer", "()J", (void*) PaintGlue::getNativeFinalizer},
- {"nInit","()J", (void*) PaintGlue::init},
- {"nInitWithPaint","(J)J", (void*) PaintGlue::initWithPaint},
- {"nBreakText","(J[CIIFI[F)I", (void*) PaintGlue::breakTextC},
- {"nBreakText","(JLjava/lang/String;ZFI[F)I", (void*) PaintGlue::breakTextS},
- {"nGetTextAdvances","(J[CIIIII[FI)F",
- (void*) PaintGlue::getTextAdvances___CIIIII_FI},
- {"nGetTextAdvances","(JLjava/lang/String;IIIII[FI)F",
- (void*) PaintGlue::getTextAdvances__StringIIIII_FI},
-
- {"nGetTextRunCursor", "(J[CIIIII)I", (void*) PaintGlue::getTextRunCursor___C},
- {"nGetTextRunCursor", "(JLjava/lang/String;IIIII)I",
- (void*) PaintGlue::getTextRunCursor__String},
- {"nGetTextPath", "(JI[CIIFFJ)V", (void*) PaintGlue::getTextPath___C},
- {"nGetTextPath", "(JILjava/lang/String;IIFFJ)V", (void*) PaintGlue::getTextPath__String},
- {"nGetStringBounds", "(JLjava/lang/String;IIILandroid/graphics/Rect;)V",
- (void*) PaintGlue::getStringBounds },
- {"nGetCharArrayBounds", "(J[CIIILandroid/graphics/Rect;)V",
- (void*) PaintGlue::getCharArrayBounds },
- {"nHasGlyph", "(JILjava/lang/String;)Z", (void*) PaintGlue::hasGlyph },
- {"nGetRunAdvance", "(J[CIIIIZI)F", (void*) PaintGlue::getRunAdvance___CIIIIZI_F},
- {"nGetOffsetForAdvance", "(J[CIIIIZF)I",
- (void*) PaintGlue::getOffsetForAdvance___CIIIIZF_I},
- {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
- (void*)PaintGlue::getFontMetricsIntForText___C},
- {"nGetFontMetricsIntForText",
- "(JLjava/lang/String;IIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
- (void*)PaintGlue::getFontMetricsIntForText___String},
-
- // --------------- @FastNative ----------------------
-
- {"nSetTextLocales","(JLjava/lang/String;)I", (void*) PaintGlue::setTextLocales},
- {"nSetFontFeatureSettings","(JLjava/lang/String;)V",
- (void*) PaintGlue::setFontFeatureSettings},
- {"nGetFontMetrics", "(JLandroid/graphics/Paint$FontMetrics;)F",
- (void*)PaintGlue::getFontMetrics},
- {"nGetFontMetricsInt", "(JLandroid/graphics/Paint$FontMetricsInt;)I",
- (void*)PaintGlue::getFontMetricsInt},
-
- // --------------- @CriticalNative ------------------
-
- {"nReset","(J)V", (void*) PaintGlue::reset},
- {"nSet","(JJ)V", (void*) PaintGlue::assign},
- {"nGetFlags","(J)I", (void*) PaintGlue::getFlags},
- {"nSetFlags","(JI)V", (void*) PaintGlue::setFlags},
- {"nGetHinting","(J)I", (void*) PaintGlue::getHinting},
- {"nSetHinting","(JI)V", (void*) PaintGlue::setHinting},
- {"nSetAntiAlias","(JZ)V", (void*) PaintGlue::setAntiAlias},
- {"nSetSubpixelText","(JZ)V", (void*) PaintGlue::setSubpixelText},
- {"nSetLinearText","(JZ)V", (void*) PaintGlue::setLinearText},
- {"nSetUnderlineText","(JZ)V", (void*) PaintGlue::setUnderlineText},
- {"nSetStrikeThruText","(JZ)V", (void*) PaintGlue::setStrikeThruText},
- {"nSetFakeBoldText","(JZ)V", (void*) PaintGlue::setFakeBoldText},
- {"nSetFilterBitmap","(JZ)V", (void*) PaintGlue::setFilterBitmap},
- {"nSetDither","(JZ)V", (void*) PaintGlue::setDither},
- {"nGetStyle","(J)I", (void*) PaintGlue::getStyle},
- {"nSetStyle","(JI)V", (void*) PaintGlue::setStyle},
- {"nSetColor","(JI)V", (void*) PaintGlue::setColor},
- {"nSetColor","(JJJ)V", (void*) PaintGlue::setColorLong},
- {"nSetAlpha","(JI)V", (void*) PaintGlue::setAlpha},
- {"nGetStrokeWidth","(J)F", (void*) PaintGlue::getStrokeWidth},
- {"nSetStrokeWidth","(JF)V", (void*) PaintGlue::setStrokeWidth},
- {"nGetStrokeMiter","(J)F", (void*) PaintGlue::getStrokeMiter},
- {"nSetStrokeMiter","(JF)V", (void*) PaintGlue::setStrokeMiter},
- {"nGetStrokeCap","(J)I", (void*) PaintGlue::getStrokeCap},
- {"nSetStrokeCap","(JI)V", (void*) PaintGlue::setStrokeCap},
- {"nGetStrokeJoin","(J)I", (void*) PaintGlue::getStrokeJoin},
- {"nSetStrokeJoin","(JI)V", (void*) PaintGlue::setStrokeJoin},
- {"nGetFillPath","(JJJ)Z", (void*) PaintGlue::getFillPath},
- {"nSetShader","(JJ)J", (void*) PaintGlue::setShader},
- {"nSetColorFilter","(JJ)J", (void*) PaintGlue::setColorFilter},
- {"nSetXfermode","(JI)V", (void*) PaintGlue::setXfermode},
- {"nSetPathEffect","(JJ)J", (void*) PaintGlue::setPathEffect},
- {"nSetMaskFilter","(JJ)J", (void*) PaintGlue::setMaskFilter},
- {"nSetTypeface","(JJ)V", (void*) PaintGlue::setTypeface},
- {"nGetTextAlign","(J)I", (void*) PaintGlue::getTextAlign},
- {"nSetTextAlign","(JI)V", (void*) PaintGlue::setTextAlign},
- {"nSetTextLocalesByMinikinLocaleListId","(JI)V",
- (void*) PaintGlue::setTextLocalesByMinikinLocaleListId},
- {"nIsElegantTextHeight","(J)Z", (void*) PaintGlue::isElegantTextHeight},
- {"nSetElegantTextHeight","(JZ)V", (void*) PaintGlue::setElegantTextHeight},
- {"nGetTextSize","(J)F", (void*) PaintGlue::getTextSize},
- {"nSetTextSize","(JF)V", (void*) PaintGlue::setTextSize},
- {"nGetTextScaleX","(J)F", (void*) PaintGlue::getTextScaleX},
- {"nSetTextScaleX","(JF)V", (void*) PaintGlue::setTextScaleX},
- {"nGetTextSkewX","(J)F", (void*) PaintGlue::getTextSkewX},
- {"nSetTextSkewX","(JF)V", (void*) PaintGlue::setTextSkewX},
- {"nGetLetterSpacing","(J)F", (void*) PaintGlue::getLetterSpacing},
- {"nSetLetterSpacing","(JF)V", (void*) PaintGlue::setLetterSpacing},
- {"nGetWordSpacing","(J)F", (void*) PaintGlue::getWordSpacing},
- {"nSetWordSpacing","(JF)V", (void*) PaintGlue::setWordSpacing},
- {"nGetStartHyphenEdit", "(J)I", (void*) PaintGlue::getStartHyphenEdit},
- {"nGetEndHyphenEdit", "(J)I", (void*) PaintGlue::getEndHyphenEdit},
- {"nSetStartHyphenEdit", "(JI)V", (void*) PaintGlue::setStartHyphenEdit},
- {"nSetEndHyphenEdit", "(JI)V", (void*) PaintGlue::setEndHyphenEdit},
- {"nAscent","(J)F", (void*) PaintGlue::ascent},
- {"nDescent","(J)F", (void*) PaintGlue::descent},
- {"nGetUnderlinePosition","(J)F", (void*) PaintGlue::getUnderlinePosition},
- {"nGetUnderlineThickness","(J)F", (void*) PaintGlue::getUnderlineThickness},
- {"nGetStrikeThruPosition","(J)F", (void*) PaintGlue::getStrikeThruPosition},
- {"nGetStrikeThruThickness","(J)F", (void*) PaintGlue::getStrikeThruThickness},
- {"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer},
- {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer},
- {"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement},
+ {"nGetNativeFinalizer", "()J", (void*)PaintGlue::getNativeFinalizer},
+ {"nInit", "()J", (void*)PaintGlue::init},
+ {"nInitWithPaint", "(J)J", (void*)PaintGlue::initWithPaint},
+ {"nBreakText", "(J[CIIFI[F)I", (void*)PaintGlue::breakTextC},
+ {"nBreakText", "(JLjava/lang/String;ZFI[F)I", (void*)PaintGlue::breakTextS},
+ {"nGetTextAdvances", "(J[CIIIII[FI)F", (void*)PaintGlue::getTextAdvances___CIIIII_FI},
+ {"nGetTextAdvances", "(JLjava/lang/String;IIIII[FI)F",
+ (void*)PaintGlue::getTextAdvances__StringIIIII_FI},
+
+ {"nGetTextRunCursor", "(J[CIIIII)I", (void*)PaintGlue::getTextRunCursor___C},
+ {"nGetTextRunCursor", "(JLjava/lang/String;IIIII)I",
+ (void*)PaintGlue::getTextRunCursor__String},
+ {"nGetTextPath", "(JI[CIIFFJ)V", (void*)PaintGlue::getTextPath___C},
+ {"nGetTextPath", "(JILjava/lang/String;IIFFJ)V", (void*)PaintGlue::getTextPath__String},
+ {"nGetStringBounds", "(JLjava/lang/String;IIILandroid/graphics/Rect;)V",
+ (void*)PaintGlue::getStringBounds},
+ {"nGetCharArrayBounds", "(J[CIIILandroid/graphics/Rect;)V",
+ (void*)PaintGlue::getCharArrayBounds},
+ {"nHasGlyph", "(JILjava/lang/String;)Z", (void*)PaintGlue::hasGlyph},
+ {"nGetRunAdvance", "(J[CIIIIZI)F", (void*)PaintGlue::getRunAdvance___CIIIIZI_F},
+ {"nGetRunCharacterAdvance", "(J[CIIIIZI[FI)F",
+ (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
+ {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
+ {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
+ (void*)PaintGlue::getFontMetricsIntForText___C},
+ {"nGetFontMetricsIntForText",
+ "(JLjava/lang/String;IIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
+ (void*)PaintGlue::getFontMetricsIntForText___String},
+
+ // --------------- @FastNative ----------------------
+
+ {"nSetTextLocales", "(JLjava/lang/String;)I", (void*)PaintGlue::setTextLocales},
+ {"nSetFontFeatureSettings", "(JLjava/lang/String;)V",
+ (void*)PaintGlue::setFontFeatureSettings},
+ {"nGetFontMetrics", "(JLandroid/graphics/Paint$FontMetrics;)F",
+ (void*)PaintGlue::getFontMetrics},
+ {"nGetFontMetricsInt", "(JLandroid/graphics/Paint$FontMetricsInt;)I",
+ (void*)PaintGlue::getFontMetricsInt},
+
+ // --------------- @CriticalNative ------------------
+
+ {"nReset", "(J)V", (void*)PaintGlue::reset},
+ {"nSet", "(JJ)V", (void*)PaintGlue::assign},
+ {"nGetFlags", "(J)I", (void*)PaintGlue::getFlags},
+ {"nSetFlags", "(JI)V", (void*)PaintGlue::setFlags},
+ {"nGetHinting", "(J)I", (void*)PaintGlue::getHinting},
+ {"nSetHinting", "(JI)V", (void*)PaintGlue::setHinting},
+ {"nSetAntiAlias", "(JZ)V", (void*)PaintGlue::setAntiAlias},
+ {"nSetSubpixelText", "(JZ)V", (void*)PaintGlue::setSubpixelText},
+ {"nSetLinearText", "(JZ)V", (void*)PaintGlue::setLinearText},
+ {"nSetUnderlineText", "(JZ)V", (void*)PaintGlue::setUnderlineText},
+ {"nSetStrikeThruText", "(JZ)V", (void*)PaintGlue::setStrikeThruText},
+ {"nSetFakeBoldText", "(JZ)V", (void*)PaintGlue::setFakeBoldText},
+ {"nSetFilterBitmap", "(JZ)V", (void*)PaintGlue::setFilterBitmap},
+ {"nSetDither", "(JZ)V", (void*)PaintGlue::setDither},
+ {"nGetStyle", "(J)I", (void*)PaintGlue::getStyle},
+ {"nSetStyle", "(JI)V", (void*)PaintGlue::setStyle},
+ {"nSetColor", "(JI)V", (void*)PaintGlue::setColor},
+ {"nSetColor", "(JJJ)V", (void*)PaintGlue::setColorLong},
+ {"nSetAlpha", "(JI)V", (void*)PaintGlue::setAlpha},
+ {"nGetStrokeWidth", "(J)F", (void*)PaintGlue::getStrokeWidth},
+ {"nSetStrokeWidth", "(JF)V", (void*)PaintGlue::setStrokeWidth},
+ {"nGetStrokeMiter", "(J)F", (void*)PaintGlue::getStrokeMiter},
+ {"nSetStrokeMiter", "(JF)V", (void*)PaintGlue::setStrokeMiter},
+ {"nGetStrokeCap", "(J)I", (void*)PaintGlue::getStrokeCap},
+ {"nSetStrokeCap", "(JI)V", (void*)PaintGlue::setStrokeCap},
+ {"nGetStrokeJoin", "(J)I", (void*)PaintGlue::getStrokeJoin},
+ {"nSetStrokeJoin", "(JI)V", (void*)PaintGlue::setStrokeJoin},
+ {"nGetFillPath", "(JJJ)Z", (void*)PaintGlue::getFillPath},
+ {"nSetShader", "(JJ)J", (void*)PaintGlue::setShader},
+ {"nSetColorFilter", "(JJ)J", (void*)PaintGlue::setColorFilter},
+ {"nSetXfermode", "(JI)V", (void*)PaintGlue::setXfermode},
+ {"nSetPathEffect", "(JJ)J", (void*)PaintGlue::setPathEffect},
+ {"nSetMaskFilter", "(JJ)J", (void*)PaintGlue::setMaskFilter},
+ {"nSetTypeface", "(JJ)V", (void*)PaintGlue::setTypeface},
+ {"nGetTextAlign", "(J)I", (void*)PaintGlue::getTextAlign},
+ {"nSetTextAlign", "(JI)V", (void*)PaintGlue::setTextAlign},
+ {"nSetTextLocalesByMinikinLocaleListId", "(JI)V",
+ (void*)PaintGlue::setTextLocalesByMinikinLocaleListId},
+ {"nIsElegantTextHeight", "(J)Z", (void*)PaintGlue::isElegantTextHeight},
+ {"nSetElegantTextHeight", "(JZ)V", (void*)PaintGlue::setElegantTextHeight},
+ {"nGetTextSize", "(J)F", (void*)PaintGlue::getTextSize},
+ {"nSetTextSize", "(JF)V", (void*)PaintGlue::setTextSize},
+ {"nGetTextScaleX", "(J)F", (void*)PaintGlue::getTextScaleX},
+ {"nSetTextScaleX", "(JF)V", (void*)PaintGlue::setTextScaleX},
+ {"nGetTextSkewX", "(J)F", (void*)PaintGlue::getTextSkewX},
+ {"nSetTextSkewX", "(JF)V", (void*)PaintGlue::setTextSkewX},
+ {"nGetLetterSpacing", "(J)F", (void*)PaintGlue::getLetterSpacing},
+ {"nSetLetterSpacing", "(JF)V", (void*)PaintGlue::setLetterSpacing},
+ {"nGetWordSpacing", "(J)F", (void*)PaintGlue::getWordSpacing},
+ {"nSetWordSpacing", "(JF)V", (void*)PaintGlue::setWordSpacing},
+ {"nGetStartHyphenEdit", "(J)I", (void*)PaintGlue::getStartHyphenEdit},
+ {"nGetEndHyphenEdit", "(J)I", (void*)PaintGlue::getEndHyphenEdit},
+ {"nSetStartHyphenEdit", "(JI)V", (void*)PaintGlue::setStartHyphenEdit},
+ {"nSetEndHyphenEdit", "(JI)V", (void*)PaintGlue::setEndHyphenEdit},
+ {"nAscent", "(J)F", (void*)PaintGlue::ascent},
+ {"nDescent", "(J)F", (void*)PaintGlue::descent},
+ {"nGetUnderlinePosition", "(J)F", (void*)PaintGlue::getUnderlinePosition},
+ {"nGetUnderlineThickness", "(J)F", (void*)PaintGlue::getUnderlineThickness},
+ {"nGetStrikeThruPosition", "(J)F", (void*)PaintGlue::getStrikeThruPosition},
+ {"nGetStrikeThruThickness", "(J)F", (void*)PaintGlue::getStrikeThruThickness},
+ {"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer},
+ {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer},
+ {"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement},
};
-
int register_android_graphics_Paint(JNIEnv* env) {
return RegisterMethodsOrDie(env, "android/graphics/Paint", methods, NELEM(methods));
}
diff --git a/libs/hwui/jni/Path.cpp b/libs/hwui/jni/Path.cpp
index d67bcf221681..a5e04763d885 100644
--- a/libs/hwui/jni/Path.cpp
+++ b/libs/hwui/jni/Path.cpp
@@ -102,6 +102,18 @@ public:
obj->rQuadTo(dx1, dy1, dx2, dy2);
}
+ static void conicTo(JNIEnv* env, jclass clazz, jlong objHandle, jfloat x1, jfloat y1, jfloat x2,
+ jfloat y2, jfloat weight) {
+ SkPath* obj = reinterpret_cast<SkPath*>(objHandle);
+ obj->conicTo(x1, y1, x2, y2, weight);
+ }
+
+ static void rConicTo(JNIEnv* env, jclass clazz, jlong objHandle, jfloat dx1, jfloat dy1,
+ jfloat dx2, jfloat dy2, jfloat weight) {
+ SkPath* obj = reinterpret_cast<SkPath*>(objHandle);
+ obj->rConicTo(dx1, dy1, dx2, dy2, weight);
+ }
+
static void cubicTo__FFFFFF(JNIEnv* env, jclass clazz, jlong objHandle, jfloat x1, jfloat y1,
jfloat x2, jfloat y2, jfloat x3, jfloat y3) {
SkPath* obj = reinterpret_cast<SkPath*>(objHandle);
@@ -170,11 +182,7 @@ public:
SkPath* obj = reinterpret_cast<SkPath*>(objHandle);
SkPathDirection dir = static_cast<SkPathDirection>(dirHandle);
AutoJavaFloatArray afa(env, array, 8);
-#ifdef SK_SCALAR_IS_FLOAT
const float* src = afa.ptr();
-#else
- #error Need to convert float array to SkScalar array before calling the following function.
-#endif
obj->addRoundRect(rect, src, dir);
}
@@ -209,6 +217,14 @@ public:
obj->setLastPt(dx, dy);
}
+ static jboolean interpolate(JNIEnv* env, jclass clazz, jlong startHandle, jlong endHandle,
+ jfloat t, jlong interpolatedHandle) {
+ SkPath* startPath = reinterpret_cast<SkPath*>(startHandle);
+ SkPath* endPath = reinterpret_cast<SkPath*>(endHandle);
+ SkPath* interpolatedPath = reinterpret_cast<SkPath*>(interpolatedHandle);
+ return startPath->interpolate(*endPath, t, interpolatedPath);
+ }
+
static void transform__MatrixPath(JNIEnv* env, jclass clazz, jlong objHandle, jlong matrixHandle,
jlong dstHandle) {
SkPath* obj = reinterpret_cast<SkPath*>(objHandle);
@@ -473,6 +489,16 @@ public:
// ---------------- @CriticalNative -------------------------
+ static jint getGenerationID(CRITICAL_JNI_PARAMS_COMMA jlong pathHandle) {
+ return (reinterpret_cast<SkPath*>(pathHandle)->getGenerationID());
+ }
+
+ static jboolean isInterpolatable(CRITICAL_JNI_PARAMS_COMMA jlong startHandle, jlong endHandle) {
+ SkPath* startPath = reinterpret_cast<SkPath*>(startHandle);
+ SkPath* endPath = reinterpret_cast<SkPath*>(endHandle);
+ return startPath->isInterpolatable(*endPath);
+ }
+
static void reset(CRITICAL_JNI_PARAMS_COMMA jlong objHandle) {
SkPath* obj = reinterpret_cast<SkPath*>(objHandle);
obj->reset();
@@ -506,48 +532,53 @@ public:
};
static const JNINativeMethod methods[] = {
- {"nInit","()J", (void*) SkPathGlue::init},
- {"nInit","(J)J", (void*) SkPathGlue::init_Path},
- {"nGetFinalizer", "()J", (void*) SkPathGlue::getFinalizer},
- {"nSet","(JJ)V", (void*) SkPathGlue::set},
- {"nComputeBounds","(JLandroid/graphics/RectF;)V", (void*) SkPathGlue::computeBounds},
- {"nIncReserve","(JI)V", (void*) SkPathGlue::incReserve},
- {"nMoveTo","(JFF)V", (void*) SkPathGlue::moveTo__FF},
- {"nRMoveTo","(JFF)V", (void*) SkPathGlue::rMoveTo},
- {"nLineTo","(JFF)V", (void*) SkPathGlue::lineTo__FF},
- {"nRLineTo","(JFF)V", (void*) SkPathGlue::rLineTo},
- {"nQuadTo","(JFFFF)V", (void*) SkPathGlue::quadTo__FFFF},
- {"nRQuadTo","(JFFFF)V", (void*) SkPathGlue::rQuadTo},
- {"nCubicTo","(JFFFFFF)V", (void*) SkPathGlue::cubicTo__FFFFFF},
- {"nRCubicTo","(JFFFFFF)V", (void*) SkPathGlue::rCubicTo},
- {"nArcTo","(JFFFFFFZ)V", (void*) SkPathGlue::arcTo},
- {"nClose","(J)V", (void*) SkPathGlue::close},
- {"nAddRect","(JFFFFI)V", (void*) SkPathGlue::addRect},
- {"nAddOval","(JFFFFI)V", (void*) SkPathGlue::addOval},
- {"nAddCircle","(JFFFI)V", (void*) SkPathGlue::addCircle},
- {"nAddArc","(JFFFFFF)V", (void*) SkPathGlue::addArc},
- {"nAddRoundRect","(JFFFFFFI)V", (void*) SkPathGlue::addRoundRectXY},
- {"nAddRoundRect","(JFFFF[FI)V", (void*) SkPathGlue::addRoundRect8},
- {"nAddPath","(JJFF)V", (void*) SkPathGlue::addPath__PathFF},
- {"nAddPath","(JJ)V", (void*) SkPathGlue::addPath__Path},
- {"nAddPath","(JJJ)V", (void*) SkPathGlue::addPath__PathMatrix},
- {"nOffset","(JFF)V", (void*) SkPathGlue::offset__FF},
- {"nSetLastPoint","(JFF)V", (void*) SkPathGlue::setLastPoint},
- {"nTransform","(JJJ)V", (void*) SkPathGlue::transform__MatrixPath},
- {"nTransform","(JJ)V", (void*) SkPathGlue::transform__Matrix},
- {"nOp","(JJIJ)Z", (void*) SkPathGlue::op},
- {"nApproximate", "(JF)[F", (void*) SkPathGlue::approximate},
-
- // ------- @FastNative below here ----------------------
- {"nIsRect","(JLandroid/graphics/RectF;)Z", (void*) SkPathGlue::isRect},
-
- // ------- @CriticalNative below here ------------------
- {"nReset","(J)V", (void*) SkPathGlue::reset},
- {"nRewind","(J)V", (void*) SkPathGlue::rewind},
- {"nIsEmpty","(J)Z", (void*) SkPathGlue::isEmpty},
- {"nIsConvex","(J)Z", (void*) SkPathGlue::isConvex},
- {"nGetFillType","(J)I", (void*) SkPathGlue::getFillType},
- {"nSetFillType","(JI)V", (void*) SkPathGlue::setFillType},
+ {"nInit", "()J", (void*)SkPathGlue::init},
+ {"nInit", "(J)J", (void*)SkPathGlue::init_Path},
+ {"nGetFinalizer", "()J", (void*)SkPathGlue::getFinalizer},
+ {"nSet", "(JJ)V", (void*)SkPathGlue::set},
+ {"nComputeBounds", "(JLandroid/graphics/RectF;)V", (void*)SkPathGlue::computeBounds},
+ {"nIncReserve", "(JI)V", (void*)SkPathGlue::incReserve},
+ {"nMoveTo", "(JFF)V", (void*)SkPathGlue::moveTo__FF},
+ {"nRMoveTo", "(JFF)V", (void*)SkPathGlue::rMoveTo},
+ {"nLineTo", "(JFF)V", (void*)SkPathGlue::lineTo__FF},
+ {"nRLineTo", "(JFF)V", (void*)SkPathGlue::rLineTo},
+ {"nQuadTo", "(JFFFF)V", (void*)SkPathGlue::quadTo__FFFF},
+ {"nRQuadTo", "(JFFFF)V", (void*)SkPathGlue::rQuadTo},
+ {"nConicTo", "(JFFFFF)V", (void*)SkPathGlue::conicTo},
+ {"nRConicTo", "(JFFFFF)V", (void*)SkPathGlue::rConicTo},
+ {"nCubicTo", "(JFFFFFF)V", (void*)SkPathGlue::cubicTo__FFFFFF},
+ {"nRCubicTo", "(JFFFFFF)V", (void*)SkPathGlue::rCubicTo},
+ {"nArcTo", "(JFFFFFFZ)V", (void*)SkPathGlue::arcTo},
+ {"nClose", "(J)V", (void*)SkPathGlue::close},
+ {"nAddRect", "(JFFFFI)V", (void*)SkPathGlue::addRect},
+ {"nAddOval", "(JFFFFI)V", (void*)SkPathGlue::addOval},
+ {"nAddCircle", "(JFFFI)V", (void*)SkPathGlue::addCircle},
+ {"nAddArc", "(JFFFFFF)V", (void*)SkPathGlue::addArc},
+ {"nAddRoundRect", "(JFFFFFFI)V", (void*)SkPathGlue::addRoundRectXY},
+ {"nAddRoundRect", "(JFFFF[FI)V", (void*)SkPathGlue::addRoundRect8},
+ {"nAddPath", "(JJFF)V", (void*)SkPathGlue::addPath__PathFF},
+ {"nAddPath", "(JJ)V", (void*)SkPathGlue::addPath__Path},
+ {"nAddPath", "(JJJ)V", (void*)SkPathGlue::addPath__PathMatrix},
+ {"nInterpolate", "(JJFJ)Z", (void*)SkPathGlue::interpolate},
+ {"nOffset", "(JFF)V", (void*)SkPathGlue::offset__FF},
+ {"nSetLastPoint", "(JFF)V", (void*)SkPathGlue::setLastPoint},
+ {"nTransform", "(JJJ)V", (void*)SkPathGlue::transform__MatrixPath},
+ {"nTransform", "(JJ)V", (void*)SkPathGlue::transform__Matrix},
+ {"nOp", "(JJIJ)Z", (void*)SkPathGlue::op},
+ {"nApproximate", "(JF)[F", (void*)SkPathGlue::approximate},
+
+ // ------- @FastNative below here ----------------------
+ {"nIsRect", "(JLandroid/graphics/RectF;)Z", (void*)SkPathGlue::isRect},
+
+ // ------- @CriticalNative below here ------------------
+ {"nGetGenerationID", "(J)I", (void*)SkPathGlue::getGenerationID},
+ {"nIsInterpolatable", "(JJ)Z", (void*)SkPathGlue::isInterpolatable},
+ {"nReset", "(J)V", (void*)SkPathGlue::reset},
+ {"nRewind", "(J)V", (void*)SkPathGlue::rewind},
+ {"nIsEmpty", "(J)Z", (void*)SkPathGlue::isEmpty},
+ {"nIsConvex", "(J)Z", (void*)SkPathGlue::isConvex},
+ {"nGetFillType", "(J)I", (void*)SkPathGlue::getFillType},
+ {"nSetFillType", "(JI)V", (void*)SkPathGlue::setFillType},
};
int register_android_graphics_Path(JNIEnv* env) {
diff --git a/libs/hwui/jni/PathEffect.cpp b/libs/hwui/jni/PathEffect.cpp
index f99bef7b7d58..3dbe1a67f52e 100644
--- a/libs/hwui/jni/PathEffect.cpp
+++ b/libs/hwui/jni/PathEffect.cpp
@@ -35,11 +35,7 @@ public:
jfloatArray intervalArray, jfloat phase) {
AutoJavaFloatArray autoInterval(env, intervalArray);
int count = autoInterval.length() & ~1; // even number
-#ifdef SK_SCALAR_IS_FLOAT
- SkScalar* intervals = autoInterval.ptr();
-#else
- #error Need to convert float array to SkScalar array before calling the following function.
-#endif
+ SkScalar* intervals = autoInterval.ptr();
SkPathEffect* effect = SkDashPathEffect::Make(intervals, count, phase).release();
return reinterpret_cast<jlong>(effect);
}
diff --git a/libs/hwui/jni/PathIterator.cpp b/libs/hwui/jni/PathIterator.cpp
new file mode 100644
index 000000000000..3884342d8d37
--- /dev/null
+++ b/libs/hwui/jni/PathIterator.cpp
@@ -0,0 +1,81 @@
+/* libs/android_runtime/android/graphics/PathMeasure.cpp
+**
+** Copyright 2007, 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 <log/log.h>
+
+#include "GraphicsJNI.h"
+#include "SkPath.h"
+#include "SkPoint.h"
+
+namespace android {
+
+class SkPathIteratorGlue {
+public:
+ static void finalizer(SkPath::RawIter* obj) { delete obj; }
+
+ static jlong getFinalizer(JNIEnv* env, jclass clazz) {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&finalizer));
+ }
+
+ static jlong create(JNIEnv* env, jobject clazz, jlong pathHandle) {
+ const SkPath* path = reinterpret_cast<SkPath*>(pathHandle);
+ return reinterpret_cast<jlong>(new SkPath::RawIter(*path));
+ }
+
+ // ---------------- @CriticalNative -------------------------
+
+ static jint peek(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle) {
+ SkPath::RawIter* iterator = reinterpret_cast<SkPath::RawIter*>(iteratorHandle);
+ return iterator->peek();
+ }
+
+ static jint next(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle, jlong pointsArray) {
+ static_assert(SkPath::kMove_Verb == 0, "SkPath::Verb unexpected index");
+ static_assert(SkPath::kLine_Verb == 1, "SkPath::Verb unexpected index");
+ static_assert(SkPath::kQuad_Verb == 2, "SkPath::Verb unexpected index");
+ static_assert(SkPath::kConic_Verb == 3, "SkPath::Verb unexpected index");
+ static_assert(SkPath::kCubic_Verb == 4, "SkPath::Verb unexpected index");
+ static_assert(SkPath::kClose_Verb == 5, "SkPath::Verb unexpected index");
+ static_assert(SkPath::kDone_Verb == 6, "SkPath::Verb unexpected index");
+
+ SkPath::RawIter* iterator = reinterpret_cast<SkPath::RawIter*>(iteratorHandle);
+ float* points = reinterpret_cast<float*>(pointsArray);
+ SkPath::Verb verb =
+ static_cast<SkPath::Verb>(iterator->next(reinterpret_cast<SkPoint*>(points)));
+ if (verb == SkPath::kConic_Verb) {
+ float weight = iterator->conicWeight();
+ points[6] = weight;
+ }
+ return static_cast<int>(verb);
+ }
+};
+
+static const JNINativeMethod methods[] = {
+ {"nCreate", "(J)J", (void*)SkPathIteratorGlue::create},
+ {"nGetFinalizer", "()J", (void*)SkPathIteratorGlue::getFinalizer},
+
+ // ------- @CriticalNative below here ------------------
+
+ {"nPeek", "(J)I", (void*)SkPathIteratorGlue::peek},
+ {"nNext", "(JJ)I", (void*)SkPathIteratorGlue::next},
+};
+
+int register_android_graphics_PathIterator(JNIEnv* env) {
+ return RegisterMethodsOrDie(env, "android/graphics/PathIterator", methods, NELEM(methods));
+}
+
+} // namespace android
diff --git a/libs/hwui/jni/RenderEffect.cpp b/libs/hwui/jni/RenderEffect.cpp
index 213f35a81b88..f3db1705e694 100644
--- a/libs/hwui/jni/RenderEffect.cpp
+++ b/libs/hwui/jni/RenderEffect.cpp
@@ -15,6 +15,7 @@
*/
#include "Bitmap.h"
#include "GraphicsJNI.h"
+#include "SkBlendMode.h"
#include "SkImageFilter.h"
#include "SkImageFilters.h"
#include "graphics_jni_helpers.h"
diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp
new file mode 100644
index 000000000000..b0f5423813b7
--- /dev/null
+++ b/libs/hwui/jni/ScopedParcel.cpp
@@ -0,0 +1,85 @@
+/*
+ * 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 "ScopedParcel.h"
+
+#ifdef __ANDROID__ // Layoutlib does not support parcel
+
+using namespace android;
+
+int32_t ScopedParcel::readInt32() {
+ int32_t temp = 0;
+ // TODO: This behavior-matches what android::Parcel does
+ // but this should probably be better
+ if (AParcel_readInt32(mParcel, &temp) != STATUS_OK) {
+ temp = 0;
+ }
+ return temp;
+}
+
+uint32_t ScopedParcel::readUint32() {
+ uint32_t temp = 0;
+ // TODO: This behavior-matches what android::Parcel does
+ // but this should probably be better
+ if (AParcel_readUint32(mParcel, &temp) != STATUS_OK) {
+ temp = 0;
+ }
+ return temp;
+}
+
+float ScopedParcel::readFloat() {
+ float temp = 0.;
+ if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) {
+ temp = 0.;
+ }
+ return temp;
+}
+
+std::optional<sk_sp<SkData>> ScopedParcel::readData() {
+ struct Data {
+ void* ptr = nullptr;
+ size_t size = 0;
+ } data;
+ auto error = AParcel_readByteArray(
+ mParcel, &data, [](void* arrayData, int32_t length, int8_t** outBuffer) -> bool {
+ Data* data = reinterpret_cast<Data*>(arrayData);
+ if (length > 0) {
+ data->ptr = sk_malloc_canfail(length);
+ if (!data->ptr) {
+ return false;
+ }
+ *outBuffer = reinterpret_cast<int8_t*>(data->ptr);
+ data->size = length;
+ }
+ return true;
+ });
+ if (error != STATUS_OK || data.size <= 0) {
+ sk_free(data.ptr);
+ return std::nullopt;
+ } else {
+ return SkData::MakeFromMalloc(data.ptr, data.size);
+ }
+}
+
+void ScopedParcel::writeData(const std::optional<sk_sp<SkData>>& optData) {
+ if (optData) {
+ const auto& data = *optData;
+ AParcel_writeByteArray(mParcel, reinterpret_cast<const int8_t*>(data->data()),
+ data->size());
+ } else {
+ AParcel_writeByteArray(mParcel, nullptr, -1);
+ }
+}
+#endif // __ANDROID__ // Layoutlib does not support parcel
diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h
new file mode 100644
index 000000000000..fd8d6a210f0f
--- /dev/null
+++ b/libs/hwui/jni/ScopedParcel.h
@@ -0,0 +1,63 @@
+/*
+ * 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 "SkData.h"
+
+#ifdef __ANDROID__ // Layoutlib does not support parcel
+#include <android-base/unique_fd.h>
+#include <android/binder_parcel.h>
+#include <android/binder_parcel_jni.h>
+#include <android/binder_parcel_platform.h>
+#include <cutils/ashmem.h>
+#include <renderthread/RenderProxy.h>
+
+class ScopedParcel {
+public:
+ explicit ScopedParcel(JNIEnv* env, jobject parcel) {
+ mParcel = AParcel_fromJavaParcel(env, parcel);
+ }
+
+ ~ScopedParcel() { AParcel_delete(mParcel); }
+
+ int32_t readInt32();
+
+ uint32_t readUint32();
+
+ float readFloat();
+
+ void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); }
+
+ void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); }
+
+ void writeFloat(float value) { AParcel_writeFloat(mParcel, value); }
+
+ bool allowFds() const { return AParcel_getAllowFds(mParcel); }
+
+ std::optional<sk_sp<SkData>> readData();
+
+ void writeData(const std::optional<sk_sp<SkData>>& optData);
+
+ AParcel* get() { return mParcel; }
+
+private:
+ AParcel* mParcel;
+};
+
+enum class BlobType : int32_t {
+ IN_PLACE,
+ ASHMEM,
+};
+
+#endif // __ANDROID__ // Layoutlib does not support parcel \ No newline at end of file
diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp
index 0bbd8a8cf97c..7eb79be6f55b 100644
--- a/libs/hwui/jni/Shader.cpp
+++ b/libs/hwui/jni/Shader.cpp
@@ -1,22 +1,34 @@
#undef LOG_TAG
#define LOG_TAG "ShaderJNI"
+#include <vector>
+
+#include "Gainmap.h"
#include "GraphicsJNI.h"
+#include "SkBitmap.h"
+#include "SkBlendMode.h"
+#include "SkColor.h"
#include "SkColorFilter.h"
#include "SkGradientShader.h"
+#include "SkImage.h"
#include "SkImagePriv.h"
+#include "SkMatrix.h"
+#include "SkPoint.h"
+#include "SkRefCnt.h"
+#include "SkSamplingOptions.h"
+#include "SkScalar.h"
#include "SkShader.h"
-#include "SkBlendMode.h"
+#include "SkString.h"
+#include "SkTileMode.h"
+#include "effects/GainmapRenderer.h"
#include "include/effects/SkRuntimeEffect.h"
-#include <vector>
-
using namespace android::uirenderer;
/**
* By default Skia gradients will interpolate their colors in unpremul space
* and then premultiply each of the results. We must set this flag to preserve
- * backwards compatiblity by premultiplying the colors of the gradient first,
+ * backwards compatibility by premultiplying the colors of the gradient first,
* and then interpolating between them.
*/
static const uint32_t sGradientShaderFlags = SkGradientShader::kInterpolateColorsInPremul_Flag;
@@ -42,12 +54,7 @@ static void Color_RGBToHSV(JNIEnv* env, jobject, jint red, jint green, jint blue
static jint Color_HSVToColor(JNIEnv* env, jobject, jint alpha, jfloatArray hsvArray)
{
AutoJavaFloatArray autoHSV(env, hsvArray, 3);
-#ifdef SK_SCALAR_IS_FLOAT
- SkScalar* hsv = autoHSV.ptr();
-#else
- #error Need to convert float array to SkScalar array before calling the following function.
-#endif
-
+ SkScalar* hsv = autoHSV.ptr();
return static_cast<jint>(SkHSVToColor(alpha, hsv));
}
@@ -61,25 +68,35 @@ static jlong Shader_getNativeFinalizer(JNIEnv*, jobject) {
return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Shader_safeUnref));
}
-///////////////////////////////////////////////////////////////////////////////////////////////
-
-static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle,
- jint tileModeX, jint tileModeY, bool filter,
- bool isDirectSampled) {
+static jlong createBitmapShaderHelper(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle,
+ jint tileModeX, jint tileModeY, bool isDirectSampled,
+ const SkSamplingOptions& sampling) {
const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr);
sk_sp<SkImage> image;
if (bitmapHandle) {
// Only pass a valid SkBitmap object to the constructor if the Bitmap exists. Otherwise,
// we'll pass an empty SkBitmap to avoid crashing/excepting for compatibility.
- image = android::bitmap::toBitmap(bitmapHandle).makeImage();
+ auto& bitmap = android::bitmap::toBitmap(bitmapHandle);
+ image = bitmap.makeImage();
+
+ if (!isDirectSampled && bitmap.hasGainmap()) {
+ sk_sp<SkShader> gainmapShader = MakeGainmapShader(
+ image, bitmap.gainmap()->bitmap->makeImage(), bitmap.gainmap()->info,
+ (SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling);
+ if (gainmapShader) {
+ if (matrix) {
+ gainmapShader = gainmapShader->makeWithLocalMatrix(*matrix);
+ }
+ return reinterpret_cast<jlong>(gainmapShader.release());
+ }
+ }
}
if (!image.get()) {
SkBitmap bitmap;
image = SkMakeImageFromRasterBitmap(bitmap, kNever_SkCopyPixelsMode);
}
- SkSamplingOptions sampling(filter ? SkFilterMode::kLinear : SkFilterMode::kNearest,
- SkMipmapMode::kNone);
+
sk_sp<SkShader> shader;
if (isDirectSampled) {
shader = image->makeRawShader((SkTileMode)tileModeX, (SkTileMode)tileModeY, sampling);
@@ -97,6 +114,26 @@ static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, j
///////////////////////////////////////////////////////////////////////////////////////////////
+static jlong BitmapShader_constructor(JNIEnv* env, jobject o, jlong matrixPtr, jlong bitmapHandle,
+ jint tileModeX, jint tileModeY, bool filter,
+ bool isDirectSampled) {
+ SkSamplingOptions sampling(filter ? SkFilterMode::kLinear : SkFilterMode::kNearest,
+ SkMipmapMode::kNone);
+ return createBitmapShaderHelper(env, o, matrixPtr, bitmapHandle, tileModeX, tileModeY,
+ isDirectSampled, sampling);
+}
+
+static jlong BitmapShader_constructorWithMaxAniso(JNIEnv* env, jobject o, jlong matrixPtr,
+ jlong bitmapHandle, jint tileModeX,
+ jint tileModeY, jint maxAniso,
+ bool isDirectSampled) {
+ auto sampling = SkSamplingOptions::Aniso(static_cast<int>(maxAniso));
+ return createBitmapShaderHelper(env, o, matrixPtr, bitmapHandle, tileModeX, tileModeY,
+ isDirectSampled, sampling);
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+
static std::vector<SkColor4f> convertColorLongs(JNIEnv* env, jlongArray colorArray) {
const size_t count = env->GetArrayLength(colorArray);
const jlong* colorValues = env->GetLongArrayElements(colorArray, nullptr);
@@ -122,11 +159,7 @@ static jlong LinearGradient_create(JNIEnv* env, jobject, jlong matrixPtr,
std::vector<SkColor4f> colors = convertColorLongs(env, colorArray);
AutoJavaFloatArray autoPos(env, posArray, colors.size());
-#ifdef SK_SCALAR_IS_FLOAT
SkScalar* pos = autoPos.ptr();
-#else
- #error Need to convert float array to SkScalar array before calling the following function.
-#endif
sk_sp<SkShader> shader(SkGradientShader::MakeLinear(pts, &colors[0],
GraphicsJNI::getNativeColorSpace(colorSpaceHandle), pos, colors.size(),
@@ -166,11 +199,7 @@ static jlong RadialGradient_create(JNIEnv* env,
std::vector<SkColor4f> colors = convertColorLongs(env, colorArray);
AutoJavaFloatArray autoPos(env, posArray, colors.size());
-#ifdef SK_SCALAR_IS_FLOAT
SkScalar* pos = autoPos.ptr();
-#else
- #error Need to convert float array to SkScalar array before calling the following function.
-#endif
auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpaceHandle);
auto skTileMode = static_cast<SkTileMode>(tileMode);
@@ -198,11 +227,7 @@ static jlong SweepGradient_create(JNIEnv* env, jobject, jlong matrixPtr, jfloat
std::vector<SkColor4f> colors = convertColorLongs(env, colorArray);
AutoJavaFloatArray autoPos(env, jpositions, colors.size());
-#ifdef SK_SCALAR_IS_FLOAT
SkScalar* pos = autoPos.ptr();
-#else
- #error Need to convert float array to SkScalar array before calling the following function.
-#endif
sk_sp<SkShader> shader = SkGradientShader::MakeSweep(x, y, &colors[0],
GraphicsJNI::getNativeColorSpace(colorSpaceHandle), pos, colors.size(),
@@ -398,6 +423,8 @@ static const JNINativeMethod gShaderMethods[] = {
static const JNINativeMethod gBitmapShaderMethods[] = {
{"nativeCreate", "(JJIIZZ)J", (void*)BitmapShader_constructor},
+ {"nativeCreateWithMaxAniso", "(JJIIIZ)J", (void*)BitmapShader_constructorWithMaxAniso},
+
};
static const JNINativeMethod gLinearGradientMethods[] = {
diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp
index d86d9ee56f4c..209b35c5537c 100644
--- a/libs/hwui/jni/Typeface.cpp
+++ b/libs/hwui/jni/Typeface.cpp
@@ -20,18 +20,21 @@
#include <minikin/FontCollection.h>
#include <minikin/FontFamily.h>
#include <minikin/FontFileParser.h>
+#include <minikin/LocaleList.h>
+#include <minikin/MinikinFontFactory.h>
#include <minikin/SystemFonts.h>
#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/ScopedUtfChars.h>
+
+#include <mutex>
+#include <unordered_map>
+
#include "FontUtils.h"
#include "GraphicsJNI.h"
#include "SkData.h"
#include "SkTypeface.h"
#include "fonts/Font.h"
-#include <mutex>
-#include <unordered_map>
-
#ifdef __ANDROID__
#include <sys/stat.h>
#endif
@@ -106,27 +109,14 @@ static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray,
jlong fallbackPtr, int weight, int italic) {
ScopedLongArrayRO families(env, familyArray);
- std::vector<std::shared_ptr<minikin::FontFamily>> familyVec;
Typeface* typeface = (fallbackPtr == 0) ? nullptr : toTypeface(fallbackPtr);
- if (typeface != nullptr) {
- const std::vector<std::shared_ptr<minikin::FontFamily>>& fallbackFamilies =
- toTypeface(fallbackPtr)->fFontCollection->getFamilies();
- familyVec.reserve(families.size() + fallbackFamilies.size());
- for (size_t i = 0; i < families.size(); i++) {
- FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]);
- familyVec.emplace_back(family->family);
- }
- for (size_t i = 0; i < fallbackFamilies.size(); i++) {
- familyVec.emplace_back(fallbackFamilies[i]);
- }
- } else {
- familyVec.reserve(families.size());
- for (size_t i = 0; i < families.size(); i++) {
- FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]);
- familyVec.emplace_back(family->family);
- }
+ std::vector<std::shared_ptr<minikin::FontFamily>> familyVec;
+ familyVec.reserve(families.size());
+ for (size_t i = 0; i < families.size(); i++) {
+ FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]);
+ familyVec.emplace_back(family->family);
}
- return toJLong(Typeface::createFromFamilies(std::move(familyVec), weight, italic));
+ return toJLong(Typeface::createFromFamilies(std::move(familyVec), weight, italic, typeface));
}
// CriticalNative
@@ -137,15 +127,13 @@ static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) {
Typeface* face = toTypeface(faceHandle);
- const std::unordered_set<minikin::AxisTag>& tagSet = face->fFontCollection->getSupportedTags();
- const size_t length = tagSet.size();
+ const size_t length = face->fFontCollection->getSupportedAxesCount();
if (length == 0) {
return nullptr;
}
std::vector<jint> tagVec(length);
- int index = 0;
- for (const auto& tag : tagSet) {
- tagVec[index++] = tag;
+ for (size_t i = 0; i < length; i++) {
+ tagVec[i] = face->fFontCollection->getSupportedAxisAt(i);
}
std::sort(tagVec.begin(), tagVec.end());
const jintArray result = env->NewIntArray(length);
@@ -204,9 +192,18 @@ static sk_sp<SkData> makeSkDataCached(const std::string& path, bool hasVerity) {
return entry;
}
-static std::shared_ptr<minikin::MinikinFont> loadMinikinFontSkia(minikin::BufferReader);
+class MinikinFontSkiaFactory : minikin::MinikinFontFactory {
+private:
+ MinikinFontSkiaFactory() : MinikinFontFactory() { MinikinFontFactory::setInstance(this); }
+
+public:
+ static void init() { static MinikinFontSkiaFactory factory; }
+ void skip(minikin::BufferReader* reader) const override;
+ std::shared_ptr<minikin::MinikinFont> create(minikin::BufferReader reader) const override;
+ void write(minikin::BufferWriter* writer, const minikin::MinikinFont* typeface) const override;
+};
-static minikin::Font::TypefaceLoader* readMinikinFontSkia(minikin::BufferReader* reader) {
+void MinikinFontSkiaFactory::skip(minikin::BufferReader* reader) const {
// Advance reader's position.
reader->skipString(); // fontPath
reader->skip<int>(); // fontIndex
@@ -216,10 +213,10 @@ static minikin::Font::TypefaceLoader* readMinikinFontSkia(minikin::BufferReader*
reader->skip<uint32_t>(); // expectedFontRevision
reader->skipString(); // expectedPostScriptName
}
- return &loadMinikinFontSkia;
}
-static std::shared_ptr<minikin::MinikinFont> loadMinikinFontSkia(minikin::BufferReader reader) {
+std::shared_ptr<minikin::MinikinFont> MinikinFontSkiaFactory::create(
+ minikin::BufferReader reader) const {
std::string_view fontPath = reader.readString();
std::string path(fontPath.data(), fontPath.size());
ATRACE_FORMAT("Loading font %s", path.c_str());
@@ -268,8 +265,8 @@ static std::shared_ptr<minikin::MinikinFont> loadMinikinFontSkia(minikin::Buffer
return minikinFont;
}
-static void writeMinikinFontSkia(minikin::BufferWriter* writer,
- const minikin::MinikinFont* typeface) {
+void MinikinFontSkiaFactory::write(minikin::BufferWriter* writer,
+ const minikin::MinikinFont* typeface) const {
// When you change the format of font metadata, please update code to parse
// typefaceMetadataReader() in
// frameworks/base/libs/hwui/jni/fonts/Font.cpp too.
@@ -293,7 +290,9 @@ static void writeMinikinFontSkia(minikin::BufferWriter* writer,
}
}
-static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongArray faceHandles) {
+static jint Typeface_writeTypefaces(JNIEnv* env, jobject, jobject buffer, jint position,
+ jlongArray faceHandles) {
+ MinikinFontSkiaFactory::init();
ScopedLongArrayRO faces(env, faceHandles);
std::vector<Typeface*> typefaces;
typefaces.reserve(faces.size());
@@ -301,7 +300,12 @@ static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongA
typefaces.push_back(toTypeface(faces[i]));
}
void* addr = buffer == nullptr ? nullptr : env->GetDirectBufferAddress(buffer);
- minikin::BufferWriter writer(addr);
+ if (addr != nullptr &&
+ reinterpret_cast<intptr_t>(addr) % minikin::BufferReader::kMaxAlignment != 0) {
+ ALOGE("addr (%p) must be aligned at kMaxAlignment, but it was not.", addr);
+ return 0;
+ }
+ minikin::BufferWriter writer(addr, position);
std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections;
std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex;
for (Typeface* typeface : typefaces) {
@@ -310,7 +314,7 @@ static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongA
fontCollections.push_back(typeface->fFontCollection);
}
}
- minikin::FontCollection::writeVector<writeMinikinFontSkia>(&writer, fontCollections);
+ minikin::FontCollection::writeVector(&writer, fontCollections);
writer.write<uint32_t>(typefaces.size());
for (Typeface* typeface : typefaces) {
writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second);
@@ -321,12 +325,20 @@ static jint Typeface_writeTypefaces(JNIEnv *env, jobject, jobject buffer, jlongA
return static_cast<jint>(writer.size());
}
-static jlongArray Typeface_readTypefaces(JNIEnv *env, jobject, jobject buffer) {
+static jlongArray Typeface_readTypefaces(JNIEnv* env, jobject, jobject buffer, jint position) {
+ MinikinFontSkiaFactory::init();
void* addr = buffer == nullptr ? nullptr : env->GetDirectBufferAddress(buffer);
- if (addr == nullptr) return nullptr;
- minikin::BufferReader reader(addr);
+ if (addr == nullptr) {
+ ALOGE("Passed a null buffer.");
+ return nullptr;
+ }
+ if (reinterpret_cast<intptr_t>(addr) % minikin::BufferReader::kMaxAlignment != 0) {
+ ALOGE("addr (%p) must be aligned at kMaxAlignment, but it was not.", addr);
+ return nullptr;
+ }
+ minikin::BufferReader reader(addr, position);
std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections =
- minikin::FontCollection::readVector<readMinikinFontSkia>(&reader);
+ minikin::FontCollection::readVector(&reader);
uint32_t typefaceCount = reader.read<uint32_t>();
std::vector<jlong> faceHandles;
faceHandles.reserve(typefaceCount);
@@ -343,7 +355,6 @@ static jlongArray Typeface_readTypefaces(JNIEnv *env, jobject, jobject buffer) {
return result;
}
-
static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring fieldName,
jobject typeface) {
ScopedUtfChars fieldNameChars(env, fieldName);
@@ -356,18 +367,6 @@ static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring f
env->SetStaticObjectField(cls, fid, typeface);
}
-// Critical Native
-static jint Typeface_getFamilySize(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
- return toTypeface(faceHandle)->fFontCollection->getFamilies().size();
-}
-
-// Critical Native
-static jlong Typeface_getFamily(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle, jint index) {
- std::shared_ptr<minikin::FontFamily> family =
- toTypeface(faceHandle)->fFontCollection->getFamilies()[index];
- return reinterpret_cast<jlong>(new FontFamilyWrapper(std::move(family)));
-}
-
// Regular JNI
static void Typeface_warmUpCache(JNIEnv* env, jobject, jstring jFilePath) {
ScopedUtfChars filePath(env, jFilePath);
@@ -380,6 +379,12 @@ static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandl
minikin::SystemFonts::addFontMap(std::move(collection));
}
+// Fast Native
+static void Typeface_registerLocaleList(JNIEnv* env, jobject, jstring jLocales) {
+ ScopedUtfChars locales(env, jLocales);
+ minikin::registerLocaleList(locales.c_str());
+}
+
///////////////////////////////////////////////////////////////////////////////
static const JNINativeMethod gTypefaceMethods[] = {
@@ -397,14 +402,13 @@ static const JNINativeMethod gTypefaceMethods[] = {
{"nativeGetSupportedAxes", "(J)[I", (void*)Typeface_getSupportedAxes},
{"nativeRegisterGenericFamily", "(Ljava/lang/String;J)V",
(void*)Typeface_registerGenericFamily},
- {"nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;[J)I", (void*)Typeface_writeTypefaces},
- {"nativeReadTypefaces", "(Ljava/nio/ByteBuffer;)[J", (void*)Typeface_readTypefaces},
+ {"nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;I[J)I", (void*)Typeface_writeTypefaces},
+ {"nativeReadTypefaces", "(Ljava/nio/ByteBuffer;I)[J", (void*)Typeface_readTypefaces},
{"nativeForceSetStaticFinalField", "(Ljava/lang/String;Landroid/graphics/Typeface;)V",
(void*)Typeface_forceSetStaticFinalField},
- {"nativeGetFamilySize", "(J)I", (void*)Typeface_getFamilySize},
- {"nativeGetFamily", "(JI)J", (void*)Typeface_getFamily},
{"nativeWarmUpCache", "(Ljava/lang/String;)V", (void*)Typeface_warmUpCache},
{"nativeAddFontCollections", "(J)V", (void*)Typeface_addFontCollection},
+ {"nativeRegisterLocaleList", "(Ljava/lang/String;)V", (void*)Typeface_registerLocaleList},
};
int register_android_graphics_Typeface(JNIEnv* env)
diff --git a/libs/hwui/jni/Utils.cpp b/libs/hwui/jni/Utils.cpp
index 106c6db57e18..9f5a2145792c 100644
--- a/libs/hwui/jni/Utils.cpp
+++ b/libs/hwui/jni/Utils.cpp
@@ -15,8 +15,9 @@
*/
#include "Utils.h"
-#include "SkUtils.h"
#include "SkData.h"
+#include "SkRefCnt.h"
+#include "SkStream.h"
#include <inttypes.h>
#include <log/log.h>
diff --git a/libs/hwui/jni/Utils.h b/libs/hwui/jni/Utils.h
index 6cdf44d85a5a..f6e3a0eeaa0e 100644
--- a/libs/hwui/jni/Utils.h
+++ b/libs/hwui/jni/Utils.h
@@ -17,8 +17,11 @@
#ifndef _ANDROID_GRAPHICS_UTILS_H_
#define _ANDROID_GRAPHICS_UTILS_H_
+#include "SkRefCnt.h"
#include "SkStream.h"
+class SkData;
+
#include <jni.h>
#include <androidfw/Asset.h>
diff --git a/libs/hwui/jni/YuvToJpegEncoder.cpp b/libs/hwui/jni/YuvToJpegEncoder.cpp
index d64d38a815f6..69418b09fee6 100644
--- a/libs/hwui/jni/YuvToJpegEncoder.cpp
+++ b/libs/hwui/jni/YuvToJpegEncoder.cpp
@@ -1,5 +1,8 @@
+#undef LOG_TAG
+#define LOG_TAG "YuvToJpegEncoder"
+
#include "CreateJavaOutputStreamAdaptor.h"
-#include "SkJPEGWriteUtility.h"
+#include "SkStream.h"
#include "YuvToJpegEncoder.h"
#include <ui/PixelFormat.h>
#include <hardware/hardware.h>
@@ -8,6 +11,15 @@
#include <csetjmp>
+extern "C" {
+ // We need to include stdio.h before jpeg because jpeg does not include it, but uses FILE
+ // See https://github.com/libjpeg-turbo/libjpeg-turbo/issues/17
+ #include <stdio.h>
+ #include "jpeglib.h"
+ #include "jerror.h"
+ #include "jmorecfg.h"
+}
+
YuvToJpegEncoder* YuvToJpegEncoder::create(int format, int* strides) {
// Only ImageFormat.NV21 and ImageFormat.YUY2 are supported
// for now.
@@ -34,11 +46,64 @@ void error_exit(j_common_ptr cinfo) {
longjmp(err->jmp, 1);
}
+/*
+ * Destination struct for directing decompressed pixels to a SkStream.
+ */
+static constexpr size_t kMgrBufferSize = 1024;
+struct skstream_destination_mgr : jpeg_destination_mgr {
+ skstream_destination_mgr(SkWStream* stream);
+
+ SkWStream* const fStream;
+
+ uint8_t fBuffer[kMgrBufferSize];
+};
+
+static void sk_init_destination(j_compress_ptr cinfo) {
+ skstream_destination_mgr* dest = (skstream_destination_mgr*)cinfo->dest;
+
+ dest->next_output_byte = dest->fBuffer;
+ dest->free_in_buffer = kMgrBufferSize;
+}
+
+static boolean sk_empty_output_buffer(j_compress_ptr cinfo) {
+ skstream_destination_mgr* dest = (skstream_destination_mgr*)cinfo->dest;
+
+ if (!dest->fStream->write(dest->fBuffer, kMgrBufferSize)) {
+ ERREXIT(cinfo, JERR_FILE_WRITE);
+ return FALSE;
+ }
+
+ dest->next_output_byte = dest->fBuffer;
+ dest->free_in_buffer = kMgrBufferSize;
+ return TRUE;
+}
+
+static void sk_term_destination(j_compress_ptr cinfo) {
+ skstream_destination_mgr* dest = (skstream_destination_mgr*)cinfo->dest;
+
+ size_t size = kMgrBufferSize - dest->free_in_buffer;
+ if (size > 0) {
+ if (!dest->fStream->write(dest->fBuffer, size)) {
+ ERREXIT(cinfo, JERR_FILE_WRITE);
+ return;
+ }
+ }
+
+ dest->fStream->flush();
+}
+
+skstream_destination_mgr::skstream_destination_mgr(SkWStream* stream)
+ : fStream(stream) {
+ this->init_destination = sk_init_destination;
+ this->empty_output_buffer = sk_empty_output_buffer;
+ this->term_destination = sk_term_destination;
+}
+
bool YuvToJpegEncoder::encode(SkWStream* stream, void* inYuv, int width,
int height, int* offsets, int jpegQuality) {
- jpeg_compress_struct cinfo;
- ErrorMgr err;
- skjpeg_destination_mgr sk_wstream(stream);
+ jpeg_compress_struct cinfo;
+ ErrorMgr err;
+ skstream_destination_mgr sk_wstream(stream);
cinfo.err = jpeg_std_error(&err.pub);
err.pub.error_exit = error_exit;
@@ -233,6 +298,99 @@ void Yuv422IToJpegEncoder::configSamplingFactors(jpeg_compress_struct* cinfo) {
}
///////////////////////////////////////////////////////////////////////////////
+using namespace android::ultrahdr;
+
+ultrahdr_color_gamut P010Yuv420ToJpegREncoder::findColorGamut(JNIEnv* env, int aDataSpace) {
+ switch (aDataSpace & ADataSpace::STANDARD_MASK) {
+ case ADataSpace::STANDARD_BT709:
+ return ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT709;
+ case ADataSpace::STANDARD_DCI_P3:
+ return ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_P3;
+ case ADataSpace::STANDARD_BT2020:
+ return ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT2100;
+ default:
+ jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException");
+ env->ThrowNew(IllegalArgumentException,
+ "The requested color gamut is not supported by JPEG/R.");
+ }
+
+ return ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+}
+
+ultrahdr_transfer_function P010Yuv420ToJpegREncoder::findHdrTransferFunction(JNIEnv* env,
+ int aDataSpace) {
+ switch (aDataSpace & ADataSpace::TRANSFER_MASK) {
+ case ADataSpace::TRANSFER_ST2084:
+ return ultrahdr_transfer_function::ULTRAHDR_TF_PQ;
+ case ADataSpace::TRANSFER_HLG:
+ return ultrahdr_transfer_function::ULTRAHDR_TF_HLG;
+ default:
+ jclass IllegalArgumentException = env->FindClass("java/lang/IllegalArgumentException");
+ env->ThrowNew(IllegalArgumentException,
+ "The requested HDR transfer function is not supported by JPEG/R.");
+ }
+
+ return ultrahdr_transfer_function::ULTRAHDR_TF_UNSPECIFIED;
+}
+
+bool P010Yuv420ToJpegREncoder::encode(JNIEnv* env,
+ SkWStream* stream, void* hdr, int hdrColorSpace, void* sdr, int sdrColorSpace,
+ int width, int height, int jpegQuality) {
+ // 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");
+ env->ThrowNew(IllegalArgumentException,
+ "The requested SDR color space is not supported. Transfer function must be SRGB");
+ return false;
+ }
+
+ ultrahdr_color_gamut hdrColorGamut = findColorGamut(env, hdrColorSpace);
+ ultrahdr_color_gamut sdrColorGamut = findColorGamut(env, sdrColorSpace);
+ ultrahdr_transfer_function hdrTransferFunction = findHdrTransferFunction(env, hdrColorSpace);
+
+ if (hdrColorGamut == ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_UNSPECIFIED
+ || sdrColorGamut == ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_UNSPECIFIED
+ || hdrTransferFunction == ultrahdr_transfer_function::ULTRAHDR_TF_UNSPECIFIED) {
+ return false;
+ }
+
+ JpegR jpegREncoder;
+
+ jpegr_uncompressed_struct p010;
+ p010.data = hdr;
+ p010.width = width;
+ p010.height = height;
+ p010.colorGamut = hdrColorGamut;
+
+ jpegr_uncompressed_struct yuv420;
+ yuv420.data = sdr;
+ yuv420.width = width;
+ yuv420.height = height;
+ yuv420.colorGamut = sdrColorGamut;
+
+ jpegr_compressed_struct jpegR;
+ jpegR.maxLength = width * height * sizeof(uint8_t);
+
+ std::unique_ptr<uint8_t[]> jpegr_data = std::make_unique<uint8_t[]>(jpegR.maxLength);
+ jpegR.data = jpegr_data.get();
+
+ if (int success = jpegREncoder.encodeJPEGR(&p010, &yuv420,
+ hdrTransferFunction,
+ &jpegR, jpegQuality, nullptr); success != android::OK) {
+ ALOGW("Encode JPEG/R failed, error code: %d.", success);
+ return false;
+ }
+
+ if (!stream->write(jpegR.data, jpegR.length)) {
+ ALOGW("Writing JPEG/R to stream failed.");
+ return false;
+ }
+
+ return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
static jboolean YuvImage_compressToJpeg(JNIEnv* env, jobject, jbyteArray inYuv,
jint format, jint width, jint height, jintArray offsets,
jintArray strides, jint jpegQuality, jobject jstream,
@@ -256,11 +414,34 @@ static jboolean YuvImage_compressToJpeg(JNIEnv* env, jobject, jbyteArray inYuv,
delete strm;
return result;
}
+
+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) {
+ jbyte* hdr = env->GetByteArrayElements(inHdr, NULL);
+ jbyte* sdr = env->GetByteArrayElements(inSdr, NULL);
+ SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
+ P010Yuv420ToJpegREncoder encoder;
+
+ jboolean result = JNI_FALSE;
+ if (encoder.encode(env, strm, hdr, hdrColorSpace, sdr, sdrColorSpace,
+ width, height, quality)) {
+ result = JNI_TRUE;
+ }
+
+ env->ReleaseByteArrayElements(inHdr, hdr, 0);
+ env->ReleaseByteArrayElements(inSdr, sdr, 0);
+ delete strm;
+ return result;
+}
///////////////////////////////////////////////////////////////////////////////
static const JNINativeMethod gYuvImageMethods[] = {
{ "nativeCompressToJpeg", "([BIII[I[IILjava/io/OutputStream;[B)Z",
- (void*)YuvImage_compressToJpeg }
+ (void*)YuvImage_compressToJpeg },
+ { "nativeCompressToJpegR", "([BI[BIIIILjava/io/OutputStream;[B)Z",
+ (void*)YuvImage_compressToJpegR }
};
int register_android_graphics_YuvImage(JNIEnv* env)
diff --git a/libs/hwui/jni/YuvToJpegEncoder.h b/libs/hwui/jni/YuvToJpegEncoder.h
index 7e7b935df276..8ef780547184 100644
--- a/libs/hwui/jni/YuvToJpegEncoder.h
+++ b/libs/hwui/jni/YuvToJpegEncoder.h
@@ -1,13 +1,16 @@
#ifndef _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_
#define _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_
-#include "SkTypes.h"
-#include "SkStream.h"
+#include <android/data_space.h>
+#include <ultrahdr/jpegr.h>
+
extern "C" {
#include "jpeglib.h"
#include "jerror.h"
}
+class SkWStream;
+
class YuvToJpegEncoder {
public:
/** Create an encoder based on the YUV format.
@@ -24,7 +27,7 @@ public:
*
* @param stream The jpeg output stream.
* @param inYuv The input yuv data.
- * @param width Width of the the Yuv data in terms of pixels.
+ * @param width Width of the Yuv data in terms of pixels.
* @param height Height of the Yuv data in terms of pixels.
* @param offsets The offsets in each image plane with respect to inYuv.
* @param jpegQuality Picture quality in [0, 100].
@@ -71,4 +74,46 @@ private:
uint8_t* vRows, int rowIndex, int width, int height);
};
+class P010Yuv420ToJpegREncoder {
+public:
+ /** Encode YUV data to jpeg/r, which is output to a stream.
+ * This method will call JpegR::EncodeJPEGR() method. If encoding failed,
+ * Corresponding error code (defined in jpegrerrorcode.h) will be printed and this
+ * method will be terminated and return false.
+ *
+ * @param env JNI environment.
+ * @param stream The jpeg output stream.
+ * @param hdr The input yuv data (p010 format).
+ * @param hdrColorSpaceId color space id for the input hdr.
+ * @param sdr The input yuv data (yuv420p format).
+ * @param sdrColorSpaceId color space id for the input sdr.
+ * @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].
+ * @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);
+
+ /** Map data space (defined in DataSpace.java and data_space.h) to the color gamut
+ * used in JPEG/R
+ *
+ * @param env JNI environment.
+ * @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);
+
+ /** Map data space (defined in DataSpace.java and data_space.h) to the transfer function
+ * used in JPEG/R
+ *
+ * @param env JNI environment.
+ * @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);
+};
+
#endif // _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index 132234b38003..8ba750372d18 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -30,12 +30,24 @@
#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/ScopedStringChars.h>
-#include "FontUtils.h"
#include "Bitmap.h"
+#include "FontUtils.h"
+#include "SkBitmap.h"
+#include "SkBlendMode.h"
+#include "SkClipOp.h"
+#include "SkColor.h"
+#include "SkColorSpace.h"
#include "SkGraphics.h"
+#include "SkImageInfo.h"
+#include "SkMatrix.h"
+#include "SkPath.h"
+#include "SkPoint.h"
+#include "SkRRect.h"
+#include "SkRect.h"
+#include "SkRefCnt.h"
#include "SkRegion.h"
+#include "SkScalar.h"
#include "SkVertices.h"
-#include "SkRRect.h"
namespace minikin {
class MeasuredText;
@@ -431,6 +443,14 @@ static void drawVertices(JNIEnv* env, jobject, jlong canvasHandle,
blendMode, *paint);
}
+static void drawMesh(JNIEnv* env, jobject, jlong canvasHandle, jlong meshHandle, jint modeHandle,
+ jlong paintHandle) {
+ const Mesh* mesh = reinterpret_cast<Mesh*>(meshHandle);
+ SkBlendMode blendMode = static_cast<SkBlendMode>(modeHandle);
+ Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+ get_canvas(canvasHandle)->drawMesh(*mesh, SkBlender::Mode(blendMode), *paint);
+}
+
static void drawNinePatch(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmapHandle,
jlong chunkHandle, jfloat left, jfloat top, jfloat right, jfloat bottom,
jlong paintHandle, jint dstDensity, jint srcDensity) {
@@ -701,9 +721,10 @@ static void setCompatibilityVersion(JNIEnv* env, jobject, jint apiLevel) {
}
static void punchHole(JNIEnv* env, jobject, jlong canvasPtr, jfloat left, jfloat top, jfloat right,
- jfloat bottom, jfloat rx, jfloat ry) {
+ jfloat bottom, jfloat rx, jfloat ry, jfloat alpha) {
auto canvas = reinterpret_cast<Canvas*>(canvasPtr);
- canvas->punchHole(SkRRect::MakeRectXY(SkRect::MakeLTRB(left, top, right, bottom), rx, ry));
+ canvas->punchHole(SkRRect::MakeRectXY(SkRect::MakeLTRB(left, top, right, bottom), rx, ry),
+ alpha);
}
}; // namespace CanvasJNI
@@ -748,38 +769,38 @@ static const JNINativeMethod gMethods[] = {
// If called from Canvas these are regular JNI
// If called from DisplayListCanvas they are @FastNative
static const JNINativeMethod gDrawMethods[] = {
- {"nDrawColor","(JII)V", (void*) CanvasJNI::drawColor},
- {"nDrawColor","(JJJI)V", (void*) CanvasJNI::drawColorLong},
- {"nDrawPaint","(JJ)V", (void*) CanvasJNI::drawPaint},
- {"nDrawPoint", "(JFFJ)V", (void*) CanvasJNI::drawPoint},
- {"nDrawPoints", "(J[FIIJ)V", (void*) CanvasJNI::drawPoints},
- {"nDrawLine", "(JFFFFJ)V", (void*) CanvasJNI::drawLine},
- {"nDrawLines", "(J[FIIJ)V", (void*) CanvasJNI::drawLines},
- {"nDrawRect","(JFFFFJ)V", (void*) CanvasJNI::drawRect},
- {"nDrawRegion", "(JJJ)V", (void*) CanvasJNI::drawRegion },
- {"nDrawRoundRect","(JFFFFFFJ)V", (void*) CanvasJNI::drawRoundRect},
- {"nDrawDoubleRoundRect", "(JFFFFFFFFFFFFJ)V", (void*) CanvasJNI::drawDoubleRoundRectXY},
- {"nDrawDoubleRoundRect", "(JFFFF[FFFFF[FJ)V", (void*) CanvasJNI::drawDoubleRoundRectRadii},
- {"nDrawCircle","(JFFFJ)V", (void*) CanvasJNI::drawCircle},
- {"nDrawOval","(JFFFFJ)V", (void*) CanvasJNI::drawOval},
- {"nDrawArc","(JFFFFFFZJ)V", (void*) CanvasJNI::drawArc},
- {"nDrawPath","(JJJ)V", (void*) CanvasJNI::drawPath},
- {"nDrawVertices", "(JII[FI[FI[II[SIIJ)V", (void*)CanvasJNI::drawVertices},
- {"nDrawNinePatch", "(JJJFFFFJII)V", (void*)CanvasJNI::drawNinePatch},
- {"nDrawBitmapMatrix", "(JJJJ)V", (void*)CanvasJNI::drawBitmapMatrix},
- {"nDrawBitmapMesh", "(JJII[FI[IIJ)V", (void*)CanvasJNI::drawBitmapMesh},
- {"nDrawBitmap","(JJFFJIII)V", (void*) CanvasJNI::drawBitmap},
- {"nDrawBitmap","(JJFFFFFFFFJII)V", (void*) CanvasJNI::drawBitmapRect},
- {"nDrawBitmap", "(J[IIIFFIIZJ)V", (void*)CanvasJNI::drawBitmapArray},
- {"nDrawGlyphs", "(J[I[FIIIJJ)V", (void*)CanvasJNI::drawGlyphs},
- {"nDrawText","(J[CIIFFIJ)V", (void*) CanvasJNI::drawTextChars},
- {"nDrawText","(JLjava/lang/String;IIFFIJ)V", (void*) CanvasJNI::drawTextString},
- {"nDrawTextRun","(J[CIIIIFFZJJ)V", (void*) CanvasJNI::drawTextRunChars},
- {"nDrawTextRun","(JLjava/lang/String;IIIIFFZJ)V", (void*) CanvasJNI::drawTextRunString},
- {"nDrawTextOnPath","(J[CIIJFFIJ)V", (void*) CanvasJNI::drawTextOnPathChars},
- {"nDrawTextOnPath","(JLjava/lang/String;JFFIJ)V", (void*) CanvasJNI::drawTextOnPathString},
- {"nPunchHole", "(JFFFFFF)V", (void*) CanvasJNI::punchHole}
-};
+ {"nDrawColor", "(JII)V", (void*)CanvasJNI::drawColor},
+ {"nDrawColor", "(JJJI)V", (void*)CanvasJNI::drawColorLong},
+ {"nDrawPaint", "(JJ)V", (void*)CanvasJNI::drawPaint},
+ {"nDrawPoint", "(JFFJ)V", (void*)CanvasJNI::drawPoint},
+ {"nDrawPoints", "(J[FIIJ)V", (void*)CanvasJNI::drawPoints},
+ {"nDrawLine", "(JFFFFJ)V", (void*)CanvasJNI::drawLine},
+ {"nDrawLines", "(J[FIIJ)V", (void*)CanvasJNI::drawLines},
+ {"nDrawRect", "(JFFFFJ)V", (void*)CanvasJNI::drawRect},
+ {"nDrawRegion", "(JJJ)V", (void*)CanvasJNI::drawRegion},
+ {"nDrawRoundRect", "(JFFFFFFJ)V", (void*)CanvasJNI::drawRoundRect},
+ {"nDrawDoubleRoundRect", "(JFFFFFFFFFFFFJ)V", (void*)CanvasJNI::drawDoubleRoundRectXY},
+ {"nDrawDoubleRoundRect", "(JFFFF[FFFFF[FJ)V", (void*)CanvasJNI::drawDoubleRoundRectRadii},
+ {"nDrawCircle", "(JFFFJ)V", (void*)CanvasJNI::drawCircle},
+ {"nDrawOval", "(JFFFFJ)V", (void*)CanvasJNI::drawOval},
+ {"nDrawArc", "(JFFFFFFZJ)V", (void*)CanvasJNI::drawArc},
+ {"nDrawPath", "(JJJ)V", (void*)CanvasJNI::drawPath},
+ {"nDrawVertices", "(JII[FI[FI[II[SIIJ)V", (void*)CanvasJNI::drawVertices},
+ {"nDrawMesh", "(JJIJ)V", (void*)CanvasJNI::drawMesh},
+ {"nDrawNinePatch", "(JJJFFFFJII)V", (void*)CanvasJNI::drawNinePatch},
+ {"nDrawBitmapMatrix", "(JJJJ)V", (void*)CanvasJNI::drawBitmapMatrix},
+ {"nDrawBitmapMesh", "(JJII[FI[IIJ)V", (void*)CanvasJNI::drawBitmapMesh},
+ {"nDrawBitmap", "(JJFFJIII)V", (void*)CanvasJNI::drawBitmap},
+ {"nDrawBitmap", "(JJFFFFFFFFJII)V", (void*)CanvasJNI::drawBitmapRect},
+ {"nDrawBitmap", "(J[IIIFFIIZJ)V", (void*)CanvasJNI::drawBitmapArray},
+ {"nDrawGlyphs", "(J[I[FIIIJJ)V", (void*)CanvasJNI::drawGlyphs},
+ {"nDrawText", "(J[CIIFFIJ)V", (void*)CanvasJNI::drawTextChars},
+ {"nDrawText", "(JLjava/lang/String;IIFFIJ)V", (void*)CanvasJNI::drawTextString},
+ {"nDrawTextRun", "(J[CIIIIFFZJJ)V", (void*)CanvasJNI::drawTextRunChars},
+ {"nDrawTextRun", "(JLjava/lang/String;IIIIFFZJ)V", (void*)CanvasJNI::drawTextRunString},
+ {"nDrawTextOnPath", "(J[CIIJFFIJ)V", (void*)CanvasJNI::drawTextOnPathChars},
+ {"nDrawTextOnPath", "(JLjava/lang/String;JFFIJ)V", (void*)CanvasJNI::drawTextOnPathString},
+ {"nPunchHole", "(JFFFFFFF)V", (void*)CanvasJNI::punchHole}};
int register_android_graphics_Canvas(JNIEnv* env) {
int ret = 0;
diff --git a/libs/hwui/jni/android_graphics_ColorSpace.cpp b/libs/hwui/jni/android_graphics_ColorSpace.cpp
index 232fd71a12b4..63d3f83febd6 100644
--- a/libs/hwui/jni/android_graphics_ColorSpace.cpp
+++ b/libs/hwui/jni/android_graphics_ColorSpace.cpp
@@ -18,7 +18,6 @@
#include "SkColor.h"
#include "SkColorSpace.h"
-#include "SkHalf.h"
using namespace android;
@@ -40,15 +39,58 @@ static skcms_Matrix3x3 getNativeXYZMatrix(JNIEnv* env, jfloatArray xyzD50) {
///////////////////////////////////////////////////////////////////////////////
+#if defined(__ANDROID__) // __fp16 is not defined on non-Android builds
static float halfToFloat(uint16_t bits) {
-#ifdef __ANDROID__ // __fp16 is not defined on non-Android builds
__fp16 h;
memcpy(&h, &bits, 2);
return (float)h;
+}
#else
- return SkHalfToFloat(bits);
-#endif
+// This is Skia's implementation of SkHalfToFloat, which is
+// based on Fabien Giesen's half_to_float_fast2()
+// see https://fgiesen.wordpress.com/2012/03/28/half-to-float-done-quic/
+static uint16_t halfMantissa(uint16_t h) {
+ return h & 0x03ff;
+}
+
+static uint16_t halfExponent(uint16_t h) {
+ return (h >> 10) & 0x001f;
+}
+
+static uint16_t halfSign(uint16_t h) {
+ return h >> 15;
+}
+
+union FloatUIntUnion {
+ uint32_t mUInt; // this must come first for the initializations below to work
+ float mFloat;
+};
+
+static float halfToFloat(uint16_t bits) {
+ static const FloatUIntUnion magic = { 126 << 23 };
+ FloatUIntUnion o;
+
+ if (halfExponent(bits) == 0) {
+ // Zero / Denormal
+ o.mUInt = magic.mUInt + halfMantissa(bits);
+ o.mFloat -= magic.mFloat;
+ } else {
+ // Set mantissa
+ o.mUInt = halfMantissa(bits) << 13;
+ // Set exponent
+ if (halfExponent(bits) == 0x1f) {
+ // Inf/NaN
+ o.mUInt |= (255 << 23);
+ } else {
+ o.mUInt |= ((127 - 15 + halfExponent(bits)) << 23);
+ }
+ }
+
+ // Set sign
+ o.mUInt |= (halfSign(bits) << 31);
+ return o.mFloat;
}
+#endif // defined(__ANDROID__)
SkColor4f GraphicsJNI::convertColorLong(jlong color) {
if ((color & 0x3f) == 0) {
diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
new file mode 100644
index 000000000000..706f18c3be80
--- /dev/null
+++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "HardwareBufferRenderer"
+#define ATRACE_TAG ATRACE_TAG_VIEW
+
+#include <GraphicsJNI.h>
+#include <RootRenderNode.h>
+#include <TreeInfo.h>
+#include <android-base/unique_fd.h>
+#include <android/native_window.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <renderthread/CanvasContext.h>
+#include <renderthread/RenderProxy.h>
+#include <renderthread/RenderThread.h>
+
+#include "HardwareBufferHelpers.h"
+#include "JvmErrorReporter.h"
+
+namespace android {
+
+using namespace android::uirenderer;
+using namespace android::uirenderer::renderthread;
+
+struct {
+ jclass clazz;
+ jmethodID invokeRenderCallback;
+} gHardwareBufferRendererClassInfo;
+
+static RenderCallback createRenderCallback(JNIEnv* env, jobject releaseCallback) {
+ if (releaseCallback == nullptr) return nullptr;
+
+ JavaVM* vm = nullptr;
+ LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM");
+ auto globalCallbackRef =
+ std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(releaseCallback));
+ return [globalCallbackRef](android::base::unique_fd&& fd, int status) {
+ globalCallbackRef->env()->CallStaticVoidMethod(
+ gHardwareBufferRendererClassInfo.clazz,
+ gHardwareBufferRendererClassInfo.invokeRenderCallback, globalCallbackRef->object(),
+ reinterpret_cast<jint>(fd.release()), reinterpret_cast<jint>(status));
+ };
+}
+
+static long android_graphics_HardwareBufferRenderer_createRootNode(JNIEnv* env, jobject) {
+ auto* node = new RootRenderNode(std::make_unique<JvmErrorReporter>(env));
+ node->incStrong(nullptr);
+ node->setName("RootRenderNode");
+ return reinterpret_cast<jlong>(node);
+}
+
+static void android_graphics_hardwareBufferRenderer_destroyRootNode(JNIEnv*, jobject,
+ jlong renderNodePtr) {
+ auto* node = reinterpret_cast<RootRenderNode*>(renderNodePtr);
+ node->destroy();
+}
+
+static long android_graphics_HardwareBufferRenderer_create(JNIEnv* env, jobject, jobject buffer,
+ jlong renderNodePtr) {
+ auto* hardwareBuffer = HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(env, buffer);
+ auto* rootRenderNode = reinterpret_cast<RootRenderNode*>(renderNodePtr);
+ ContextFactoryImpl factory(rootRenderNode);
+ auto* proxy = new RenderProxy(false, rootRenderNode, &factory);
+ proxy->setHardwareBuffer(hardwareBuffer);
+ return (jlong)proxy;
+}
+
+static void HardwareBufferRenderer_destroy(jlong renderProxy) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxy);
+ delete proxy;
+}
+
+static SkMatrix createMatrixFromBufferTransform(SkScalar width, SkScalar height, int transform) {
+ switch (transform) {
+ case ANATIVEWINDOW_TRANSFORM_ROTATE_90:
+ return SkMatrix::MakeAll(0, -1, height, 1, 0, 0, 0, 0, 1);
+ case ANATIVEWINDOW_TRANSFORM_ROTATE_180:
+ return SkMatrix::MakeAll(-1, 0, width, 0, -1, height, 0, 0, 1);
+ case ANATIVEWINDOW_TRANSFORM_ROTATE_270:
+ return SkMatrix::MakeAll(0, 1, 0, -1, 0, width, 0, 0, 1);
+ default:
+ ALOGE("Invalid transform provided. Transform should be validated from"
+ "the java side. Leveraging identity transform as a fallback");
+ [[fallthrough]];
+ case ANATIVEWINDOW_TRANSFORM_IDENTITY:
+ return SkMatrix::I();
+ }
+}
+
+static int android_graphics_HardwareBufferRenderer_render(JNIEnv* env, jobject, jlong renderProxy,
+ jint transform, jint width, jint height,
+ jlong colorspacePtr, jobject consumer) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxy);
+ auto skWidth = static_cast<SkScalar>(width);
+ auto skHeight = static_cast<SkScalar>(height);
+ auto matrix = createMatrixFromBufferTransform(skWidth, skHeight, transform);
+ auto colorSpace = GraphicsJNI::getNativeColorSpace(colorspacePtr);
+ proxy->setHardwareBufferRenderParams(HardwareBufferRenderParams(
+ width, height, matrix, colorSpace, createRenderCallback(env, consumer)));
+ nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC);
+ UiFrameInfoBuilder(proxy->frameInfo())
+ .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID,
+ UiFrameInfoBuilder::UNKNOWN_DEADLINE,
+ UiFrameInfoBuilder::UNKNOWN_FRAME_INTERVAL)
+ .addFlag(FrameInfoFlags::SurfaceCanvas);
+ return proxy->syncAndDrawFrame();
+}
+
+static void android_graphics_HardwareBufferRenderer_setLightGeometry(JNIEnv*, jobject,
+ jlong renderProxyPtr,
+ jfloat lightX, jfloat lightY,
+ jfloat lightZ,
+ jfloat lightRadius) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxyPtr);
+ proxy->setLightGeometry((Vector3){lightX, lightY, lightZ}, lightRadius);
+}
+
+static void android_graphics_HardwareBufferRenderer_setLightAlpha(JNIEnv* env, jobject,
+ jlong renderProxyPtr,
+ jfloat ambientShadowAlpha,
+ jfloat spotShadowAlpha) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxyPtr);
+ proxy->setLightAlpha((uint8_t)(255 * ambientShadowAlpha), (uint8_t)(255 * spotShadowAlpha));
+}
+
+static jlong android_graphics_HardwareBufferRenderer_getFinalizer() {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&HardwareBufferRenderer_destroy));
+}
+
+// ----------------------------------------------------------------------------
+// JNI Glue
+// ----------------------------------------------------------------------------
+
+const char* const kClassPathName = "android/graphics/HardwareBufferRenderer";
+
+static const JNINativeMethod gMethods[] = {
+ {"nCreateHardwareBufferRenderer", "(Landroid/hardware/HardwareBuffer;J)J",
+ (void*)android_graphics_HardwareBufferRenderer_create},
+ {"nRender", "(JIIIJLjava/util/function/Consumer;)I",
+ (void*)android_graphics_HardwareBufferRenderer_render},
+ {"nCreateRootRenderNode", "()J",
+ (void*)android_graphics_HardwareBufferRenderer_createRootNode},
+ {"nSetLightGeometry", "(JFFFF)V",
+ (void*)android_graphics_HardwareBufferRenderer_setLightGeometry},
+ {"nSetLightAlpha", "(JFF)V", (void*)android_graphics_HardwareBufferRenderer_setLightAlpha},
+ {"nGetFinalizer", "()J", (void*)android_graphics_HardwareBufferRenderer_getFinalizer},
+ {"nDestroyRootRenderNode", "(J)V",
+ (void*)android_graphics_hardwareBufferRenderer_destroyRootNode}};
+
+int register_android_graphics_HardwareBufferRenderer(JNIEnv* env) {
+ jclass hardwareBufferRendererClazz =
+ FindClassOrDie(env, "android/graphics/HardwareBufferRenderer");
+ gHardwareBufferRendererClassInfo.clazz =
+ reinterpret_cast<jclass>(env->NewGlobalRef(hardwareBufferRendererClazz));
+ gHardwareBufferRendererClassInfo.invokeRenderCallback =
+ GetStaticMethodIDOrDie(env, hardwareBufferRendererClazz, "invokeRenderCallback",
+ "(Ljava/util/function/Consumer;II)V");
+ HardwareBufferHelpers::init();
+ return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
+}
+
+} // namespace android \ No newline at end of file
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index c48448dffdd2..d04de37f6961 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -23,8 +23,16 @@
#include <Picture.h>
#include <Properties.h>
#include <RootRenderNode.h>
+#include <SkBitmap.h>
+#include <SkColorSpace.h>
+#include <SkData.h>
+#include <SkImage.h>
#include <SkImagePriv.h>
+#include <SkPicture.h>
+#include <SkPixmap.h>
#include <SkSerialProcs.h>
+#include <SkStream.h>
+#include <SkTypeface.h>
#include <dlfcn.h>
#include <gui/TraceUtils.h>
#include <inttypes.h>
@@ -48,6 +56,7 @@
#include <atomic>
#include <vector>
+#include "JvmErrorReporter.h"
#include "android_graphics_HardwareRendererObserver.h"
namespace android {
@@ -80,6 +89,11 @@ struct {
jmethodID onFrameComplete;
} gFrameCompleteCallback;
+struct {
+ jmethodID onCopyFinished;
+ jmethodID getDestinationBitmap;
+} gCopyRequest;
+
static JNIEnv* getenv(JavaVM* vm) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
@@ -91,20 +105,6 @@ static JNIEnv* getenv(JavaVM* vm) {
typedef ANativeWindow* (*ANW_fromSurface)(JNIEnv* env, jobject surface);
ANW_fromSurface fromSurface;
-class JvmErrorReporter : public ErrorHandler {
-public:
- JvmErrorReporter(JNIEnv* env) {
- env->GetJavaVM(&mVm);
- }
-
- virtual void onError(const std::string& message) override {
- JNIEnv* env = getenv(mVm);
- jniThrowException(env, "java/lang/IllegalStateException", message.c_str());
- }
-private:
- JavaVM* mVm;
-};
-
class FrameCommitWrapper : public LightRefBase<FrameCommitWrapper> {
public:
explicit FrameCommitWrapper(JNIEnv* env, jobject jobject) {
@@ -242,10 +242,16 @@ static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz,
proxy->setOpaque(opaque);
}
-static void android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz,
- jlong proxyPtr, jint colorMode) {
+static jfloat android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz, jlong proxyPtr,
+ jint colorMode) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
- proxy->setColorMode(static_cast<ColorMode>(colorMode));
+ return proxy->setColorMode(static_cast<ColorMode>(colorMode));
+}
+
+static void android_view_ThreadedRenderer_setTargetSdrHdrRatio(JNIEnv* env, jobject clazz,
+ jlong proxyPtr, jfloat ratio) {
+ RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
+ return proxy->setRenderSdrHdrRatio(ratio);
}
static void android_view_ThreadedRenderer_setSdrWhitePoint(JNIEnv* env, jobject clazz,
@@ -258,6 +264,16 @@ static void android_view_ThreadedRenderer_setIsHighEndGfx(JNIEnv* env, jobject c
Properties::setIsHighEndGfx(jIsHighEndGfx);
}
+static void android_view_ThreadedRenderer_setIsLowRam(JNIEnv* env, jobject clazz,
+ jboolean isLowRam) {
+ Properties::setIsLowRam(isLowRam);
+}
+
+static void android_view_ThreadedRenderer_setIsSystemOrPersistent(JNIEnv* env, jobject clazz,
+ jboolean isSystemOrPersistent) {
+ Properties::setIsSystemOrPersistent(isSystemOrPersistent);
+}
+
static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz,
jlong proxyPtr, jlongArray frameInfo,
jint frameInfoSize) {
@@ -346,6 +362,10 @@ static void android_view_ThreadedRenderer_trimMemory(JNIEnv* env, jobject clazz,
RenderProxy::trimMemory(level);
}
+static void android_view_ThreadedRenderer_trimCaches(JNIEnv* env, jobject clazz, jint level) {
+ RenderProxy::trimCaches(level);
+}
+
static void android_view_ThreadedRenderer_overrideProperty(JNIEnv* env, jobject clazz,
jstring name, jstring value) {
const char* nameCharArray = env->GetStringUTFChars(name, NULL);
@@ -420,26 +440,6 @@ static void android_view_ThreadedRenderer_forceDrawNextFrame(JNIEnv* env, jobjec
proxy->forceDrawNextFrame();
}
-class JGlobalRefHolder {
-public:
- JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {}
-
- virtual ~JGlobalRefHolder() {
- getenv(mVm)->DeleteGlobalRef(mObject);
- mObject = nullptr;
- }
-
- jobject object() { return mObject; }
- JavaVM* vm() { return mVm; }
-
-private:
- JGlobalRefHolder(const JGlobalRefHolder&) = delete;
- void operator=(const JGlobalRefHolder&) = delete;
-
- JavaVM* mVm;
- jobject mObject;
-};
-
using TextureMap = std::unordered_map<uint32_t, sk_sp<SkImage>>;
struct PictureCaptureState {
@@ -451,7 +451,7 @@ struct PictureCaptureState {
};
// TODO: This & Multi-SKP & Single-SKP should all be de-duped into
-// a single "make a SkPicture serailizable-safe" utility somewhere
+// a single "make a SkPicture serializable-safe" utility somewhere
class PictureWrapper : public Picture {
public:
PictureWrapper(sk_sp<SkPicture>&& src, const std::shared_ptr<PictureCaptureState>& state)
@@ -555,10 +555,9 @@ static void android_view_ThreadedRenderer_setPictureCapturedCallbackJNI(JNIEnv*
auto pictureState = std::make_shared<PictureCaptureState>();
proxy->setPictureCapturedCallback([globalCallbackRef,
pictureState](sk_sp<SkPicture>&& picture) {
- JNIEnv* env = getenv(globalCallbackRef->vm());
Picture* wrapper = new PictureWrapper{std::move(picture), pictureState};
- env->CallStaticVoidMethod(gHardwareRenderer.clazz,
- gHardwareRenderer.invokePictureCapturedCallback,
+ globalCallbackRef->env()->CallStaticVoidMethod(
+ gHardwareRenderer.clazz, gHardwareRenderer.invokePictureCapturedCallback,
static_cast<jlong>(reinterpret_cast<intptr_t>(wrapper)),
globalCallbackRef->object());
});
@@ -575,16 +574,14 @@ static void android_view_ThreadedRenderer_setASurfaceTransactionCallback(
LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM");
auto globalCallbackRef = std::make_shared<JGlobalRefHolder>(
vm, env->NewGlobalRef(aSurfaceTransactionCallback));
- proxy->setASurfaceTransactionCallback(
- [globalCallbackRef](int64_t transObj, int64_t scObj, int64_t frameNr) -> bool {
- JNIEnv* env = getenv(globalCallbackRef->vm());
- jboolean ret = env->CallBooleanMethod(
- globalCallbackRef->object(),
- gASurfaceTransactionCallback.onMergeTransaction,
- static_cast<jlong>(transObj), static_cast<jlong>(scObj),
- static_cast<jlong>(frameNr));
- return ret;
- });
+ proxy->setASurfaceTransactionCallback([globalCallbackRef](int64_t transObj, int64_t scObj,
+ int64_t frameNr) -> bool {
+ jboolean ret = globalCallbackRef->env()->CallBooleanMethod(
+ globalCallbackRef->object(), gASurfaceTransactionCallback.onMergeTransaction,
+ static_cast<jlong>(transObj), static_cast<jlong>(scObj),
+ static_cast<jlong>(frameNr));
+ return ret;
+ });
}
}
@@ -599,9 +596,8 @@ static void android_view_ThreadedRenderer_setPrepareSurfaceControlForWebviewCall
auto globalCallbackRef =
std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(callback));
proxy->setPrepareSurfaceControlForWebviewCallback([globalCallbackRef]() {
- JNIEnv* env = getenv(globalCallbackRef->vm());
- env->CallVoidMethod(globalCallbackRef->object(),
- gPrepareSurfaceControlForWebviewCallback.prepare);
+ globalCallbackRef->env()->CallVoidMethod(
+ globalCallbackRef->object(), gPrepareSurfaceControlForWebviewCallback.prepare);
});
}
}
@@ -618,7 +614,7 @@ static void android_view_ThreadedRenderer_setFrameCallback(JNIEnv* env,
env->NewGlobalRef(frameCallback));
proxy->setFrameCallback([globalCallbackRef](int32_t syncResult,
int64_t frameNr) -> std::function<void(bool)> {
- JNIEnv* env = getenv(globalCallbackRef->vm());
+ JNIEnv* env = globalCallbackRef->env();
ScopedLocalRef<jobject> frameCommitCallback(
env, env->CallObjectMethod(
globalCallbackRef->object(), gFrameDrawingCallback.onFrameDraw,
@@ -657,22 +653,45 @@ static void android_view_ThreadedRenderer_setFrameCompleteCallback(JNIEnv* env,
auto globalCallbackRef =
std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(callback));
proxy->setFrameCompleteCallback([globalCallbackRef]() {
- JNIEnv* env = getenv(globalCallbackRef->vm());
- env->CallVoidMethod(globalCallbackRef->object(),
- gFrameCompleteCallback.onFrameComplete);
+ globalCallbackRef->env()->CallVoidMethod(globalCallbackRef->object(),
+ gFrameCompleteCallback.onFrameComplete);
});
}
}
-static jint android_view_ThreadedRenderer_copySurfaceInto(JNIEnv* env,
- jobject clazz, jobject jsurface, jint left, jint top,
- jint right, jint bottom, jlong bitmapPtr) {
- SkBitmap bitmap;
- bitmap::toBitmap(bitmapPtr).getSkBitmap(&bitmap);
+class CopyRequestAdapter : public CopyRequest {
+public:
+ CopyRequestAdapter(JavaVM* vm, jobject jCopyRequest, Rect srcRect)
+ : CopyRequest(srcRect), mRefHolder(vm, jCopyRequest) {}
+
+ virtual SkBitmap getDestinationBitmap(int srcWidth, int srcHeight) override {
+ jlong bitmapPtr = mRefHolder.env()->CallLongMethod(
+ mRefHolder.object(), gCopyRequest.getDestinationBitmap, srcWidth, srcHeight);
+ SkBitmap bitmap;
+ bitmap::toBitmap(bitmapPtr).getSkBitmap(&bitmap);
+ return bitmap;
+ }
+
+ virtual void onCopyFinished(CopyResult result) override {
+ mRefHolder.env()->CallVoidMethod(mRefHolder.object(), gCopyRequest.onCopyFinished,
+ static_cast<jint>(result));
+ }
+
+private:
+ JGlobalRefHolder mRefHolder;
+};
+
+static void android_view_ThreadedRenderer_copySurfaceInto(JNIEnv* env, jobject clazz,
+ jobject jsurface, jint left, jint top,
+ jint right, jint bottom,
+ jobject jCopyRequest) {
+ JavaVM* vm = nullptr;
+ LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM");
+ auto copyRequest = std::make_shared<CopyRequestAdapter>(vm, env->NewGlobalRef(jCopyRequest),
+ Rect(left, top, right, bottom));
ANativeWindow* window = fromSurface(env, jsurface);
- jint result = RenderProxy::copySurfaceInto(window, left, top, right, bottom, &bitmap);
+ RenderProxy::copySurfaceInto(window, std::move(copyRequest));
ANativeWindow_release(window);
- return result;
}
class ContextFactory : public IContextFactory {
@@ -811,6 +830,16 @@ static void android_view_ThreadedRenderer_setRtAnimationsEnabled(JNIEnv* env, jo
RenderProxy::setRtAnimationsEnabled(enabled);
}
+static void android_view_ThreadedRenderer_notifyCallbackPending(JNIEnv*, jclass, jlong proxyPtr) {
+ RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
+ proxy->notifyCallbackPending();
+}
+
+static void android_view_ThreadedRenderer_notifyExpensiveFrame(JNIEnv*, jclass, jlong proxyPtr) {
+ RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
+ proxy->notifyExpensiveFrame();
+}
+
// Plumbs the display density down to DeviceInfo.
static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, jint densityDpi) {
// Convert from dpi to density-independent pixels.
@@ -818,17 +847,18 @@ static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass,
DeviceInfo::setDensity(density);
}
-static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv*, jclass, jint physicalWidth,
- jint physicalHeight, jfloat refreshRate,
- jint wideColorDataspace,
- jlong appVsyncOffsetNanos,
- jlong presentationDeadlineNanos) {
+static void android_view_ThreadedRenderer_initDisplayInfo(
+ JNIEnv* env, jclass, jint physicalWidth, jint physicalHeight, jfloat refreshRate,
+ jint wideColorDataspace, jlong appVsyncOffsetNanos, jlong presentationDeadlineNanos,
+ jboolean supportFp16ForHdr, jboolean supportMixedColorSpaces) {
DeviceInfo::setWidth(physicalWidth);
DeviceInfo::setHeight(physicalHeight);
DeviceInfo::setRefreshRate(refreshRate);
DeviceInfo::setWideColorDataspace(static_cast<ADataSpace>(wideColorDataspace));
DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos);
DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos);
+ DeviceInfo::setSupportFp16ForHdr(supportFp16ForHdr);
+ DeviceInfo::setSupportMixedColorSpaces(supportMixedColorSpaces);
}
static void android_view_ThreadedRenderer_setDrawingEnabled(JNIEnv*, jclass, jboolean enabled) {
@@ -907,9 +937,14 @@ static const JNINativeMethod gMethods[] = {
{"nSetLightAlpha", "(JFF)V", (void*)android_view_ThreadedRenderer_setLightAlpha},
{"nSetLightGeometry", "(JFFFF)V", (void*)android_view_ThreadedRenderer_setLightGeometry},
{"nSetOpaque", "(JZ)V", (void*)android_view_ThreadedRenderer_setOpaque},
- {"nSetColorMode", "(JI)V", (void*)android_view_ThreadedRenderer_setColorMode},
+ {"nSetColorMode", "(JI)F", (void*)android_view_ThreadedRenderer_setColorMode},
+ {"nSetTargetSdrHdrRatio", "(JF)V",
+ (void*)android_view_ThreadedRenderer_setTargetSdrHdrRatio},
{"nSetSdrWhitePoint", "(JF)V", (void*)android_view_ThreadedRenderer_setSdrWhitePoint},
{"nSetIsHighEndGfx", "(Z)V", (void*)android_view_ThreadedRenderer_setIsHighEndGfx},
+ {"nSetIsLowRam", "(Z)V", (void*)android_view_ThreadedRenderer_setIsLowRam},
+ {"nSetIsSystemOrPersistent", "(Z)V",
+ (void*)android_view_ThreadedRenderer_setIsSystemOrPersistent},
{"nSyncAndDrawFrame", "(J[JI)I", (void*)android_view_ThreadedRenderer_syncAndDrawFrame},
{"nDestroy", "(JJ)V", (void*)android_view_ThreadedRenderer_destroy},
{"nRegisterAnimatingRenderNode", "(JJ)V",
@@ -961,7 +996,8 @@ static const JNINativeMethod gMethods[] = {
(void*)android_view_ThreadedRenderer_setFrameCompleteCallback},
{"nAddObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_addObserver},
{"nRemoveObserver", "(JJ)V", (void*)android_view_ThreadedRenderer_removeObserver},
- {"nCopySurfaceInto", "(Landroid/view/Surface;IIIIJ)I",
+ {"nCopySurfaceInto",
+ "(Landroid/view/Surface;IIIILandroid/graphics/HardwareRenderer$CopyRequest;)V",
(void*)android_view_ThreadedRenderer_copySurfaceInto},
{"nCreateHardwareBitmap", "(JII)Landroid/graphics/Bitmap;",
(void*)android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode},
@@ -974,7 +1010,7 @@ static const JNINativeMethod gMethods[] = {
{"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark},
{"nSetDisplayDensityDpi", "(I)V",
(void*)android_view_ThreadedRenderer_setDisplayDensityDpi},
- {"nInitDisplayInfo", "(IIFIJJ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
+ {"nInitDisplayInfo", "(IIFIJJZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
{"preload", "()V", (void*)android_view_ThreadedRenderer_preload},
{"isWebViewOverlaysEnabled", "()Z",
(void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled},
@@ -982,6 +1018,11 @@ static const JNINativeMethod gMethods[] = {
{"nIsDrawingEnabled", "()Z", (void*)android_view_ThreadedRenderer_isDrawingEnabled},
{"nSetRtAnimationsEnabled", "(Z)V",
(void*)android_view_ThreadedRenderer_setRtAnimationsEnabled},
+ {"nNotifyCallbackPending", "(J)V",
+ (void*)android_view_ThreadedRenderer_notifyCallbackPending},
+ {"nNotifyExpensiveFrame", "(J)V",
+ (void*)android_view_ThreadedRenderer_notifyExpensiveFrame},
+ {"nTrimCaches", "(I)V", (void*)android_view_ThreadedRenderer_trimCaches},
};
static JavaVM* mJvm = nullptr;
@@ -1034,6 +1075,11 @@ int register_android_view_ThreadedRenderer(JNIEnv* env) {
gFrameCompleteCallback.onFrameComplete =
GetMethodIDOrDie(env, frameCompleteClass, "onFrameComplete", "()V");
+ jclass copyRequest = FindClassOrDie(env, "android/graphics/HardwareRenderer$CopyRequest");
+ gCopyRequest.onCopyFinished = GetMethodIDOrDie(env, copyRequest, "onCopyFinished", "(I)V");
+ gCopyRequest.getDestinationBitmap =
+ GetMethodIDOrDie(env, copyRequest, "getDestinationBitmap", "(II)J");
+
void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
fromSurface = (ANW_fromSurface)dlsym(handle_, "ANativeWindow_fromSurface");
LOG_ALWAYS_FATAL_IF(fromSurface == nullptr,
diff --git a/libs/hwui/jni/android_graphics_Matrix.cpp b/libs/hwui/jni/android_graphics_Matrix.cpp
index cf6702e45fff..ca667b0d09bc 100644
--- a/libs/hwui/jni/android_graphics_Matrix.cpp
+++ b/libs/hwui/jni/android_graphics_Matrix.cpp
@@ -23,8 +23,6 @@ namespace android {
static_assert(sizeof(SkMatrix) == 40, "Unexpected sizeof(SkMatrix), "
"update size in Matrix.java#NATIVE_ALLOCATION_SIZE and here");
-static_assert(SK_SCALAR_IS_FLOAT, "SK_SCALAR_IS_FLOAT is false, "
- "only float scalar is supported");
class SkMatrixGlue {
public:
diff --git a/libs/hwui/jni/android_graphics_Mesh.cpp b/libs/hwui/jni/android_graphics_Mesh.cpp
new file mode 100644
index 000000000000..5cb43e54e499
--- /dev/null
+++ b/libs/hwui/jni/android_graphics_Mesh.cpp
@@ -0,0 +1,205 @@
+/*
+ * 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 <Mesh.h>
+#include <SkMesh.h>
+#include <jni.h>
+
+#include <utility>
+
+#include "BufferUtils.h"
+#include "GraphicsJNI.h"
+#include "graphics_jni_helpers.h"
+
+#define gIndexByteSize 2
+
+namespace android {
+
+static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject vertexBuffer,
+ jboolean isDirect, jint vertexCount, jint vertexOffset, jfloat left, jfloat top,
+ jfloat right, jfloat bottom) {
+ auto skMeshSpec = sk_ref_sp(reinterpret_cast<SkMeshSpecification*>(meshSpec));
+ size_t bufferSize = vertexCount * skMeshSpec->stride();
+ auto buffer = copyJavaNioBufferToVector(env, vertexBuffer, bufferSize, isDirect);
+ if (env->ExceptionCheck()) {
+ return 0;
+ }
+ auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
+ auto meshPtr = new Mesh(skMeshSpec, mode, std::move(buffer), vertexCount, vertexOffset,
+ std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect);
+ auto [valid, msg] = meshPtr->validate();
+ if (!valid) {
+ jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str());
+ }
+ return reinterpret_cast<jlong>(meshPtr);
+}
+
+static jlong makeIndexed(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject vertexBuffer,
+ jboolean isVertexDirect, jint vertexCount, jint vertexOffset,
+ jobject indexBuffer, jboolean isIndexDirect, jint indexCount,
+ jint indexOffset, jfloat left, jfloat top, jfloat right, jfloat bottom) {
+ auto skMeshSpec = sk_ref_sp(reinterpret_cast<SkMeshSpecification*>(meshSpec));
+ auto vertexBufferSize = vertexCount * skMeshSpec->stride();
+ auto indexBufferSize = indexCount * gIndexByteSize;
+ auto vBuf = copyJavaNioBufferToVector(env, vertexBuffer, vertexBufferSize, isVertexDirect);
+ if (env->ExceptionCheck()) {
+ return 0;
+ }
+ auto iBuf = copyJavaNioBufferToVector(env, indexBuffer, indexBufferSize, isIndexDirect);
+ if (env->ExceptionCheck()) {
+ return 0;
+ }
+ auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
+ auto meshPtr = new Mesh(skMeshSpec, mode, std::move(vBuf), vertexCount, vertexOffset,
+ std::move(iBuf), indexCount, indexOffset,
+ std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect);
+ auto [valid, msg] = meshPtr->validate();
+ if (!valid) {
+ jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str());
+ }
+
+ return reinterpret_cast<jlong>(meshPtr);
+}
+
+static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ int ret = jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", fmt, args);
+ va_end(args);
+ return ret;
+}
+
+static bool isIntUniformType(const SkRuntimeEffect::Uniform::Type& type) {
+ switch (type) {
+ case SkRuntimeEffect::Uniform::Type::kFloat:
+ case SkRuntimeEffect::Uniform::Type::kFloat2:
+ case SkRuntimeEffect::Uniform::Type::kFloat3:
+ case SkRuntimeEffect::Uniform::Type::kFloat4:
+ case SkRuntimeEffect::Uniform::Type::kFloat2x2:
+ case SkRuntimeEffect::Uniform::Type::kFloat3x3:
+ case SkRuntimeEffect::Uniform::Type::kFloat4x4:
+ return false;
+ case SkRuntimeEffect::Uniform::Type::kInt:
+ case SkRuntimeEffect::Uniform::Type::kInt2:
+ case SkRuntimeEffect::Uniform::Type::kInt3:
+ case SkRuntimeEffect::Uniform::Type::kInt4:
+ return true;
+ }
+}
+
+static void nativeUpdateFloatUniforms(JNIEnv* env, MeshUniformBuilder* builder,
+ const char* uniformName, const float values[], int count,
+ bool isColor) {
+ MeshUniformBuilder::MeshUniform uniform = builder->uniform(uniformName);
+ if (uniform.fVar == nullptr) {
+ ThrowIAEFmt(env, "unable to find uniform named %s", uniformName);
+ } else if (isColor != ((uniform.fVar->flags & SkRuntimeEffect::Uniform::kColor_Flag) != 0)) {
+ if (isColor) {
+ jniThrowExceptionFmt(
+ env, "java/lang/IllegalArgumentException",
+ "attempting to set a color uniform using the non-color specific APIs: %s %x",
+ uniformName, uniform.fVar->flags);
+ } else {
+ ThrowIAEFmt(env,
+ "attempting to set a non-color uniform using the setColorUniform APIs: %s",
+ uniformName);
+ }
+ } else if (isIntUniformType(uniform.fVar->type)) {
+ ThrowIAEFmt(env, "attempting to set a int uniform using the setUniform APIs: %s",
+ uniformName);
+ } else if (!uniform.set<float>(values, count)) {
+ ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]",
+ uniform.fVar->sizeInBytes(), sizeof(float) * count);
+ }
+}
+
+static void updateFloatUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName,
+ jfloat value1, jfloat value2, jfloat value3, jfloat value4,
+ jint count) {
+ auto* wrapper = reinterpret_cast<Mesh*>(meshWrapper);
+ ScopedUtfChars name(env, uniformName);
+ const float values[4] = {value1, value2, value3, value4};
+ nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count, false);
+ wrapper->markDirty();
+}
+
+static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring jUniformName,
+ jfloatArray jvalues, jboolean isColor) {
+ auto wrapper = reinterpret_cast<Mesh*>(meshWrapper);
+ ScopedUtfChars name(env, jUniformName);
+ AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess);
+ nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(),
+ autoValues.length(), isColor);
+ wrapper->markDirty();
+}
+
+static void nativeUpdateIntUniforms(JNIEnv* env, MeshUniformBuilder* builder,
+ const char* uniformName, const int values[], int count) {
+ MeshUniformBuilder::MeshUniform uniform = builder->uniform(uniformName);
+ if (uniform.fVar == nullptr) {
+ ThrowIAEFmt(env, "unable to find uniform named %s", uniformName);
+ } else if (!isIntUniformType(uniform.fVar->type)) {
+ ThrowIAEFmt(env, "attempting to set a non-int uniform using the setIntUniform APIs: %s",
+ uniformName);
+ } else if (!uniform.set<int>(values, count)) {
+ ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]",
+ uniform.fVar->sizeInBytes(), sizeof(float) * count);
+ }
+}
+
+static void updateIntUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName,
+ jint value1, jint value2, jint value3, jint value4, jint count) {
+ auto wrapper = reinterpret_cast<Mesh*>(meshWrapper);
+ ScopedUtfChars name(env, uniformName);
+ const int values[4] = {value1, value2, value3, value4};
+ nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count);
+ wrapper->markDirty();
+}
+
+static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName,
+ jintArray values) {
+ auto wrapper = reinterpret_cast<Mesh*>(meshWrapper);
+ ScopedUtfChars name(env, uniformName);
+ AutoJavaIntArray autoValues(env, values, 0);
+ nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(),
+ autoValues.length());
+ wrapper->markDirty();
+}
+
+static void MeshWrapper_destroy(Mesh* wrapper) {
+ delete wrapper;
+}
+
+static jlong getMeshFinalizer(JNIEnv*, jobject) {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshWrapper_destroy));
+}
+
+static const JNINativeMethod gMeshMethods[] = {
+ {"nativeGetFinalizer", "()J", (void*)getMeshFinalizer},
+ {"nativeMake", "(JILjava/nio/Buffer;ZIIFFFF)J", (void*)make},
+ {"nativeMakeIndexed", "(JILjava/nio/Buffer;ZIILjava/nio/ShortBuffer;ZIIFFFF)J",
+ (void*)makeIndexed},
+ {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", (void*)updateFloatArrayUniforms},
+ {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V", (void*)updateFloatUniforms},
+ {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V", (void*)updateIntArrayUniforms},
+ {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V", (void*)updateIntUniforms}};
+
+int register_android_graphics_Mesh(JNIEnv* env) {
+ android::RegisterMethodsOrDie(env, "android/graphics/Mesh", gMeshMethods, NELEM(gMeshMethods));
+ return 0;
+}
+
+} // namespace android \ No newline at end of file
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index db7639029187..8c7b9a4b5e94 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -526,6 +526,11 @@ static jlong android_view_RenderNode_getUniqueId(CRITICAL_JNI_PARAMS_COMMA jlong
return reinterpret_cast<RenderNode*>(renderNodePtr)->uniqueId();
}
+static void android_view_RenderNode_setIsTextureView(
+ CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr) {
+ reinterpret_cast<RenderNode*>(renderNodePtr)->setIsTextureView();
+}
+
// ----------------------------------------------------------------------------
// RenderProperties - Animations
// ----------------------------------------------------------------------------
@@ -584,13 +589,15 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
uirenderer::Rect bounds(props.getWidth(), props.getHeight());
bool useStretchShader =
Properties::getStretchEffectBehavior() != StretchEffectBehavior::UniformScale;
- if (useStretchShader && info.stretchEffectCount) {
+ // Compute the transform bounds first before calculating the stretch
+ transform.mapRect(bounds);
+
+ bool hasStretch = useStretchShader && info.stretchEffectCount;
+ if (hasStretch) {
handleStretchEffect(info, bounds);
}
- transform.mapRect(bounds);
-
- if (CC_LIKELY(transform.isPureTranslate())) {
+ if (CC_LIKELY(transform.isPureTranslate()) && !hasStretch) {
// snap/round the computed bounds, so they match the rounding behavior
// of the clear done in SurfaceView#draw().
bounds.snapGeometryToPixelBoundaries(false);
@@ -605,15 +612,25 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
}
mPreviousPosition = bounds;
+ ATRACE_NAME("Update SurfaceView position");
+
#ifdef __ANDROID__ // Layoutlib does not support CanvasContext
- incStrong(0);
- auto functor = std::bind(
- std::mem_fn(&PositionListenerTrampoline::doUpdatePositionAsync), this,
- (jlong) info.canvasContext.getFrameNumber(),
- (jint) bounds.left, (jint) bounds.top,
- (jint) bounds.right, (jint) bounds.bottom);
-
- info.canvasContext.enqueueFrameWork(std::move(functor));
+ JNIEnv* env = jnienv();
+ // Update the new position synchronously. We cannot defer this to
+ // a worker pool to process asynchronously because the UI thread
+ // may be unblocked by the time a worker thread can process this,
+ // In particular if the app removes a view from the view tree before
+ // this callback is dispatched, then we lose the position
+ // information for this frame.
+ jboolean keepListening = env->CallStaticBooleanMethod(
+ gPositionListener.clazz, gPositionListener.callPositionChanged, mListener,
+ static_cast<jlong>(info.canvasContext.getFrameNumber()),
+ static_cast<jint>(bounds.left), static_cast<jint>(bounds.top),
+ static_cast<jint>(bounds.right), static_cast<jint>(bounds.bottom));
+ if (!keepListening) {
+ env->DeleteGlobalRef(mListener);
+ mListener = nullptr;
+ }
#endif
}
@@ -628,7 +645,14 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
ATRACE_NAME("SurfaceView position lost");
JNIEnv* env = jnienv();
#ifdef __ANDROID__ // Layoutlib does not support CanvasContext
- // TODO: Remember why this is synchronous and then make a comment
+ // Update the lost position synchronously. We cannot defer this to
+ // a worker pool to process asynchronously because the UI thread
+ // may be unblocked by the time a worker thread can process this,
+ // In particular if a view's rendernode is readded to the scene
+ // before this callback is dispatched, then we report that we lost
+ // position information on the wrong frame, which can be problematic
+ // for views like SurfaceView which rely on RenderNode callbacks
+ // for driving visibility.
jboolean keepListening = env->CallStaticBooleanMethod(
gPositionListener.clazz, gPositionListener.callPositionLost, mListener,
info ? info->canvasContext.getFrameNumber() : 0);
@@ -648,45 +672,42 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
return env;
}
- void stretchTargetBounds(const StretchEffect& stretchEffect,
- float width, float height,
- const SkRect& childRelativeBounds,
- uirenderer::Rect& bounds) {
- float normalizedLeft = childRelativeBounds.left() / width;
- float normalizedTop = childRelativeBounds.top() / height;
- float normalizedRight = childRelativeBounds.right() / width;
- float normalizedBottom = childRelativeBounds.bottom() / height;
- float reverseLeft = width *
- (stretchEffect.computeStretchedPositionX(normalizedLeft) -
- normalizedLeft);
- float reverseTop = height *
- (stretchEffect.computeStretchedPositionY(normalizedTop) -
- normalizedTop);
- float reverseRight = width *
- (stretchEffect.computeStretchedPositionX(normalizedRight) -
- normalizedLeft);
- float reverseBottom = height *
- (stretchEffect.computeStretchedPositionY(normalizedBottom) -
- normalizedTop);
- bounds.left = reverseLeft;
- bounds.top = reverseTop;
- bounds.right = reverseRight;
- bounds.bottom = reverseBottom;
- }
-
void handleStretchEffect(const TreeInfo& info, uirenderer::Rect& targetBounds) {
// Search up to find the nearest stretcheffect parent
const DamageAccumulator::StretchResult result =
info.damageAccumulator->findNearestStretchEffect();
const StretchEffect* effect = result.stretchEffect;
- if (!effect) {
+ if (effect) {
+ // Compute the number of pixels that the stretching container
+ // scales by.
+ // Then compute the scale factor that the child would need
+ // to scale in order to occupy the same pixel bounds.
+ auto& parentBounds = result.parentBounds;
+ auto parentWidth = parentBounds.width();
+ auto parentHeight = parentBounds.height();
+ auto& stretchDirection = effect->getStretchDirection();
+ auto stretchX = stretchDirection.x();
+ auto stretchY = stretchDirection.y();
+ auto stretchXPixels = parentWidth * std::abs(stretchX);
+ auto stretchYPixels = parentHeight * std::abs(stretchY);
+ SkMatrix stretchMatrix;
+
+ auto childScaleX = 1 + (stretchXPixels / targetBounds.getWidth());
+ auto childScaleY = 1 + (stretchYPixels / targetBounds.getHeight());
+ auto pivotX = stretchX > 0 ? targetBounds.left : targetBounds.right;
+ auto pivotY = stretchY > 0 ? targetBounds.top : targetBounds.bottom;
+ stretchMatrix.setScale(childScaleX, childScaleY, pivotX, pivotY);
+ SkRect rect = SkRect::MakeLTRB(targetBounds.left, targetBounds.top,
+ targetBounds.right, targetBounds.bottom);
+ SkRect dst = stretchMatrix.mapRect(rect);
+ targetBounds.left = dst.left();
+ targetBounds.top = dst.top();
+ targetBounds.right = dst.right();
+ targetBounds.bottom = dst.bottom();
+ } else {
return;
}
- const auto& childRelativeBounds = result.childRelativeBounds;
- stretchTargetBounds(*effect, result.width, result.height,
- childRelativeBounds,targetBounds);
-
if (Properties::getStretchEffectBehavior() ==
StretchEffectBehavior::Shader) {
JNIEnv* env = jnienv();
@@ -697,9 +718,8 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
gPositionListener.clazz, gPositionListener.callApplyStretch, mListener,
info.canvasContext.getFrameNumber(), result.width, result.height,
stretchDirection.fX, stretchDirection.fY, effect->maxStretchAmountX,
- effect->maxStretchAmountY, childRelativeBounds.left(),
- childRelativeBounds.top(), childRelativeBounds.right(),
- childRelativeBounds.bottom());
+ effect->maxStretchAmountY, targetBounds.left, targetBounds.top,
+ targetBounds.right, targetBounds.bottom);
if (!keepListening) {
env->DeleteGlobalRef(mListener);
mListener = nullptr;
@@ -708,23 +728,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
}
}
- void doUpdatePositionAsync(jlong frameNumber, jint left, jint top,
- jint right, jint bottom) {
- ATRACE_NAME("Update SurfaceView position");
-
- JNIEnv* env = jnienv();
- jboolean keepListening = env->CallStaticBooleanMethod(
- gPositionListener.clazz, gPositionListener.callPositionChanged, mListener,
- frameNumber, left, top, right, bottom);
- if (!keepListening) {
- env->DeleteGlobalRef(mListener);
- mListener = nullptr;
- }
-
- // We need to release ourselves here
- decStrong(0);
- }
-
JavaVM* mVm;
jobject mListener;
uirenderer::Rect mPreviousPosition;
@@ -846,6 +849,7 @@ static const JNINativeMethod gMethods[] = {
{"nSetAllowForceDark", "(JZ)Z", (void*)android_view_RenderNode_setAllowForceDark},
{"nGetAllowForceDark", "(J)Z", (void*)android_view_RenderNode_getAllowForceDark},
{"nGetUniqueId", "(J)J", (void*)android_view_RenderNode_getUniqueId},
+ {"nSetIsTextureView", "(J)V", (void*)android_view_RenderNode_setIsTextureView},
};
int register_android_view_RenderNode(JNIEnv* env) {
diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp
index d78df3dca3b1..1af60b2f5fae 100644
--- a/libs/hwui/jni/fonts/Font.cpp
+++ b/libs/hwui/jni/fonts/Font.cpp
@@ -22,7 +22,10 @@
#include "SkFont.h"
#include "SkFontMetrics.h"
#include "SkFontMgr.h"
+#include "SkRect.h"
#include "SkRefCnt.h"
+#include "SkScalar.h"
+#include "SkStream.h"
#include "SkTypeface.h"
#include "GraphicsJNI.h"
#include <nativehelper/ScopedUtfChars.h>
@@ -226,7 +229,7 @@ static jlong Font_getReleaseNativeFontFunc(CRITICAL_JNI_PARAMS) {
static jstring Font_getFontPath(JNIEnv* env, jobject, jlong fontPtr) {
FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr);
minikin::BufferReader reader = font->font->typefaceMetadataReader();
- if (reader.data() != nullptr) {
+ if (reader.current() != nullptr) {
std::string path = std::string(reader.readString());
if (path.empty()) {
return nullptr;
@@ -268,7 +271,7 @@ static jint Font_getPackedStyle(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) {
static jint Font_getIndex(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) {
FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr);
minikin::BufferReader reader = font->font->typefaceMetadataReader();
- if (reader.data() != nullptr) {
+ if (reader.current() != nullptr) {
reader.skipString(); // fontPath
return reader.read<int>();
} else {
@@ -281,7 +284,7 @@ static jint Font_getIndex(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) {
static jint Font_getAxisCount(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr) {
FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr);
minikin::BufferReader reader = font->font->typefaceMetadataReader();
- if (reader.data() != nullptr) {
+ if (reader.current() != nullptr) {
reader.skipString(); // fontPath
reader.skip<int>(); // fontIndex
return reader.readArray<minikin::FontVariation>().second;
@@ -296,7 +299,7 @@ static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr, jint inde
FontWrapper* font = reinterpret_cast<FontWrapper*>(fontPtr);
minikin::BufferReader reader = font->font->typefaceMetadataReader();
minikin::FontVariation var;
- if (reader.data() != nullptr) {
+ if (reader.current() != nullptr) {
reader.skipString(); // fontPath
reader.skip<int>(); // fontIndex
var = reader.readArray<minikin::FontVariation>().first[index];
diff --git a/libs/hwui/jni/fonts/FontFamily.cpp b/libs/hwui/jni/fonts/FontFamily.cpp
index ac1c05ed6afe..ee158ee4e316 100644
--- a/libs/hwui/jni/fonts/FontFamily.cpp
+++ b/libs/hwui/jni/fonts/FontFamily.cpp
@@ -59,7 +59,8 @@ static void FontFamily_Builder_addFont(CRITICAL_JNI_PARAMS_COMMA jlong builderPt
// Regular JNI
static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr,
- jstring langTags, jint variant, jboolean isCustomFallback) {
+ jstring langTags, jint variant, jboolean isCustomFallback,
+ jboolean isDefaultFallback) {
std::unique_ptr<NativeFamilyBuilder> builder(toBuilder(builderPtr));
uint32_t localeId;
if (langTags == nullptr) {
@@ -68,9 +69,9 @@ static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderP
ScopedUtfChars str(env, langTags);
localeId = minikin::registerLocaleList(str.c_str());
}
- std::shared_ptr<minikin::FontFamily> family = std::make_shared<minikin::FontFamily>(
+ std::shared_ptr<minikin::FontFamily> family = minikin::FontFamily::create(
localeId, static_cast<minikin::FamilyVariant>(variant), std::move(builder->fonts),
- isCustomFallback);
+ isCustomFallback, isDefaultFallback);
if (family->getCoverage().length() == 0) {
// No coverage means minikin rejected given font for some reasons.
jniThrowException(env, "java/lang/IllegalArgumentException",
@@ -118,10 +119,10 @@ static jlong FontFamily_getFont(CRITICAL_JNI_PARAMS_COMMA jlong familyPtr, jint
///////////////////////////////////////////////////////////////////////////////
static const JNINativeMethod gFontFamilyBuilderMethods[] = {
- { "nInitBuilder", "()J", (void*) FontFamily_Builder_initBuilder },
- { "nAddFont", "(JJ)V", (void*) FontFamily_Builder_addFont },
- { "nBuild", "(JLjava/lang/String;IZ)J", (void*) FontFamily_Builder_build },
- { "nGetReleaseNativeFamily", "()J", (void*) FontFamily_Builder_GetReleaseFunc },
+ {"nInitBuilder", "()J", (void*)FontFamily_Builder_initBuilder},
+ {"nAddFont", "(JJ)V", (void*)FontFamily_Builder_addFont},
+ {"nBuild", "(JLjava/lang/String;IZZ)J", (void*)FontFamily_Builder_build},
+ {"nGetReleaseNativeFamily", "()J", (void*)FontFamily_Builder_GetReleaseFunc},
};
static const JNINativeMethod gFontFamilyMethods[] = {
diff --git a/libs/hwui/jni/text/GraphemeBreak.cpp b/libs/hwui/jni/text/GraphemeBreak.cpp
new file mode 100644
index 000000000000..55f03bd9f7b1
--- /dev/null
+++ b/libs/hwui/jni/text/GraphemeBreak.cpp
@@ -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.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "GraphemeBreaker"
+
+#include <minikin/GraphemeBreak.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+
+#include "GraphicsJNI.h"
+
+namespace android {
+
+static void nIsGraphemeBreak(JNIEnv* env, jclass, jfloatArray advances, jcharArray text, jint start,
+ jint end, jbooleanArray isGraphemeBreak) {
+ if (start > end || env->GetArrayLength(advances) < end ||
+ env->GetArrayLength(isGraphemeBreak) < end - start) {
+ doThrowAIOOBE(env);
+ }
+
+ if (start == end) {
+ return;
+ }
+
+ ScopedFloatArrayRO advancesArray(env, advances);
+ ScopedCharArrayRO textArray(env, text);
+ ScopedBooleanArrayRW isGraphemeBreakArray(env, isGraphemeBreak);
+
+ size_t count = end - start;
+ for (size_t offset = 0; offset < count; ++offset) {
+ bool isBreak = minikin::GraphemeBreak::isGraphemeBreak(advancesArray.get(), textArray.get(),
+ start, end, start + offset);
+ isGraphemeBreakArray[offset] = isBreak ? JNI_TRUE : JNI_FALSE;
+ }
+}
+
+static const JNINativeMethod gMethods[] = {
+ {"nIsGraphemeBreak",
+ "("
+ "[F" // advances
+ "[C" // text
+ "I" // start
+ "I" // end
+ "[Z" // isGraphemeBreak
+ ")V",
+ (void*)nIsGraphemeBreak},
+};
+
+int register_android_graphics_text_GraphemeBreak(JNIEnv* env) {
+ return RegisterMethodsOrDie(env, "android/graphics/text/GraphemeBreak", gMethods,
+ NELEM(gMethods));
+}
+
+} // namespace android
diff --git a/libs/hwui/pipeline/skia/DumpOpsCanvas.h b/libs/hwui/pipeline/skia/DumpOpsCanvas.h
index 3f89c0712407..6a052dbb7cea 100644
--- a/libs/hwui/pipeline/skia/DumpOpsCanvas.h
+++ b/libs/hwui/pipeline/skia/DumpOpsCanvas.h
@@ -19,6 +19,8 @@
#include "RenderNode.h"
#include "SkiaDisplayList.h"
+class SkRRect;
+
namespace android {
namespace uirenderer {
namespace skiapipeline {
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
index dc72aead4873..8d5967bbd461 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
@@ -24,7 +24,10 @@
#include "SkClipStack.h"
#include "SkRect.h"
#include "SkM44.h"
+#include "include/gpu/GpuTypes.h" // from Skia
#include "utils/GLUtils.h"
+#include <effects/GainmapRenderer.h>
+#include "renderthread/CanvasContext.h"
namespace android {
namespace uirenderer {
@@ -92,7 +95,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
SkImageInfo surfaceInfo =
canvas->imageInfo().makeWH(clipBounds.width(), clipBounds.height());
tmpSurface =
- SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, surfaceInfo);
+ SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes, surfaceInfo);
tmpSurface->getCanvas()->clear(SK_ColorTRANSPARENT);
GrGLFramebufferInfo fboInfo;
@@ -128,6 +131,9 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
info.height = fboSize.height();
mat4.getColMajor(&info.transform[0]);
info.color_space_ptr = canvas->imageInfo().colorSpace();
+ info.currentHdrSdrRatio = getTargetHdrSdrRatio(info.color_space_ptr);
+ info.fboColorType = canvas->imageInfo().colorType();
+ info.shouldDither = renderthread::CanvasContext::shouldDither();
// ensure that the framebuffer that the webview will render into is bound before we clear
// the stencil and/or draw the functor.
diff --git a/libs/hwui/pipeline/skia/HolePunch.h b/libs/hwui/pipeline/skia/HolePunch.h
index 92c6f7721a08..d0e1ca35049a 100644
--- a/libs/hwui/pipeline/skia/HolePunch.h
+++ b/libs/hwui/pipeline/skia/HolePunch.h
@@ -17,7 +17,6 @@
#pragma once
#include <string>
-#include "SkRRect.h"
namespace android {
namespace uirenderer {
diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp
index 3ba540921f64..99f54c19d2e5 100644
--- a/libs/hwui/pipeline/skia/LayerDrawable.cpp
+++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp
@@ -25,6 +25,7 @@
#include "SkColorFilter.h"
#include "SkRuntimeEffect.h"
#include "SkSurface.h"
+#include "Tonemapper.h"
#include "gl/GrGLTypes.h"
#include "math/mat4.h"
#include "system/graphics-base-v1.0.h"
@@ -76,37 +77,6 @@ static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, cons
isIntegerAligned(dstDevRect.y()));
}
-static sk_sp<SkShader> createLinearEffectShader(sk_sp<SkShader> shader,
- const shaders::LinearEffect& linearEffect,
- float maxDisplayLuminance,
- float currentDisplayLuminanceNits,
- float maxLuminance) {
- auto shaderString = SkString(shaders::buildLinearEffectSkSL(linearEffect));
- auto [runtimeEffect, error] = SkRuntimeEffect::MakeForShader(std::move(shaderString));
- if (!runtimeEffect) {
- LOG_ALWAYS_FATAL("LinearColorFilter construction error: %s", error.c_str());
- }
-
- SkRuntimeShaderBuilder effectBuilder(std::move(runtimeEffect));
-
- effectBuilder.child("child") = std::move(shader);
-
- const auto uniforms = shaders::buildLinearEffectUniforms(
- linearEffect, mat4(), maxDisplayLuminance, currentDisplayLuminanceNits, maxLuminance);
-
- for (const auto& uniform : uniforms) {
- effectBuilder.uniform(uniform.name.c_str()).set(uniform.value.data(), uniform.value.size());
- }
-
- return effectBuilder.makeShader();
-}
-
-static bool isHdrDataspace(ui::Dataspace dataspace) {
- const auto transfer = dataspace & HAL_DATASPACE_TRANSFER_MASK;
-
- return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG;
-}
-
static void adjustCropForYUV(uint32_t format, int bufferWidth, int bufferHeight, SkRect* cropRect) {
// Chroma channels of YUV420 images are subsampled we may need to shrink the crop region by
// a whole texel on each side. Since skia still adds its own 0.5 inset, we apply an
@@ -215,31 +185,10 @@ bool LayerDrawable::DrawLayer(GrRecordingContext* context,
sampling = SkSamplingOptions(SkFilterMode::kLinear);
}
- const auto sourceDataspace = static_cast<ui::Dataspace>(
- ColorSpaceToADataSpace(layerImage->colorSpace(), layerImage->colorType()));
- const SkImageInfo& imageInfo = canvas->imageInfo();
- const auto destinationDataspace = static_cast<ui::Dataspace>(
- ColorSpaceToADataSpace(imageInfo.colorSpace(), imageInfo.colorType()));
-
- if (isHdrDataspace(sourceDataspace) || isHdrDataspace(destinationDataspace)) {
- const auto effect = shaders::LinearEffect{
- .inputDataspace = sourceDataspace,
- .outputDataspace = destinationDataspace,
- .undoPremultipliedAlpha = layerImage->alphaType() == kPremul_SkAlphaType,
- .fakeInputDataspace = destinationDataspace};
- auto shader = layerImage->makeShader(sampling,
- SkMatrix::RectToRect(skiaSrcRect, skiaDestRect));
- constexpr float kMaxDisplayBrightess = 1000.f;
- constexpr float kCurrentDisplayBrightness = 500.f;
- shader = createLinearEffectShader(std::move(shader), effect, kMaxDisplayBrightess,
- kCurrentDisplayBrightness,
- layer->getMaxLuminanceNits());
- paint.setShader(shader);
- canvas->drawRect(skiaDestRect, paint);
- } else {
- canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint,
- constraint);
- }
+ tonemapPaint(layerImage->imageInfo(), canvas->imageInfo(), layer->getMaxLuminanceNits(),
+ paint);
+ canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint,
+ constraint);
canvas->restore();
// restore the original matrix
diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
index 9e17b9e6d985..da4f66d45a70 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -15,7 +15,11 @@
*/
#include "RenderNodeDrawable.h"
+#include <SkPaint.h>
#include <SkPaintFilterCanvas.h>
+#include <SkPoint.h>
+#include <SkRRect.h>
+#include <SkRect.h>
#include <gui/TraceUtils.h>
#include "RenderNode.h"
#include "SkiaDisplayList.h"
@@ -197,6 +201,7 @@ protected:
paint.setAlpha((uint8_t)paint.getAlpha() * mAlpha);
return true;
}
+
void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override {
// We unroll the drawable using "this" canvas, so that draw calls contained inside will
// get their alpha applied. The default SkPaintFilterCanvas::onDrawDrawable does not unroll.
@@ -288,8 +293,10 @@ void RenderNodeDrawable::drawContent(SkCanvas* canvas) const {
// with the same canvas transformation + clip into the target
// canvas then draw the layer on top
if (renderNode->hasHolePunches()) {
- TransformCanvas transformCanvas(canvas, SkBlendMode::kClear);
+ canvas->save();
+ TransformCanvas transformCanvas(canvas, SkBlendMode::kDstOut);
displayList->draw(&transformCanvas);
+ canvas->restore();
}
canvas->drawImageRect(snapshotImage, SkRect::Make(srcBounds),
SkRect::Make(dstBounds), sampling, &paint,
diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.h b/libs/hwui/pipeline/skia/RenderNodeDrawable.h
index 6c390c3fce24..c7582e734009 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.h
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.h
@@ -18,6 +18,7 @@
#include "SkiaUtils.h"
+#include <SkBlendMode.h>
#include <SkCanvas.h>
#include <SkDrawable.h>
#include <SkMatrix.h>
diff --git a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp
index 7cfccb56382c..11977bd54c2c 100644
--- a/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp
+++ b/libs/hwui/pipeline/skia/ReorderBarrierDrawables.cpp
@@ -19,8 +19,15 @@
#include "SkiaDisplayList.h"
#include "LightingInfo.h"
+#include <SkColor.h>
+#include <SkMatrix.h>
+#include <SkPath.h>
#include <SkPathOps.h>
+#include <SkPoint3.h>
+#include <SkRect.h>
+#include <SkScalar.h>
#include <SkShadowUtils.h>
+#include <include/private/SkShadowFlags.h>
namespace android {
namespace uirenderer {
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index 2ab7a58556a2..00919dc3f22a 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -16,6 +16,7 @@
#include "ShaderCache.h"
#include <GrDirectContext.h>
+#include <SkData.h>
#include <gui/TraceUtils.h>
#include <log/log.h>
#include <openssl/sha.h>
@@ -32,7 +33,8 @@ namespace skiapipeline {
// Cache size limits.
static const size_t maxKeySize = 1024;
static const size_t maxValueSize = 2 * 1024 * 1024;
-static const size_t maxTotalSize = 1024 * 1024;
+static const size_t maxTotalSize = 4 * 1024 * 1024;
+static_assert(maxKeySize + maxValueSize < maxTotalSize);
ShaderCache::ShaderCache() {
// There is an "incomplete FileBlobCache type" compilation error, if ctor is moved to header.
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index 4e3eb816da29..f5506d60f811 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -17,12 +17,15 @@
#pragma once
#include <GrContextOptions.h>
+#include <SkRefCnt.h>
#include <cutils/compiler.h>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
+class SkData;
+
namespace android {
class BlobCache;
@@ -45,7 +48,7 @@ public:
* and puts the ShaderCache into an initialized state, such that it is
* able to insert and retrieve entries from the cache. If identity is
* non-null and validation fails, the cache is initialized but contains
- * no data. If size is less than zero, the cache is initilaized but
+ * no data. If size is less than zero, the cache is initialized but
* contains no data.
*
* This should be called when HWUI pipeline is initialized. When not in
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
index fcfc4f82abed..af2d3b34bac7 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
@@ -23,6 +23,7 @@
#else
#include "DamageAccumulator.h"
#endif
+#include "TreeInfo.h"
#include "VectorDrawable.h"
#ifdef __ANDROID__
#include "renderthread/CanvasContext.h"
@@ -102,6 +103,12 @@ bool SkiaDisplayList::prepareListAndChildren(
info.prepareTextures = false;
info.canvasContext.unpinImages();
}
+
+ auto grContext = info.canvasContext.getGrContext();
+ for (auto mesh : mMeshes) {
+ mesh->updateSkMesh(grContext);
+ }
+
#endif
bool hasBackwardProjectedNodesHere = false;
@@ -168,6 +175,7 @@ void SkiaDisplayList::reset() {
mDisplayList.reset();
+ mMeshes.clear();
mMutableImages.clear();
mVectorDrawables.clear();
mAnimatedImages.clear();
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h
index 2a677344b7b2..7af31a4dc4c6 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.h
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h
@@ -18,6 +18,7 @@
#include <deque>
+#include "Mesh.h"
#include "RecordingCanvas.h"
#include "RenderNodeDrawable.h"
#include "TreeInfo.h"
@@ -167,6 +168,7 @@ public:
std::deque<RenderNodeDrawable> mChildNodes;
std::deque<FunctorDrawable*> mChildFunctors;
std::vector<SkImage*> mMutableImages;
+ std::vector<const Mesh*> mMeshes;
private:
std::vector<Pair<VectorDrawableRoot*, SkMatrix>> mVectorDrawables;
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index b5c1b490182f..cf31173d266e 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -57,7 +57,9 @@ MakeCurrentResult SkiaOpenGLPipeline::makeCurrent() {
// In case the surface was destroyed (e.g. a previous trimMemory call) we
// need to recreate it here.
- if (!isSurfaceReady() && mNativeWindow) {
+ if (mHardwareBuffer) {
+ mRenderThread.requireGlContext();
+ } else if (!isSurfaceReady() && mNativeWindow) {
setSurface(mNativeWindow.get(), mSwapBehavior);
}
@@ -112,8 +114,9 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw(
const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
- const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) {
- if (!isCapturingSkp()) {
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams) {
+ if (!isCapturingSkp() && !mHardwareBuffer) {
mEglManager.damageFrame(frame, dirty);
}
@@ -137,22 +140,35 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw(
GrBackendRenderTarget backendRT(frame.width(), frame.height(), 0, STENCIL_BUFFER_SIZE, fboInfo);
- SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+ SkSurfaceProps props(mColorMode == ColorMode::Default ? 0 : SkSurfaceProps::kAlwaysDither_Flag,
+ kUnknown_SkPixelGeometry);
SkASSERT(mRenderThread.getGrContext() != nullptr);
- sk_sp<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget(
- mRenderThread.getGrContext(), backendRT, this->getSurfaceOrigin(), colorType,
- mSurfaceColorSpace, &props));
+ sk_sp<SkSurface> surface;
+ SkMatrix preTransform;
+ if (mHardwareBuffer) {
+ surface = getBufferSkSurface(bufferParams);
+ preTransform = bufferParams.getTransform();
+ } else {
+ surface = SkSurface::MakeFromBackendRenderTarget(mRenderThread.getGrContext(), backendRT,
+ getSurfaceOrigin(), colorType,
+ mSurfaceColorSpace, &props);
+ preTransform = SkMatrix::I();
+ }
- LightingInfo::updateLighting(lightGeometry, lightInfo);
+ SkPoint lightCenter = preTransform.mapXY(lightGeometry.center.x, lightGeometry.center.y);
+ LightGeometry localGeometry = lightGeometry;
+ localGeometry.center.x = lightCenter.fX;
+ localGeometry.center.y = lightCenter.fY;
+ LightingInfo::updateLighting(localGeometry, lightInfo);
renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, surface,
- SkMatrix::I());
+ preTransform);
// Draw visual debugging features
if (CC_UNLIKELY(Properties::showDirtyRegions ||
ProfileType::None != Properties::getProfileType())) {
SkCanvas* profileCanvas = surface->getCanvas();
- SkiaProfileRenderer profileRenderer(profileCanvas);
+ SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height());
profiler->draw(profileRenderer);
}
@@ -178,6 +194,10 @@ bool SkiaOpenGLPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect
// metrics the frame was swapped at this point
currentFrameInfo->markSwapBuffers();
+ if (mHardwareBuffer) {
+ return false;
+ }
+
*requireSwap = drew || mEglManager.damageRequiresSwap();
if (*requireSwap && (CC_UNLIKELY(!mEglManager.swapBuffers(frame, screenDirty)))) {
@@ -233,6 +253,26 @@ bool SkiaOpenGLPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBeh
return false;
}
+[[nodiscard]] android::base::unique_fd SkiaOpenGLPipeline::flush() {
+ int fence = -1;
+ EGLSyncKHR sync = EGL_NO_SYNC_KHR;
+ mEglManager.createReleaseFence(true, &sync, &fence);
+ // If a sync object is returned here then the device does not support native
+ // fences, we block on the returned sync and return -1 as a file descriptor
+ if (sync != EGL_NO_SYNC_KHR) {
+ EGLDisplay display = mEglManager.eglDisplay();
+ EGLint result = eglClientWaitSyncKHR(display, sync, 0, 1000000000);
+ if (result == EGL_FALSE) {
+ ALOGE("EglManager::createReleaseFence: error waiting for previous fence: %#x",
+ eglGetError());
+ } else if (result == EGL_TIMEOUT_EXPIRED_KHR) {
+ ALOGE("EglManager::createReleaseFence: timeout waiting for previous fence");
+ }
+ eglDestroySyncKHR(display, sync);
+ }
+ return android::base::unique_fd(fence);
+}
+
bool SkiaOpenGLPipeline::isSurfaceReady() {
return CC_UNLIKELY(mEglSurface != EGL_NO_SURFACE);
}
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
index fe414201094b..f0461bef170c 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
@@ -21,6 +21,7 @@
#include "SkiaPipeline.h"
#include "renderstate/RenderState.h"
+#include "renderthread/HardwareBufferRenderParams.h"
namespace android {
@@ -36,19 +37,18 @@ public:
renderthread::MakeCurrentResult makeCurrent() override;
renderthread::Frame getFrame() override;
- renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame,
- const SkRect& screenDirty, const SkRect& dirty,
- const LightGeometry& lightGeometry,
- LayerUpdateQueue* layerUpdateQueue,
- const Rect& contentDrawBounds, bool opaque,
- const LightInfo& lightInfo,
- const std::vector<sp<RenderNode> >& renderNodes,
- FrameInfoVisualizer* profiler) override;
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams) override;
GrSurfaceOrigin getSurfaceOrigin() override { return kBottomLeft_GrSurfaceOrigin; }
bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) override;
DeferredLayerUpdater* createTextureLayer() override;
bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override;
+ [[nodiscard]] android::base::unique_fd flush() override;
void onStop() override;
bool isSurfaceReady() override;
bool isContextReady() override;
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index c9bb73e132de..cb23bcc166c8 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -16,24 +16,35 @@
#include "SkiaPipeline.h"
+#include <SkCanvas.h>
+#include <SkColor.h>
+#include <SkColorSpace.h>
+#include <SkData.h>
+#include <SkImage.h>
#include <SkImageEncoder.h>
#include <SkImageInfo.h>
#include <SkImagePriv.h>
+#include <SkMatrix.h>
#include <SkMultiPictureDocument.h>
#include <SkOverdrawCanvas.h>
#include <SkOverdrawColorFilter.h>
#include <SkPicture.h>
#include <SkPictureRecorder.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
#include <SkSerialProcs.h>
+#include <SkStream.h>
+#include <SkString.h>
#include <SkTypeface.h>
#include <android-base/properties.h>
+#include <gui/TraceUtils.h>
#include <unistd.h>
#include <sstream>
-#include <gui/TraceUtils.h>
#include "LightingInfo.h"
#include "VectorDrawable.h"
+#include "include/gpu/GpuTypes.h" // from Skia
#include "thread/CommonPool.h"
#include "tools/SkSharingProc.h"
#include "utils/Color.h"
@@ -177,7 +188,7 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator
SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
SkASSERT(mRenderThread.getGrContext() != nullptr);
node->setLayerSurface(SkSurface::MakeRenderTarget(mRenderThread.getGrContext(),
- SkBudgeted::kYes, info, 0,
+ skgpu::Budgeted::kYes, info, 0,
this->getSurfaceOrigin(), &props));
if (node->getLayerSurface()) {
// update the transform in window of the layer to reset its origin wrt light source
@@ -488,8 +499,7 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip,
}
canvas->concat(preTransform);
- // STOPSHIP: Revert, temporary workaround to clear always F16 frame buffer for b/74976293
- if (!opaque || getSurfaceColorType() == kRGBA_F16_SkColorType) {
+ if (!opaque) {
canvas->clear(SK_ColorTRANSPARENT);
}
@@ -594,6 +604,31 @@ void SkiaPipeline::dumpResourceCacheUsage() const {
ALOGD("%s", log.c_str());
}
+void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (mHardwareBuffer) {
+ AHardwareBuffer_release(mHardwareBuffer);
+ mHardwareBuffer = nullptr;
+ }
+
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ mHardwareBuffer = buffer;
+ }
+}
+
+sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams) {
+ auto bufferColorSpace = bufferParams.getColorSpace();
+ if (mBufferSurface == nullptr || mBufferColorSpace == nullptr ||
+ !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) {
+ mBufferSurface = SkSurface::MakeFromAHardwareBuffer(
+ mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin,
+ bufferColorSpace, nullptr, true);
+ mBufferColorSpace = bufferColorSpace;
+ }
+ return mBufferSurface;
+}
+
void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
mColorMode = colorMode;
switch (colorMode) {
@@ -606,12 +641,14 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace();
break;
case ColorMode::Hdr:
- mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType;
- mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020);
+ mSurfaceColorType = SkColorType::kN32_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(
+ GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
break;
case ColorMode::Hdr10:
mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType;
- mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020);
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(
+ GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
break;
case ColorMode::A8:
mSurfaceColorType = SkColorType::kAlpha_8_SkColorType;
@@ -620,6 +657,16 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
}
}
+void SkiaPipeline::setTargetSdrHdrRatio(float ratio) {
+ if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) {
+ mTargetSdrHdrRatio = ratio;
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio),
+ SkNamedGamut::kDisplayP3);
+ } else {
+ mTargetSdrHdrRatio = 1.f;
+ }
+}
+
// Overdraw debugging
// These colors should be kept in sync with Caches::getOverdrawColor() with a few differences.
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index bc8a5659dd83..befee8989383 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -16,14 +16,18 @@
#pragma once
-#include <SkSurface.h>
+#include <SkColorSpace.h>
#include <SkDocument.h>
#include <SkMultiPictureDocument.h>
+#include <SkSurface.h>
+
#include "Lighting.h"
#include "hwui/AnimatedImageDrawable.h"
#include "renderthread/CanvasContext.h"
+#include "renderthread/HardwareBufferRenderParams.h"
#include "renderthread/IRenderPipeline.h"
+class SkFILEWStream;
class SkPictureRecorder;
struct SkSharingSerialContext;
@@ -71,14 +75,26 @@ public:
mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None;
}
+ virtual void setHardwareBuffer(AHardwareBuffer* buffer) override;
+ bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; }
+
+ void setTargetSdrHdrRatio(float ratio) override;
+
protected:
+ sk_sp<SkSurface> getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams);
void dumpResourceCacheUsage() const;
renderthread::RenderThread& mRenderThread;
+ AHardwareBuffer* mHardwareBuffer = nullptr;
+ sk_sp<SkSurface> mBufferSurface = nullptr;
+ sk_sp<SkColorSpace> mBufferColorSpace = nullptr;
+
ColorMode mColorMode = ColorMode::Default;
SkColorType mSurfaceColorType;
sk_sp<SkColorSpace> mSurfaceColorSpace;
+ float mTargetSdrHdrRatio = 1.f;
bool isCapturingSkp() const { return mCaptureMode != CaptureMode::None; }
diff --git a/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp b/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp
index 492c39f1288c..81cfc5d93419 100644
--- a/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp
+++ b/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp
@@ -33,13 +33,5 @@ void SkiaProfileRenderer::drawRects(const float* rects, int count, const SkPaint
}
}
-uint32_t SkiaProfileRenderer::getViewportWidth() {
- return mCanvas->imageInfo().width();
-}
-
-uint32_t SkiaProfileRenderer::getViewportHeight() {
- return mCanvas->imageInfo().height();
-}
-
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaProfileRenderer.h b/libs/hwui/pipeline/skia/SkiaProfileRenderer.h
index dc8420f4e01b..96d2a5e58139 100644
--- a/libs/hwui/pipeline/skia/SkiaProfileRenderer.h
+++ b/libs/hwui/pipeline/skia/SkiaProfileRenderer.h
@@ -23,18 +23,21 @@ namespace uirenderer {
class SkiaProfileRenderer : public IProfileRenderer {
public:
- explicit SkiaProfileRenderer(SkCanvas* canvas) : mCanvas(canvas) {}
+ explicit SkiaProfileRenderer(SkCanvas* canvas, uint32_t width, uint32_t height)
+ : mCanvas(canvas), mWidth(width), mHeight(height) {}
void drawRect(float left, float top, float right, float bottom, const SkPaint& paint) override;
void drawRects(const float* rects, int count, const SkPaint& paint) override;
- uint32_t getViewportWidth() override;
- uint32_t getViewportHeight() override;
+ uint32_t getViewportWidth() override { return mWidth; }
+ uint32_t getViewportHeight() override { return mHeight; }
virtual ~SkiaProfileRenderer() {}
private:
// Does not have ownership.
SkCanvas* mCanvas;
+ uint32_t mWidth;
+ uint32_t mHeight;
};
} /* namespace uirenderer */
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 9c51e628e04a..3ca7eeb37a89 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -16,7 +16,19 @@
#include "SkiaRecordingCanvas.h"
#include "hwui/Paint.h"
+#include <SkBlendMode.h>
+#include <SkData.h>
+#include <SkDrawable.h>
+#include <SkImage.h>
#include <SkImagePriv.h>
+#include <SkMatrix.h>
+#include <SkPaint.h>
+#include <SkPoint.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
+#include <SkRRect.h>
+#include <SkSamplingOptions.h>
+#include <SkTypes.h>
#include "CanvasTransform.h"
#ifdef __ANDROID__ // Layoutlib does not support Layers
#include "Layer.h"
@@ -30,6 +42,8 @@
#include "pipeline/skia/VkFunctorDrawable.h"
#include "pipeline/skia/VkInteropFunctorDrawable.h"
#endif
+#include <log/log.h>
+#include <ui/FatVector.h>
namespace android {
namespace uirenderer {
@@ -42,7 +56,7 @@ namespace skiapipeline {
void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, int width,
int height) {
mCurrentBarrier = nullptr;
- SkASSERT(mDisplayList.get() == nullptr);
+ LOG_FATAL_IF(mDisplayList.get() != nullptr);
if (renderNode) {
mDisplayList = renderNode->detachAvailableList();
@@ -56,20 +70,22 @@ void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, in
mDisplayList->setHasHolePunches(false);
}
-void SkiaRecordingCanvas::punchHole(const SkRRect& rect) {
- // Add the marker annotation to allow HWUI to determine where the current
- // clip/transformation should be applied
+void SkiaRecordingCanvas::punchHole(const SkRRect& rect, float alpha) {
+ // Add the marker annotation to allow HWUI to determine the current
+ // clip/transformation and alpha should be applied
SkVector vector = rect.getSimpleRadii();
- float data[2];
+ float data[3];
data[0] = vector.x();
data[1] = vector.y();
+ data[2] = alpha;
mRecorder.drawAnnotation(rect.rect(), HOLE_PUNCH_ANNOTATION.c_str(),
- SkData::MakeWithCopy(data, 2 * sizeof(float)));
+ SkData::MakeWithCopy(data, sizeof(data)));
// Clear the current rect within the layer itself
SkPaint paint = SkPaint();
- paint.setColor(0);
- paint.setBlendMode(SkBlendMode::kClear);
+ paint.setColor(SkColors::kBlack);
+ paint.setAlphaf(alpha);
+ paint.setBlendMode(SkBlendMode::kDstOut);
mRecorder.drawRRect(rect, paint);
mDisplayList->setHasHolePunches(true);
@@ -192,40 +208,52 @@ void SkiaRecordingCanvas::FilterForImage(SkPaint& paint) {
}
}
+void SkiaRecordingCanvas::handleMutableImages(Bitmap& bitmap, DrawImagePayload& payload) {
+ // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means
+ // it is not safe to store a raw SkImage pointer, because the image object will be destroyed
+ // when this function ends.
+ if (!bitmap.isImmutable() && payload.image.get() && !payload.image->unique()) {
+ mDisplayList->mMutableImages.push_back(payload.image.get());
+ }
+
+ if (bitmap.hasGainmap()) {
+ auto gainmapBitmap = bitmap.gainmap()->bitmap;
+ // Not all DrawImagePayload receivers will store the gainmap (such as DrawImageLattice),
+ // so only store it in the mutable list if it was actually recorded
+ if (!gainmapBitmap->isImmutable() && payload.gainmapImage.get() &&
+ !payload.gainmapImage->unique()) {
+ mDisplayList->mMutableImages.push_back(payload.gainmapImage.get());
+ }
+ }
+}
+
void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) {
- sk_sp<SkImage> image = bitmap.makeImage();
+ auto payload = DrawImagePayload(bitmap);
applyLooper(
paint,
[&](const Paint& p) {
- mRecorder.drawImage(image, left, top, p.sampling(), &p, bitmap.palette());
+ mRecorder.drawImage(DrawImagePayload(payload), left, top, p.sampling(), &p);
},
FilterForImage);
- // if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means
- // it is not safe to store a raw SkImage pointer, because the image object will be destroyed
- // when this function ends.
- if (!bitmap.isImmutable() && image.get() && !image->unique()) {
- mDisplayList->mMutableImages.push_back(image.get());
- }
+ handleMutableImages(bitmap, payload);
}
void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, const SkMatrix& matrix, const Paint* paint) {
SkAutoCanvasRestore acr(&mRecorder, true);
concat(matrix);
- sk_sp<SkImage> image = bitmap.makeImage();
+ auto payload = DrawImagePayload(bitmap);
applyLooper(
paint,
[&](const Paint& p) {
- mRecorder.drawImage(image, 0, 0, p.sampling(), &p, bitmap.palette());
+ mRecorder.drawImage(DrawImagePayload(payload), 0, 0, p.sampling(), &p);
},
FilterForImage);
- if (!bitmap.isImmutable() && image.get() && !image->unique()) {
- mDisplayList->mMutableImages.push_back(image.get());
- }
+ handleMutableImages(bitmap, payload);
}
void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop, float srcRight,
@@ -234,20 +262,17 @@ void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float srcLeft, float srcTop
SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom);
SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom);
- sk_sp<SkImage> image = bitmap.makeImage();
+ auto payload = DrawImagePayload(bitmap);
applyLooper(
paint,
[&](const Paint& p) {
- mRecorder.drawImageRect(image, srcRect, dstRect, p.sampling(), &p,
- SkCanvas::kFast_SrcRectConstraint, bitmap.palette());
+ mRecorder.drawImageRect(DrawImagePayload(payload), srcRect, dstRect, p.sampling(),
+ &p, SkCanvas::kFast_SrcRectConstraint);
},
FilterForImage);
- if (!bitmap.isImmutable() && image.get() && !image->unique() && !srcRect.isEmpty() &&
- !dstRect.isEmpty()) {
- mDisplayList->mMutableImages.push_back(image.get());
- }
+ handleMutableImages(bitmap, payload);
}
void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& chunk, float dstLeft,
@@ -265,15 +290,17 @@ void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& ch
numFlags = (lattice.fXCount + 1) * (lattice.fYCount + 1);
}
- SkAutoSTMalloc<25, SkCanvas::Lattice::RectType> flags(numFlags);
- SkAutoSTMalloc<25, SkColor> colors(numFlags);
+ // Most times, we do not have very many flags/colors, so the stack allocated part of
+ // FatVector will save us a heap allocation.
+ FatVector<SkCanvas::Lattice::RectType, 25> flags(numFlags);
+ FatVector<SkColor, 25> colors(numFlags);
if (numFlags > 0) {
- NinePatchUtils::SetLatticeFlags(&lattice, flags.get(), numFlags, chunk, colors.get());
+ NinePatchUtils::SetLatticeFlags(&lattice, flags.data(), numFlags, chunk, colors.data());
}
lattice.fBounds = nullptr;
SkRect dst = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom);
- sk_sp<SkImage> image = bitmap.makeImage();
+ auto payload = DrawImagePayload(bitmap);
// HWUI always draws 9-patches with linear filtering, regardless of the Paint.
const SkFilterMode filter = SkFilterMode::kLinear;
@@ -281,13 +308,11 @@ void SkiaRecordingCanvas::drawNinePatch(Bitmap& bitmap, const Res_png_9patch& ch
applyLooper(
paint,
[&](const SkPaint& p) {
- mRecorder.drawImageLattice(image, lattice, dst, filter, &p, bitmap.palette());
+ mRecorder.drawImageLattice(DrawImagePayload(payload), lattice, dst, filter, &p);
},
FilterForImage);
- if (!bitmap.isImmutable() && image.get() && !image->unique() && !dst.isEmpty()) {
- mDisplayList->mMutableImages.push_back(image.get());
- }
+ handleMutableImages(bitmap, payload);
}
double SkiaRecordingCanvas::drawAnimatedImage(AnimatedImageDrawable* animatedImage) {
@@ -296,6 +321,11 @@ double SkiaRecordingCanvas::drawAnimatedImage(AnimatedImageDrawable* animatedIma
return 0;
}
+void SkiaRecordingCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) {
+ mDisplayList->mMeshes.push_back(&mesh);
+ mRecorder.drawMesh(mesh, blender, paint);
+}
+
} // namespace skiapipeline
} // namespace uirenderer
} // namespace android
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index 1445a27e4248..a8e4580dc200 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -22,6 +22,11 @@
#include "SkiaDisplayList.h"
#include "pipeline/skia/AnimatedDrawables.h"
+class SkBitmap;
+class SkMatrix;
+class SkPaint;
+class SkRRect;
+
namespace android {
namespace uirenderer {
namespace skiapipeline {
@@ -45,7 +50,7 @@ public:
initDisplayList(renderNode, width, height);
}
- virtual void punchHole(const SkRRect& rect) override;
+ virtual void punchHole(const SkRRect& rect, float alpha) override;
virtual void finishRecording(uirenderer::RenderNode* destination) override;
std::unique_ptr<SkiaDisplayList> finishRecording();
@@ -76,6 +81,7 @@ public:
virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override;
virtual void enableZ(bool enableZ) override;
+ virtual void drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) override;
virtual void drawLayer(uirenderer::DeferredLayerUpdater* layerHandle) override;
virtual void drawRenderNode(uirenderer::RenderNode* renderNode) override;
@@ -97,6 +103,8 @@ private:
*/
void initDisplayList(uirenderer::RenderNode* renderNode, int width, int height);
+ void handleMutableImages(Bitmap& bitmap, DrawImagePayload& payload);
+
using INHERITED = SkiaCanvas;
};
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index 18e0b91f0253..86096d5bd01c 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -57,7 +57,9 @@ VulkanManager& SkiaVulkanPipeline::vulkanManager() {
MakeCurrentResult SkiaVulkanPipeline::makeCurrent() {
// In case the surface was destroyed (e.g. a previous trimMemory call) we
// need to recreate it here.
- if (!isSurfaceReady() && mNativeWindow) {
+ if (mHardwareBuffer) {
+ mRenderThread.requireVkContext();
+ } else if (!isSurfaceReady() && mNativeWindow) {
setSurface(mNativeWindow.get(), SwapBehavior::kSwap_default);
}
return isContextReady() ? MakeCurrentResult::AlreadyCurrent : MakeCurrentResult::Failed;
@@ -72,28 +74,39 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw(
const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
- const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) {
- sk_sp<SkSurface> backBuffer = mVkSurface->getCurrentSkSurface();
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams) {
+ sk_sp<SkSurface> backBuffer;
+ SkMatrix preTransform;
+ if (mHardwareBuffer) {
+ backBuffer = getBufferSkSurface(bufferParams);
+ preTransform = bufferParams.getTransform();
+ } else {
+ backBuffer = mVkSurface->getCurrentSkSurface();
+ preTransform = mVkSurface->getCurrentPreTransform();
+ }
+
if (backBuffer.get() == nullptr) {
return {false, -1};
}
// update the coordinates of the global light position based on surface rotation
- SkPoint lightCenter = mVkSurface->getCurrentPreTransform().mapXY(lightGeometry.center.x,
- lightGeometry.center.y);
+ SkPoint lightCenter = preTransform.mapXY(lightGeometry.center.x, lightGeometry.center.y);
LightGeometry localGeometry = lightGeometry;
localGeometry.center.x = lightCenter.fX;
localGeometry.center.y = lightCenter.fY;
LightingInfo::updateLighting(localGeometry, lightInfo);
renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, backBuffer,
- mVkSurface->getCurrentPreTransform());
+ preTransform);
// Draw visual debugging features
if (CC_UNLIKELY(Properties::showDirtyRegions ||
ProfileType::None != Properties::getProfileType())) {
SkCanvas* profileCanvas = backBuffer->getCanvas();
- SkiaProfileRenderer profileRenderer(profileCanvas);
+ SkAutoCanvasRestore saver(profileCanvas, true);
+ profileCanvas->concat(mVkSurface->getCurrentPreTransform());
+ SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height());
profiler->draw(profileRenderer);
}
@@ -114,12 +127,16 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw(
bool SkiaVulkanPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) {
- *requireSwap = drew;
-
// Even if we decided to cancel the frame, from the perspective of jank
// metrics the frame was swapped at this point
currentFrameInfo->markSwapBuffers();
+ if (mHardwareBuffer) {
+ return false;
+ }
+
+ *requireSwap = drew;
+
if (*requireSwap) {
vulkanManager().swapBuffers(mVkSurface, screenDirty);
}
@@ -135,6 +152,12 @@ DeferredLayerUpdater* SkiaVulkanPipeline::createTextureLayer() {
void SkiaVulkanPipeline::onStop() {}
+[[nodiscard]] android::base::unique_fd SkiaVulkanPipeline::flush() {
+ int fence = -1;
+ vulkanManager().createReleaseFence(&fence, mRenderThread.getGrContext());
+ return android::base::unique_fd(fence);
+}
+
// We can safely ignore the swap behavior because VkManager will always operate
// in a mode equivalent to EGLManager::SwapBehavior::kBufferAge
bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior /*swapBehavior*/) {
@@ -155,6 +178,13 @@ bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior /*swapB
return mVkSurface != nullptr;
}
+void SkiaVulkanPipeline::setTargetSdrHdrRatio(float ratio) {
+ SkiaPipeline::setTargetSdrHdrRatio(ratio);
+ if (mVkSurface) {
+ mVkSurface->setColorSpace(mSurfaceColorSpace);
+ }
+}
+
bool SkiaVulkanPipeline::isSurfaceReady() {
return CC_UNLIKELY(mVkSurface != nullptr);
}
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
index 7c8f65b87605..284cde537ec0 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
@@ -16,11 +16,15 @@
#pragma once
+#include "SkRefCnt.h"
#include "SkiaPipeline.h"
+#include "renderstate/RenderState.h"
+#include "renderthread/HardwareBufferRenderParams.h"
#include "renderthread/VulkanManager.h"
#include "renderthread/VulkanSurface.h"
-#include "renderstate/RenderState.h"
+class SkBitmap;
+struct SkRect;
namespace android {
namespace uirenderer {
@@ -33,22 +37,23 @@ public:
renderthread::MakeCurrentResult makeCurrent() override;
renderthread::Frame getFrame() override;
- renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame,
- const SkRect& screenDirty, const SkRect& dirty,
- const LightGeometry& lightGeometry,
- LayerUpdateQueue* layerUpdateQueue,
- const Rect& contentDrawBounds, bool opaque,
- const LightInfo& lightInfo,
- const std::vector<sp<RenderNode> >& renderNodes,
- FrameInfoVisualizer* profiler) override;
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams) override;
GrSurfaceOrigin getSurfaceOrigin() override { return kTopLeft_GrSurfaceOrigin; }
bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) override;
DeferredLayerUpdater* createTextureLayer() override;
+ [[nodiscard]] android::base::unique_fd flush() override;
+
bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override;
void onStop() override;
bool isSurfaceReady() override;
bool isContextReady() override;
+ void setTargetSdrHdrRatio(float ratio) override;
const SkM44& getPixelSnapMatrix() const override;
static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor);
@@ -60,7 +65,6 @@ protected:
private:
renderthread::VulkanManager& vulkanManager();
-
renderthread::VulkanSurface* mVkSurface = nullptr;
sp<ANativeWindow> mNativeWindow;
};
diff --git a/libs/hwui/pipeline/skia/StretchMask.cpp b/libs/hwui/pipeline/skia/StretchMask.cpp
index 2dbeb3adfab3..cad3703d8d2b 100644
--- a/libs/hwui/pipeline/skia/StretchMask.cpp
+++ b/libs/hwui/pipeline/skia/StretchMask.cpp
@@ -14,8 +14,12 @@
* limitations under the License.
*/
#include "StretchMask.h"
-#include "SkSurface.h"
+
+#include "SkBlendMode.h"
#include "SkCanvas.h"
+#include "SkSurface.h"
+#include "include/gpu/GpuTypes.h" // from Skia
+
#include "TransformCanvas.h"
#include "SkiaDisplayList.h"
@@ -34,7 +38,7 @@ void StretchMask::draw(GrRecordingContext* context,
// not match.
mMaskSurface = SkSurface::MakeRenderTarget(
context,
- SkBudgeted::kYes,
+ skgpu::Budgeted::kYes,
SkImageInfo::Make(
width,
height,
diff --git a/libs/hwui/pipeline/skia/TransformCanvas.cpp b/libs/hwui/pipeline/skia/TransformCanvas.cpp
index 41e36874b862..c320df035d08 100644
--- a/libs/hwui/pipeline/skia/TransformCanvas.cpp
+++ b/libs/hwui/pipeline/skia/TransformCanvas.cpp
@@ -19,19 +19,25 @@
#include "HolePunch.h"
#include "SkData.h"
#include "SkDrawable.h"
+#include "SkMatrix.h"
+#include "SkPaint.h"
+#include "SkRect.h"
+#include "SkRRect.h"
using namespace android::uirenderer::skiapipeline;
void TransformCanvas::onDrawAnnotation(const SkRect& rect, const char* key, SkData* value) {
if (HOLE_PUNCH_ANNOTATION == key) {
auto* rectParams = reinterpret_cast<const float*>(value->data());
- float radiusX = rectParams[0];
- float radiusY = rectParams[1];
+ const float radiusX = rectParams[0];
+ const float radiusY = rectParams[1];
+ const float alpha = rectParams[2];
SkRRect roundRect = SkRRect::MakeRectXY(rect, radiusX, radiusY);
SkPaint paint;
paint.setColor(SkColors::kBlack);
paint.setBlendMode(mHolePunchBlendMode);
+ paint.setAlphaf(alpha);
mWrappedCanvas->drawRRect(roundRect, paint);
}
}
diff --git a/libs/hwui/pipeline/skia/TransformCanvas.h b/libs/hwui/pipeline/skia/TransformCanvas.h
index 685b71d017e9..15f0c1abc55a 100644
--- a/libs/hwui/pipeline/skia/TransformCanvas.h
+++ b/libs/hwui/pipeline/skia/TransformCanvas.h
@@ -19,6 +19,13 @@
#include "SkPaintFilterCanvas.h"
#include <effects/StretchEffect.h>
+class SkData;
+class SkDrawable;
+class SkMatrix;
+class SkPaint;
+enum class SkBlendMode;
+struct SkRect;
+
class TransformCanvas : public SkPaintFilterCanvas {
public:
TransformCanvas(SkCanvas* target, SkBlendMode blendmode) :
diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
index e6ef95b9cf91..b62711f50c94 100644
--- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
@@ -15,17 +15,21 @@
*/
#include "VkFunctorDrawable.h"
-#include <private/hwui/DrawVkInfo.h>
#include <GrBackendDrawableInfo.h>
#include <SkAndroidFrameworkUtils.h>
#include <SkImage.h>
#include <SkM44.h>
#include <gui/TraceUtils.h>
+#include <private/hwui/DrawVkInfo.h>
#include <utils/Color.h>
#include <utils/Trace.h>
#include <vk/GrVkTypes.h>
+
#include <thread>
+
+#include "effects/GainmapRenderer.h"
+#include "renderthread/CanvasContext.h"
#include "renderthread/RenderThread.h"
#include "renderthread/VulkanManager.h"
#include "thread/ThreadBase.h"
@@ -73,6 +77,8 @@ void VkFunctorDrawHandler::draw(const GrBackendDrawableInfo& info) {
.clip_right = mClip.fRight,
.clip_bottom = mClip.fBottom,
.is_layer = !vulkan_info.fFromSwapchainOrAndroidWindow,
+ .currentHdrSdrRatio = getTargetHdrSdrRatio(mImageInfo.colorSpace()),
+ .shouldDither = renderthread::CanvasContext::shouldDither(),
};
mat4.getColMajor(&params.transform[0]);
params.secondary_command_buffer = vulkan_info.fSecondaryCommandBuffer;
diff --git a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp
index 3c7617d35c7c..adf3c06b8624 100644
--- a/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/VkInteropFunctorDrawable.cpp
@@ -32,6 +32,9 @@
#include "renderthread/EglManager.h"
#include "thread/ThreadBase.h"
#include "utils/TimeUtils.h"
+#include "effects/GainmapRenderer.h"
+
+#include <SkBlendMode.h>
namespace android {
namespace uirenderer {
@@ -137,6 +140,7 @@ void VkInteropFunctorDrawable::onDraw(SkCanvas* canvas) {
info.height = mFBInfo.height();
mat4.getColMajor(&info.transform[0]);
info.color_space_ptr = canvas->imageInfo().colorSpace();
+ info.currentHdrSdrRatio = getTargetHdrSdrRatio(info.color_space_ptr);
glViewport(0, 0, info.width, info.height);
diff --git a/libs/hwui/private/hwui/DrawGlInfo.h b/libs/hwui/private/hwui/DrawGlInfo.h
index 501b8df9bc36..eb1f9304a5c8 100644
--- a/libs/hwui/private/hwui/DrawGlInfo.h
+++ b/libs/hwui/private/hwui/DrawGlInfo.h
@@ -18,6 +18,7 @@
#define ANDROID_HWUI_DRAW_GL_INFO_H
#include <SkColorSpace.h>
+#include <SkColorType.h>
namespace android {
namespace uirenderer {
@@ -86,6 +87,17 @@ struct DrawGlInfo {
// commands are issued.
kStatusDrew = 0x4
};
+
+ // The current HDR/SDR ratio that we are rendering to. The transform to SDR will already
+ // be baked into the color_space_ptr, so this is just to indicate the amount of extended
+ // range is available if desired
+ float currentHdrSdrRatio;
+
+ // Whether or not dithering is globally enabled
+ bool shouldDither;
+
+ // The color type of the destination framebuffer
+ SkColorType fboColorType;
}; // struct DrawGlInfo
} // namespace uirenderer
diff --git a/libs/hwui/private/hwui/DrawVkInfo.h b/libs/hwui/private/hwui/DrawVkInfo.h
index 5c596576df4e..122080658927 100644
--- a/libs/hwui/private/hwui/DrawVkInfo.h
+++ b/libs/hwui/private/hwui/DrawVkInfo.h
@@ -71,6 +71,14 @@ struct VkFunctorDrawParams {
// Input: Whether destination surface is offscreen surface.
bool is_layer;
+
+ // The current HDR/SDR ratio that we are rendering to. The transform to SDR will already
+ // be baked into the color_space_ptr, so this is just to indicate the amount of extended
+ // range is available if desired
+ float currentHdrSdrRatio;
+
+ // Whether or not dithering is globally enabled
+ bool shouldDither;
};
} // namespace uirenderer
diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp
index ded2b06fb3cf..babce88b8e1e 100644
--- a/libs/hwui/renderthread/CacheManager.cpp
+++ b/libs/hwui/renderthread/CacheManager.cpp
@@ -16,49 +16,63 @@
#include "CacheManager.h"
+#include <GrContextOptions.h>
+#include <SkExecutor.h>
+#include <SkGraphics.h>
+#include <math.h>
+#include <utils/Trace.h>
+
+#include <set>
+
+#include "CanvasContext.h"
#include "DeviceInfo.h"
#include "Layer.h"
#include "Properties.h"
#include "RenderThread.h"
+#include "VulkanManager.h"
#include "pipeline/skia/ATraceMemoryDump.h"
#include "pipeline/skia/ShaderCache.h"
#include "pipeline/skia/SkiaMemoryTracer.h"
#include "renderstate/RenderState.h"
#include "thread/CommonPool.h"
-#include <utils/Trace.h>
-
-#include <GrContextOptions.h>
-#include <SkExecutor.h>
-#include <SkGraphics.h>
-#include <SkMathPriv.h>
-#include <math.h>
-#include <set>
namespace android {
namespace uirenderer {
namespace renderthread {
-// This multiplier was selected based on historical review of cache sizes relative
-// to the screen resolution. This is meant to be a conservative default based on
-// that analysis. The 4.0f is used because the default pixel format is assumed to
-// be ARGB_8888.
-#define SURFACE_SIZE_MULTIPLIER (12.0f * 4.0f)
-#define BACKGROUND_RETENTION_PERCENTAGE (0.5f)
-
-CacheManager::CacheManager()
- : mMaxSurfaceArea(DeviceInfo::getWidth() * DeviceInfo::getHeight())
- , mMaxResourceBytes(mMaxSurfaceArea * SURFACE_SIZE_MULTIPLIER)
- , mBackgroundResourceBytes(mMaxResourceBytes * BACKGROUND_RETENTION_PERCENTAGE)
- // This sets the maximum size for a single texture atlas in the GPU font cache. If
- // necessary, the cache can allocate additional textures that are counted against the
- // total cache limits provided to Skia.
- , mMaxGpuFontAtlasBytes(GrNextSizePow2(mMaxSurfaceArea))
- // This sets the maximum size of the CPU font cache to be at least the same size as the
- // total number of GPU font caches (i.e. 4 separate GPU atlases).
- , mMaxCpuFontCacheBytes(
- std::max(mMaxGpuFontAtlasBytes * 4, SkGraphics::GetFontCacheLimit()))
- , mBackgroundCpuFontCacheBytes(mMaxCpuFontCacheBytes * BACKGROUND_RETENTION_PERCENTAGE) {
+CacheManager::CacheManager(RenderThread& thread)
+ : mRenderThread(thread), mMemoryPolicy(loadMemoryPolicy()) {
+ mMaxSurfaceArea = static_cast<size_t>((DeviceInfo::getWidth() * DeviceInfo::getHeight()) *
+ mMemoryPolicy.initialMaxSurfaceAreaScale);
+ setupCacheLimits();
+}
+
+static inline int countLeadingZeros(uint32_t mask) {
+ // __builtin_clz(0) is undefined, so we have to detect that case.
+ return mask ? __builtin_clz(mask) : 32;
+}
+
+// Return the smallest power-of-2 >= n.
+static inline uint32_t nextPowerOfTwo(uint32_t n) {
+ return n ? (1 << (32 - countLeadingZeros(n - 1))) : 1;
+}
+
+void CacheManager::setupCacheLimits() {
+ mMaxResourceBytes = mMaxSurfaceArea * mMemoryPolicy.surfaceSizeMultiplier;
+ mBackgroundResourceBytes = mMaxResourceBytes * mMemoryPolicy.backgroundRetentionPercent;
+ // This sets the maximum size for a single texture atlas in the GPU font cache. If
+ // necessary, the cache can allocate additional textures that are counted against the
+ // total cache limits provided to Skia.
+ mMaxGpuFontAtlasBytes = nextPowerOfTwo(mMaxSurfaceArea);
+ // This sets the maximum size of the CPU font cache to be at least the same size as the
+ // total number of GPU font caches (i.e. 4 separate GPU atlases).
+ mMaxCpuFontCacheBytes = std::max(mMaxGpuFontAtlasBytes * 4, SkGraphics::GetFontCacheLimit());
+ mBackgroundCpuFontCacheBytes = mMaxCpuFontCacheBytes * mMemoryPolicy.backgroundRetentionPercent;
+
SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes);
+ if (mGrContext) {
+ mGrContext->setResourceCacheLimit(mMaxResourceBytes);
+ }
}
void CacheManager::reset(sk_sp<GrDirectContext> context) {
@@ -69,6 +83,7 @@ void CacheManager::reset(sk_sp<GrDirectContext> context) {
if (context) {
mGrContext = std::move(context);
mGrContext->setResourceCacheLimit(mMaxResourceBytes);
+ mLastDeferredCleanup = systemTime(CLOCK_MONOTONIC);
}
}
@@ -93,10 +108,9 @@ void CacheManager::configureContext(GrContextOptions* contextOptions, const void
auto& cache = skiapipeline::ShaderCache::get();
cache.initShaderDiskCache(identity, size);
contextOptions->fPersistentCache = &cache;
- contextOptions->fGpuPathRenderers &= ~GpuPathRenderers::kCoverageCounting;
}
-void CacheManager::trimMemory(TrimMemoryMode mode) {
+void CacheManager::trimMemory(TrimLevel mode) {
if (!mGrContext) {
return;
}
@@ -105,20 +119,42 @@ void CacheManager::trimMemory(TrimMemoryMode mode) {
mGrContext->flushAndSubmit(/*syncCpu=*/true);
switch (mode) {
- case TrimMemoryMode::Complete:
+ case TrimLevel::BACKGROUND:
mGrContext->freeGpuResources();
SkGraphics::PurgeAllCaches();
+ mRenderThread.destroyRenderingContext();
break;
- case TrimMemoryMode::UiHidden:
+ case TrimLevel::UI_HIDDEN:
// Here we purge all the unlocked scratch resources and then toggle the resources cache
// limits between the background and max amounts. This causes the unlocked resources
// that have persistent data to be purged in LRU order.
- mGrContext->purgeUnlockedResources(true);
mGrContext->setResourceCacheLimit(mBackgroundResourceBytes);
- mGrContext->setResourceCacheLimit(mMaxResourceBytes);
SkGraphics::SetFontCacheLimit(mBackgroundCpuFontCacheBytes);
+ mGrContext->purgeUnlockedResources(mMemoryPolicy.purgeScratchOnly);
+ mGrContext->setResourceCacheLimit(mMaxResourceBytes);
SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes);
break;
+ default:
+ break;
+ }
+}
+
+void CacheManager::trimCaches(CacheTrimLevel mode) {
+ switch (mode) {
+ case CacheTrimLevel::FONT_CACHE:
+ SkGraphics::PurgeFontCache();
+ break;
+ case CacheTrimLevel::RESOURCE_CACHE:
+ SkGraphics::PurgeResourceCache();
+ break;
+ case CacheTrimLevel::ALL_CACHES:
+ SkGraphics::PurgeAllCaches();
+ if (mGrContext) {
+ mGrContext->purgeUnlockedResources(false);
+ }
+ break;
+ default:
+ break;
}
}
@@ -147,11 +183,35 @@ void CacheManager::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) {
}
void CacheManager::dumpMemoryUsage(String8& log, const RenderState* renderState) {
+ log.appendFormat(R"(Memory policy:
+ Max surface area: %zu
+ Max resource usage: %.2fMB (x%.0f)
+ Background retention: %.0f%% (altUiHidden = %s)
+)",
+ mMaxSurfaceArea, mMaxResourceBytes / 1000000.f,
+ mMemoryPolicy.surfaceSizeMultiplier,
+ mMemoryPolicy.backgroundRetentionPercent * 100.0f,
+ mMemoryPolicy.useAlternativeUiHidden ? "true" : "false");
+ if (Properties::isSystemOrPersistent) {
+ log.appendFormat(" IsSystemOrPersistent\n");
+ }
+ log.appendFormat(" GPU Context timeout: %" PRIu64 "\n", ns2s(mMemoryPolicy.contextTimeout));
+ size_t stoppedContexts = 0;
+ for (auto context : mCanvasContexts) {
+ if (context->isStopped()) stoppedContexts++;
+ }
+ log.appendFormat("Contexts: %zu (stopped = %zu)\n", mCanvasContexts.size(), stoppedContexts);
+
+ auto vkInstance = VulkanManager::peekInstance();
if (!mGrContext) {
- log.appendFormat("No valid cache instance.\n");
+ if (!vkInstance) {
+ log.appendFormat("No GPU context.\n");
+ } else {
+ log.appendFormat("No GrContext; however %d remaining Vulkan refs",
+ vkInstance->getStrongCount() - 1);
+ }
return;
}
-
std::vector<skiapipeline::ResourcePair> cpuResourceMap = {
{"skia/sk_resource_cache/bitmap_", "Bitmaps"},
{"skia/sk_resource_cache/rrect-blur_", "Masks"},
@@ -199,6 +259,8 @@ void CacheManager::dumpMemoryUsage(String8& log, const RenderState* renderState)
}
void CacheManager::onFrameCompleted() {
+ cancelDestroyContext();
+ mFrameCompletions.next() = systemTime(CLOCK_MONOTONIC);
if (ATRACE_ENABLED()) {
static skiapipeline::ATraceMemoryDump tracer;
tracer.startFrame();
@@ -210,11 +272,82 @@ void CacheManager::onFrameCompleted() {
}
}
-void CacheManager::performDeferredCleanup(nsecs_t cleanupOlderThanMillis) {
- if (mGrContext) {
- mGrContext->performDeferredCleanup(
- std::chrono::milliseconds(cleanupOlderThanMillis),
- /* scratchResourcesOnly */true);
+void CacheManager::onThreadIdle() {
+ if (!mGrContext || mFrameCompletions.size() == 0) return;
+
+ const nsecs_t now = systemTime(CLOCK_MONOTONIC);
+ // Rate limiting
+ if ((now - mLastDeferredCleanup) < 25_ms) {
+ mLastDeferredCleanup = now;
+ const nsecs_t frameCompleteNanos = mFrameCompletions[0];
+ const nsecs_t frameDiffNanos = now - frameCompleteNanos;
+ const nsecs_t cleanupMillis =
+ ns2ms(std::max(frameDiffNanos, mMemoryPolicy.minimumResourceRetention));
+ mGrContext->performDeferredCleanup(std::chrono::milliseconds(cleanupMillis),
+ mMemoryPolicy.purgeScratchOnly);
+ }
+}
+
+void CacheManager::scheduleDestroyContext() {
+ if (mMemoryPolicy.contextTimeout > 0) {
+ mRenderThread.queue().postDelayed(mMemoryPolicy.contextTimeout,
+ [this, genId = mGenerationId] {
+ if (mGenerationId != genId) return;
+ // GenID should have already stopped this, but just in
+ // case
+ if (!areAllContextsStopped()) return;
+ mRenderThread.destroyRenderingContext();
+ });
+ }
+}
+
+void CacheManager::cancelDestroyContext() {
+ if (mIsDestructionPending) {
+ mIsDestructionPending = false;
+ mGenerationId++;
+ }
+}
+
+bool CacheManager::areAllContextsStopped() {
+ for (auto context : mCanvasContexts) {
+ if (!context->isStopped()) return false;
+ }
+ return true;
+}
+
+void CacheManager::checkUiHidden() {
+ if (!mGrContext) return;
+
+ if (mMemoryPolicy.useAlternativeUiHidden && areAllContextsStopped()) {
+ trimMemory(TrimLevel::UI_HIDDEN);
+ }
+}
+
+void CacheManager::registerCanvasContext(CanvasContext* context) {
+ mCanvasContexts.push_back(context);
+ cancelDestroyContext();
+}
+
+void CacheManager::unregisterCanvasContext(CanvasContext* context) {
+ std::erase(mCanvasContexts, context);
+ checkUiHidden();
+ if (mCanvasContexts.empty()) {
+ scheduleDestroyContext();
+ }
+}
+
+void CacheManager::onContextStopped(CanvasContext* context) {
+ checkUiHidden();
+ if (mMemoryPolicy.releaseContextOnStoppedOnly && areAllContextsStopped()) {
+ scheduleDestroyContext();
+ }
+}
+
+void CacheManager::notifyNextFrameSize(int width, int height) {
+ int frameArea = width * height;
+ if (frameArea > mMaxSurfaceArea) {
+ mMaxSurfaceArea = frameArea;
+ setupCacheLimits();
}
}
diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h
index af82672c6f23..5e43ac209696 100644
--- a/libs/hwui/renderthread/CacheManager.h
+++ b/libs/hwui/renderthread/CacheManager.h
@@ -22,7 +22,11 @@
#endif
#include <SkSurface.h>
#include <utils/String8.h>
+
#include <vector>
+
+#include "MemoryPolicy.h"
+#include "utils/RingBuffer.h"
#include "utils/TimeUtils.h"
namespace android {
@@ -35,17 +39,16 @@ class RenderState;
namespace renderthread {
-class IRenderPipeline;
class RenderThread;
+class CanvasContext;
class CacheManager {
public:
- enum class TrimMemoryMode { Complete, UiHidden };
-
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
void configureContext(GrContextOptions* context, const void* identity, ssize_t size);
#endif
- void trimMemory(TrimMemoryMode mode);
+ void trimMemory(TrimLevel mode);
+ void trimCaches(CacheTrimLevel mode);
void trimStaleResources();
void dumpMemoryUsage(String8& log, const RenderState* renderState = nullptr);
void getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage);
@@ -53,30 +56,50 @@ public:
size_t getCacheSize() const { return mMaxResourceBytes; }
size_t getBackgroundCacheSize() const { return mBackgroundResourceBytes; }
void onFrameCompleted();
+ void notifyNextFrameSize(int width, int height);
+
+ void onThreadIdle();
- void performDeferredCleanup(nsecs_t cleanupOlderThanMillis);
+ void registerCanvasContext(CanvasContext* context);
+ void unregisterCanvasContext(CanvasContext* context);
+ void onContextStopped(CanvasContext* context);
private:
friend class RenderThread;
- explicit CacheManager();
+ explicit CacheManager(RenderThread& thread);
+ void setupCacheLimits();
+ bool areAllContextsStopped();
+ void checkUiHidden();
+ void scheduleDestroyContext();
+ void cancelDestroyContext();
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
void reset(sk_sp<GrDirectContext> grContext);
#endif
void destroy();
- const size_t mMaxSurfaceArea;
+ RenderThread& mRenderThread;
+ const MemoryPolicy& mMemoryPolicy;
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
sk_sp<GrDirectContext> mGrContext;
#endif
- const size_t mMaxResourceBytes;
- const size_t mBackgroundResourceBytes;
+ size_t mMaxSurfaceArea = 0;
+
+ size_t mMaxResourceBytes = 0;
+ size_t mBackgroundResourceBytes = 0;
+
+ size_t mMaxGpuFontAtlasBytes = 0;
+ size_t mMaxCpuFontCacheBytes = 0;
+ size_t mBackgroundCpuFontCacheBytes = 0;
+
+ std::vector<CanvasContext*> mCanvasContexts;
+ RingBuffer<uint64_t, 100> mFrameCompletions;
- const size_t mMaxGpuFontAtlasBytes;
- const size_t mMaxCpuFontCacheBytes;
- const size_t mBackgroundCpuFontCacheBytes;
+ nsecs_t mLastDeferredCleanup = 0;
+ bool mIsDestructionPending = false;
+ uint32_t mGenerationId = 0;
};
} /* namespace renderthread */
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index f56d19bfcea0..16b35ffcabac 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -42,9 +42,6 @@
#include "utils/GLUtils.h"
#include "utils/TimeUtils.h"
-#define TRIM_MEMORY_COMPLETE 80
-#define TRIM_MEMORY_UI_HIDDEN 20
-
#define LOG_FRAMETIME_MMA 0
#if LOG_FRAMETIME_MMA
@@ -74,16 +71,19 @@ CanvasContext* ScopedActiveContext::sActiveContext = nullptr;
} /* namespace */
CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent,
- RenderNode* rootRenderNode, IContextFactory* contextFactory) {
+ RenderNode* rootRenderNode, IContextFactory* contextFactory,
+ int32_t uiThreadId, int32_t renderThreadId) {
auto renderType = Properties::getRenderPipelineType();
switch (renderType) {
case RenderPipelineType::SkiaGL:
return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
- std::make_unique<skiapipeline::SkiaOpenGLPipeline>(thread));
+ std::make_unique<skiapipeline::SkiaOpenGLPipeline>(thread),
+ uiThreadId, renderThreadId);
case RenderPipelineType::SkiaVulkan:
return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
- std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread));
+ std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread),
+ uiThreadId, renderThreadId);
default:
LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t)renderType);
break;
@@ -113,7 +113,8 @@ void CanvasContext::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
IContextFactory* contextFactory,
- std::unique_ptr<IRenderPipeline> renderPipeline)
+ std::unique_ptr<IRenderPipeline> renderPipeline, pid_t uiThreadId,
+ pid_t renderThreadId)
: mRenderThread(thread)
, mGenerationID(0)
, mOpaque(!translucent)
@@ -121,7 +122,9 @@ CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode*
, mJankTracker(&thread.globalProfileData())
, mProfiler(mJankTracker.frames(), thread.timeLord().frameIntervalNanos())
, mContentDrawBounds(0, 0, 0, 0)
- , mRenderPipeline(std::move(renderPipeline)) {
+ , mRenderPipeline(std::move(renderPipeline))
+ , mHintSessionWrapper(uiThreadId, renderThreadId) {
+ mRenderThread.cacheManager().registerCanvasContext(this);
rootRenderNode->makeRoot();
mRenderNodes.emplace_back(rootRenderNode);
mProfiler.setDensity(DeviceInfo::getDensity());
@@ -133,6 +136,7 @@ CanvasContext::~CanvasContext() {
node->clearRoot();
}
mRenderNodes.clear();
+ mRenderThread.cacheManager().unregisterCanvasContext(this);
}
void CanvasContext::addRenderNode(RenderNode* node, bool placeFront) {
@@ -149,11 +153,13 @@ void CanvasContext::removeRenderNode(RenderNode* node) {
void CanvasContext::destroy() {
stopDrawing();
+ setHardwareBuffer(nullptr);
setSurface(nullptr);
setSurfaceControl(nullptr);
freePrefetchedLayers();
destroyHardwareResources();
mAnimationContext->destroy();
+ mRenderThread.cacheManager().onContextStopped(this);
}
static void setBufferCount(ANativeWindow* window) {
@@ -171,6 +177,19 @@ static void setBufferCount(ANativeWindow* window) {
native_window_set_buffer_count(window, bufferCount);
}
+void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (mHardwareBuffer) {
+ AHardwareBuffer_release(mHardwareBuffer);
+ mHardwareBuffer = nullptr;
+ }
+
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ mHardwareBuffer = buffer;
+ }
+ mRenderPipeline->setHardwareBuffer(mHardwareBuffer);
+}
+
void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) {
ATRACE_CALL();
@@ -217,7 +236,6 @@ void CanvasContext::setupPipelineSurface() {
if (mNativeSurface && !mNativeSurface->didSetExtraBuffers()) {
setBufferCount(mNativeSurface->getNativeWindow());
-
}
mFrameNumber = 0;
@@ -229,6 +247,8 @@ void CanvasContext::setupPipelineSurface() {
// Order is important when new and old surfaces are the same, because old surface has
// its frame stats disabled automatically.
native_window_enable_frame_timestamps(mNativeSurface->getNativeWindow(), true);
+ native_window_set_scaling_mode(mNativeSurface->getNativeWindow(),
+ NATIVE_WINDOW_SCALING_MODE_FREEZE);
} else {
mRenderThread.removeFrameCallback(this);
mGenerationID++;
@@ -251,7 +271,8 @@ void CanvasContext::setStopped(bool stopped) {
mGenerationID++;
mRenderThread.removeFrameCallback(this);
mRenderPipeline->onStop();
- } else if (mIsDirty && hasSurface()) {
+ mRenderThread.cacheManager().onContextStopped(this);
+ } else if (mIsDirty && hasOutputTarget()) {
mRenderThread.postFrameCallback(this);
}
}
@@ -277,9 +298,39 @@ void CanvasContext::setOpaque(bool opaque) {
mOpaque = opaque;
}
-void CanvasContext::setColorMode(ColorMode mode) {
- mRenderPipeline->setSurfaceColorProperties(mode);
- setupPipelineSurface();
+float CanvasContext::setColorMode(ColorMode mode) {
+ if (mode != mColorMode) {
+ mColorMode = mode;
+ mRenderPipeline->setSurfaceColorProperties(mode);
+ setupPipelineSurface();
+ }
+ switch (mColorMode) {
+ case ColorMode::Hdr:
+ return Properties::maxHdrHeadroomOn8bit;
+ case ColorMode::Hdr10:
+ return 10.f;
+ default:
+ return 1.f;
+ }
+}
+
+float CanvasContext::targetSdrHdrRatio() const {
+ if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) {
+ return mTargetSdrHdrRatio;
+ } else {
+ return 1.f;
+ }
+}
+
+void CanvasContext::setTargetSdrHdrRatio(float ratio) {
+ if (mTargetSdrHdrRatio == ratio) return;
+
+ mTargetSdrHdrRatio = ratio;
+ mRenderPipeline->setTargetSdrHdrRatio(ratio);
+ // We don't actually but we need to behave as if we do. Specifically we need to ensure
+ // all buffers in the swapchain are fully re-rendered as any partial updates to them will
+ // result in mixed target white points which looks really bad & flickery
+ mHaveNewSurface = true;
}
bool CanvasContext::makeCurrent() {
@@ -384,7 +435,7 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
mIsDirty = true;
- if (CC_UNLIKELY(!hasSurface())) {
+ if (CC_UNLIKELY(!hasOutputTarget())) {
mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
info.out.canDrawThisFrame = false;
return;
@@ -461,7 +512,6 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
}
void CanvasContext::stopDrawing() {
- cleanupResources();
mRenderThread.removeFrameCallback(this);
mAnimationContext->pauseAnimators();
mGenerationID++;
@@ -470,18 +520,33 @@ void CanvasContext::stopDrawing() {
void CanvasContext::notifyFramePending() {
ATRACE_CALL();
mRenderThread.pushBackFrameCallback(this);
+ sendLoadResetHint();
}
-nsecs_t CanvasContext::draw() {
+Frame CanvasContext::getFrame() {
+ if (mHardwareBuffer != nullptr) {
+ return {mBufferParams.getLogicalWidth(), mBufferParams.getLogicalHeight(), 0};
+ } else {
+ return mRenderPipeline->getFrame();
+ }
+}
+
+void CanvasContext::draw(bool solelyTextureViewUpdates) {
if (auto grContext = getGrContext()) {
if (grContext->abandoned()) {
LOG_ALWAYS_FATAL("GrContext is abandoned/device lost at start of CanvasContext::draw");
- return 0;
+ return;
}
}
SkRect dirty;
mDamageAccumulator.finish(&dirty);
+ // reset syncDelayDuration each time we draw
+ nsecs_t syncDelayDuration = mSyncDelayDuration;
+ nsecs_t idleDuration = mIdleDuration;
+ mSyncDelayDuration = 0;
+ mIdleDuration = 0;
+
if (!Properties::isDrawingEnabled() ||
(dirty.isEmpty() && Properties::skipEmptyFrames && !surfaceRequiresRedraw())) {
mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
@@ -498,7 +563,7 @@ nsecs_t CanvasContext::draw() {
std::invoke(func, false /* didProduceBuffer */);
}
mFrameCommitCallbacks.clear();
- return 0;
+ return;
}
ScopedActiveContext activeContext(this);
@@ -507,7 +572,8 @@ nsecs_t CanvasContext::draw() {
mCurrentFrameInfo->markIssueDrawCommandsStart();
- Frame frame = mRenderPipeline->getFrame();
+ Frame frame = getFrame();
+
SkRect windowDirty = computeDirtyRect(frame, &dirty);
ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty));
@@ -523,7 +589,7 @@ nsecs_t CanvasContext::draw() {
std::scoped_lock lock(mFrameMetricsReporterMutex);
drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry,
&mLayerUpdateQueue, mContentDrawBounds, mOpaque,
- mLightInfo, mRenderNodes, &(profiler()));
+ mLightInfo, mRenderNodes, &(profiler()), mBufferParams);
}
uint64_t frameCompleteNr = getFrameNumber();
@@ -538,11 +604,14 @@ nsecs_t CanvasContext::draw() {
static_cast<int32_t>(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId));
native_window_set_frame_timeline_info(
mNativeSurface->getNativeWindow(), frameCompleteNr, vsyncId, inputEventId,
- mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime));
+ mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime),
+ solelyTextureViewUpdates);
}
}
bool requireSwap = false;
+ bool didDraw = false;
+
int error = OK;
bool didSwap = mRenderPipeline->swapBuffers(frame, drawResult.success, windowDirty,
mCurrentFrameInfo, &requireSwap);
@@ -553,7 +622,7 @@ nsecs_t CanvasContext::draw() {
mIsDirty = false;
if (requireSwap) {
- bool didDraw = true;
+ didDraw = true;
// Handle any swapchain errors
error = mNativeSurface->getAndClearError();
if (error == TIMED_OUT) {
@@ -648,23 +717,25 @@ nsecs_t CanvasContext::draw() {
}
}
- cleanupResources();
- mRenderThread.cacheManager().onFrameCompleted();
- return mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration);
-}
+ int64_t intendedVsync = mCurrentFrameInfo->get(FrameInfoIndex::IntendedVsync);
+ int64_t frameDeadline = mCurrentFrameInfo->get(FrameInfoIndex::FrameDeadline);
+ int64_t dequeueBufferDuration = mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration);
+
+ mHintSessionWrapper.updateTargetWorkDuration(frameDeadline - intendedVsync);
-void CanvasContext::cleanupResources() {
- auto& tracker = mJankTracker.frames();
- auto size = tracker.size();
- auto capacity = tracker.capacity();
- if (size == capacity) {
- nsecs_t nowNanos = systemTime(SYSTEM_TIME_MONOTONIC);
- nsecs_t frameCompleteNanos =
- tracker[0].get(FrameInfoIndex::FrameCompleted);
- nsecs_t frameDiffNanos = nowNanos - frameCompleteNanos;
- nsecs_t cleanupMillis = ns2ms(std::max(frameDiffNanos, 10_s));
- mRenderThread.cacheManager().performDeferredCleanup(cleanupMillis);
+ if (didDraw) {
+ int64_t frameStartTime = mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime);
+ int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime;
+ int64_t actualDuration = frameDuration -
+ (std::min(syncDelayDuration, mLastDequeueBufferDuration)) -
+ dequeueBufferDuration - idleDuration;
+ mHintSessionWrapper.reportActualWorkDuration(actualDuration);
}
+
+ mLastDequeueBufferDuration = dequeueBufferDuration;
+
+ mRenderThread.cacheManager().onFrameCompleted();
+ return;
}
void CanvasContext::reportMetricsWithPresentTime() {
@@ -777,6 +848,8 @@ void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceContro
// Called by choreographer to do an RT-driven animation
void CanvasContext::doFrame() {
if (!mRenderPipeline->isSurfaceReady()) return;
+ mIdleDuration =
+ systemTime(SYSTEM_TIME_MONOTONIC) - mRenderThread.timeLord().computeFrameTimeNanos();
prepareAndDraw(nullptr);
}
@@ -790,6 +863,7 @@ SkISize CanvasContext::getNextFrameSize() const {
SkISize size;
size.fWidth = ANativeWindow_getWidth(anw);
size.fHeight = ANativeWindow_getHeight(anw);
+ mRenderThread.cacheManager().notifyNextFrameSize(size.fWidth, size.fHeight);
return size;
}
@@ -812,7 +886,7 @@ void CanvasContext::prepareAndDraw(RenderNode* node) {
TreeInfo info(TreeInfo::MODE_RT_ONLY, *this);
prepareTree(info, frameInfo, systemTime(SYSTEM_TIME_MONOTONIC), node);
if (info.out.canDrawThisFrame) {
- draw();
+ draw(info.out.solelyTextureViewUpdates);
} else {
// wait on fences so tasks don't overlap next frame
waitOnFences();
@@ -872,18 +946,6 @@ void CanvasContext::destroyHardwareResources() {
}
}
-void CanvasContext::trimMemory(RenderThread& thread, int level) {
- ATRACE_CALL();
- if (!thread.getGrContext()) return;
- ATRACE_CALL();
- if (level >= TRIM_MEMORY_COMPLETE) {
- thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete);
- thread.destroyRenderingContext();
- } else if (level >= TRIM_MEMORY_UI_HIDDEN) {
- thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::UiHidden);
- }
-}
-
DeferredLayerUpdater* CanvasContext::createTextureLayer() {
return mRenderPipeline->createTextureLayer();
}
@@ -1000,6 +1062,28 @@ void CanvasContext::prepareSurfaceControlForWebview() {
}
}
+void CanvasContext::sendLoadResetHint() {
+ mHintSessionWrapper.sendLoadResetHint();
+}
+
+void CanvasContext::sendLoadIncreaseHint() {
+ mHintSessionWrapper.sendLoadIncreaseHint();
+}
+
+void CanvasContext::setSyncDelayDuration(nsecs_t duration) {
+ mSyncDelayDuration = duration;
+}
+
+void CanvasContext::startHintSession() {
+ mHintSessionWrapper.init();
+}
+
+bool CanvasContext::shouldDither() {
+ CanvasContext* self = getActiveContext();
+ if (!self) return false;
+ return self->mColorMode != ColorMode::Default;
+}
+
} /* namespace renderthread */
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index d85e579e371c..5219b5757008 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -16,10 +16,26 @@
#pragma once
+#include <SkBitmap.h>
+#include <SkRect.h>
+#include <SkSize.h>
+#include <cutils/compiler.h>
+#include <utils/Functor.h>
+#include <utils/Mutex.h>
+
+#include <functional>
+#include <future>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "ColorMode.h"
#include "DamageAccumulator.h"
#include "FrameInfo.h"
#include "FrameInfoVisualizer.h"
#include "FrameMetricsReporter.h"
+#include "HintSessionWrapper.h"
#include "IContextFactory.h"
#include "IRenderPipeline.h"
#include "JankTracker.h"
@@ -30,21 +46,6 @@
#include "renderthread/RenderTask.h"
#include "renderthread/RenderThread.h"
#include "utils/RingBuffer.h"
-#include "ColorMode.h"
-
-#include <SkBitmap.h>
-#include <SkRect.h>
-#include <SkSize.h>
-#include <cutils/compiler.h>
-#include <utils/Functor.h>
-#include <utils/Mutex.h>
-
-#include <functional>
-#include <future>
-#include <set>
-#include <string>
-#include <utility>
-#include <vector>
namespace android {
namespace uirenderer {
@@ -66,7 +67,8 @@ class Frame;
class CanvasContext : public IFrameCallback {
public:
static CanvasContext* create(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
- IContextFactory* contextFactory);
+ IContextFactory* contextFactory, pid_t uiThreadId,
+ pid_t renderThreadId);
virtual ~CanvasContext();
/**
@@ -123,21 +125,25 @@ public:
// Won't take effect until next EGLSurface creation
void setSwapBehavior(SwapBehavior swapBehavior);
+ void setHardwareBuffer(AHardwareBuffer* buffer);
void setSurface(ANativeWindow* window, bool enableTimeout = true);
void setSurfaceControl(ASurfaceControl* surfaceControl);
bool pauseSurface();
void setStopped(bool stopped);
- bool hasSurface() const { return mNativeSurface.get(); }
+ bool isStopped() { return mStopped || !hasOutputTarget(); }
+ bool hasOutputTarget() const { return mNativeSurface.get() || mHardwareBuffer; }
void allocateBuffers();
void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
void setLightGeometry(const Vector3& lightCenter, float lightRadius);
void setOpaque(bool opaque);
- void setColorMode(ColorMode mode);
+ float setColorMode(ColorMode mode);
+ float targetSdrHdrRatio() const;
+ void setTargetSdrHdrRatio(float ratio);
bool makeCurrent();
void prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued, RenderNode* target);
// Returns the DequeueBufferDuration.
- nsecs_t draw();
+ void draw(bool solelyTextureViewUpdates);
void destroy();
// IFrameCallback, Choreographer-driven frame callback entry point
@@ -148,7 +154,6 @@ public:
void markLayerInUse(RenderNode* node);
void destroyHardwareResources();
- static void trimMemory(RenderThread& thread, int level);
DeferredLayerUpdater* createTextureLayer();
@@ -207,6 +212,10 @@ public:
mASurfaceTransactionCallback = callback;
}
+ void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) {
+ mBufferParams = params;
+ }
+
bool mergeTransaction(ASurfaceTransaction* transaction, ASurfaceControl* control);
void setPrepareSurfaceControlForWebviewCallback(const std::function<void()>& callback) {
@@ -217,9 +226,20 @@ public:
static CanvasContext* getActiveContext();
+ void sendLoadResetHint();
+
+ void sendLoadIncreaseHint();
+
+ void setSyncDelayDuration(nsecs_t duration);
+
+ void startHintSession();
+
+ static bool shouldDither();
+
private:
CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
- IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline);
+ IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline,
+ pid_t uiThreadId, pid_t renderThreadId);
friend class RegisterFrameCallbackTask;
// TODO: Replace with something better for layer & other GL object
@@ -249,11 +269,16 @@ private:
FrameInfo* getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId);
+ Frame getFrame();
+
// The same type as Frame.mWidth and Frame.mHeight
int32_t mLastFrameWidth = 0;
int32_t mLastFrameHeight = 0;
RenderThread& mRenderThread;
+
+ AHardwareBuffer* mHardwareBuffer = nullptr;
+ HardwareBufferRenderParams mBufferParams;
std::unique_ptr<ReliableSurface> mNativeSurface;
// The SurfaceControl reference is passed from ViewRootImpl, can be set to
// NULL to remove the reference
@@ -334,7 +359,13 @@ private:
std::function<bool(int64_t, int64_t, int64_t)> mASurfaceTransactionCallback;
std::function<void()> mPrepareSurfaceControlForWebviewCallback;
- void cleanupResources();
+ HintSessionWrapper mHintSessionWrapper;
+ nsecs_t mLastDequeueBufferDuration = 0;
+ nsecs_t mSyncDelayDuration = 0;
+ nsecs_t mIdleDuration = 0;
+
+ ColorMode mColorMode = ColorMode::Default;
+ float mTargetSdrHdrRatio = 1.f;
};
} /* namespace renderthread */
diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp
index 3c75499391f6..53b43ba417d0 100644
--- a/libs/hwui/renderthread/DrawFrameTask.cpp
+++ b/libs/hwui/renderthread/DrawFrameTask.cpp
@@ -16,9 +16,9 @@
#include "DrawFrameTask.h"
-#include <dlfcn.h>
#include <gui/TraceUtils.h>
#include <utils/Log.h>
+
#include <algorithm>
#include "../DeferredLayerUpdater.h"
@@ -26,64 +26,13 @@
#include "../Properties.h"
#include "../RenderNode.h"
#include "CanvasContext.h"
+#include "HardwareBufferRenderParams.h"
#include "RenderThread.h"
-#include "thread/CommonPool.h"
namespace android {
namespace uirenderer {
namespace renderthread {
-namespace {
-
-typedef APerformanceHintManager* (*APH_getManager)();
-typedef APerformanceHintSession* (*APH_createSession)(APerformanceHintManager*, const int32_t*,
- size_t, int64_t);
-typedef void (*APH_updateTargetWorkDuration)(APerformanceHintSession*, int64_t);
-typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t);
-typedef void (*APH_closeSession)(APerformanceHintSession* session);
-
-bool gAPerformanceHintBindingInitialized = false;
-APH_getManager gAPH_getManagerFn = nullptr;
-APH_createSession gAPH_createSessionFn = nullptr;
-APH_updateTargetWorkDuration gAPH_updateTargetWorkDurationFn = nullptr;
-APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr;
-APH_closeSession gAPH_closeSessionFn = nullptr;
-
-void ensureAPerformanceHintBindingInitialized() {
- if (gAPerformanceHintBindingInitialized) return;
-
- void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
- LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!");
-
- gAPH_getManagerFn = (APH_getManager)dlsym(handle_, "APerformanceHint_getManager");
- LOG_ALWAYS_FATAL_IF(gAPH_getManagerFn == nullptr,
- "Failed to find required symbol APerformanceHint_getManager!");
-
- gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession");
- LOG_ALWAYS_FATAL_IF(gAPH_createSessionFn == nullptr,
- "Failed to find required symbol APerformanceHint_createSession!");
-
- gAPH_updateTargetWorkDurationFn = (APH_updateTargetWorkDuration)dlsym(
- handle_, "APerformanceHint_updateTargetWorkDuration");
- LOG_ALWAYS_FATAL_IF(
- gAPH_updateTargetWorkDurationFn == nullptr,
- "Failed to find required symbol APerformanceHint_updateTargetWorkDuration!");
-
- gAPH_reportActualWorkDurationFn = (APH_reportActualWorkDuration)dlsym(
- handle_, "APerformanceHint_reportActualWorkDuration");
- LOG_ALWAYS_FATAL_IF(
- gAPH_reportActualWorkDurationFn == nullptr,
- "Failed to find required symbol APerformanceHint_reportActualWorkDuration!");
-
- gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession");
- LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr,
- "Failed to find required symbol APerformanceHint_closeSession!");
-
- gAPerformanceHintBindingInitialized = true;
-}
-
-} // namespace
-
DrawFrameTask::DrawFrameTask()
: mRenderThread(nullptr)
, mContext(nullptr)
@@ -92,13 +41,11 @@ DrawFrameTask::DrawFrameTask()
DrawFrameTask::~DrawFrameTask() {}
-void DrawFrameTask::setContext(RenderThread* thread, CanvasContext* context, RenderNode* targetNode,
- int32_t uiThreadId, int32_t renderThreadId) {
+void DrawFrameTask::setContext(RenderThread* thread, CanvasContext* context,
+ RenderNode* targetNode) {
mRenderThread = thread;
mContext = context;
mTargetNode = targetNode;
- mUiThreadId = uiThreadId;
- mRenderThreadId = renderThreadId;
}
void DrawFrameTask::pushLayerUpdate(DeferredLayerUpdater* layer) {
@@ -142,16 +89,23 @@ void DrawFrameTask::postAndWait() {
void DrawFrameTask::run() {
const int64_t vsyncId = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameTimelineVsyncId)];
ATRACE_FORMAT("DrawFrames %" PRId64, vsyncId);
- nsecs_t syncDelayDuration = systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued;
+ mContext->setSyncDelayDuration(systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued);
+ mContext->setTargetSdrHdrRatio(mRenderSdrHdrRatio);
+
+ auto hardwareBufferParams = mHardwareBufferParams;
+ mContext->setHardwareBufferRenderParams(hardwareBufferParams);
+ IRenderPipeline* pipeline = mContext->getRenderPipeline();
bool canUnblockUiThread;
bool canDrawThisFrame;
+ bool solelyTextureViewUpdates;
{
TreeInfo info(TreeInfo::MODE_FULL, *mContext);
info.forceDrawFrame = mForceDrawFrame;
mForceDrawFrame = false;
canUnblockUiThread = syncFrameState(info);
canDrawThisFrame = info.out.canDrawThisFrame;
+ solelyTextureViewUpdates = info.out.solelyTextureViewUpdates;
if (mFrameCommitCallback) {
mContext->addFrameCommitListener(std::move(mFrameCommitCallback));
@@ -166,9 +120,6 @@ void DrawFrameTask::run() {
std::function<void()> frameCompleteCallback = std::move(mFrameCompleteCallback);
mFrameCallback = nullptr;
mFrameCompleteCallback = nullptr;
- int64_t intendedVsync = mFrameInfo[static_cast<int>(FrameInfoIndex::IntendedVsync)];
- int64_t frameDeadline = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameDeadline)];
- int64_t frameStartTime = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameStartTime)];
// From this point on anything in "this" is *UNSAFE TO ACCESS*
if (canUnblockUiThread) {
@@ -179,16 +130,15 @@ void DrawFrameTask::run() {
if (CC_UNLIKELY(frameCallback)) {
context->enqueueFrameWork([frameCallback, context, syncResult = mSyncResult,
frameNr = context->getFrameNumber()]() {
- auto frameCommitCallback = std::move(frameCallback(syncResult, frameNr));
+ auto frameCommitCallback = frameCallback(syncResult, frameNr);
if (frameCommitCallback) {
context->addFrameCommitListener(std::move(frameCommitCallback));
}
});
}
- nsecs_t dequeueBufferDuration = 0;
if (CC_LIKELY(canDrawThisFrame)) {
- dequeueBufferDuration = context->draw();
+ context->draw(solelyTextureViewUpdates);
} else {
// Do a flush in case syncFrameState performed any texture uploads. Since we skipped
// the draw() call, those uploads (or deletes) will end up sitting in the queue.
@@ -208,26 +158,10 @@ void DrawFrameTask::run() {
unblockUiThread();
}
- if (!mHintSessionWrapper) mHintSessionWrapper.emplace(mUiThreadId, mRenderThreadId);
- constexpr int64_t kSanityCheckLowerBound = 100000; // 0.1ms
- constexpr int64_t kSanityCheckUpperBound = 10000000000; // 10s
- int64_t targetWorkDuration = frameDeadline - intendedVsync;
- targetWorkDuration = targetWorkDuration * Properties::targetCpuTimePercentage / 100;
- if (targetWorkDuration > kSanityCheckLowerBound &&
- targetWorkDuration < kSanityCheckUpperBound &&
- targetWorkDuration != mLastTargetWorkDuration) {
- mLastTargetWorkDuration = targetWorkDuration;
- mHintSessionWrapper->updateTargetWorkDuration(targetWorkDuration);
+ if (pipeline->hasHardwareBuffer()) {
+ auto fence = pipeline->flush();
+ hardwareBufferParams.invokeRenderCallback(std::move(fence), 0);
}
- int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime;
- int64_t actualDuration = frameDuration -
- (std::min(syncDelayDuration, mLastDequeueBufferDuration)) -
- dequeueBufferDuration;
- if (actualDuration > kSanityCheckLowerBound && actualDuration < kSanityCheckUpperBound) {
- mHintSessionWrapper->reportActualWorkDuration(actualDuration);
- }
-
- mLastDequeueBufferDuration = dequeueBufferDuration;
}
bool DrawFrameTask::syncFrameState(TreeInfo& info) {
@@ -247,14 +181,16 @@ bool DrawFrameTask::syncFrameState(TreeInfo& info) {
mLayers[i]->apply();
}
}
+
mLayers.clear();
mContext->setContentDrawBounds(mContentDrawBounds);
mContext->prepareTree(info, mFrameInfo, mSyncQueued, mTargetNode);
// This is after the prepareTree so that any pending operations
// (RenderNode tree state, prefetched layers, etc...) will be flushed.
- if (CC_UNLIKELY(!mContext->hasSurface() || !canDraw)) {
- if (!mContext->hasSurface()) {
+ bool hasTarget = mContext->hasOutputTarget();
+ if (CC_UNLIKELY(!hasTarget || !canDraw)) {
+ if (!hasTarget) {
mSyncResult |= SyncResult::LostSurfaceRewardIfFound;
} else {
// If we have a surface but can't draw we must be stopped
@@ -280,49 +216,6 @@ void DrawFrameTask::unblockUiThread() {
mSignal.signal();
}
-void DrawFrameTask::createHintSession(pid_t uiThreadId, pid_t renderThreadId) {
- if (mHintSessionWrapper) return;
- mHintSessionWrapper.emplace(uiThreadId, renderThreadId);
-}
-
-DrawFrameTask::HintSessionWrapper::HintSessionWrapper(int32_t uiThreadId, int32_t renderThreadId) {
- if (!Properties::useHintManager) return;
- if (uiThreadId < 0 || renderThreadId < 0) return;
-
- ensureAPerformanceHintBindingInitialized();
-
- APerformanceHintManager* manager = gAPH_getManagerFn();
- if (!manager) return;
-
- std::vector<int32_t> tids = CommonPool::getThreadIds();
- tids.push_back(uiThreadId);
- tids.push_back(renderThreadId);
-
- // DrawFrameTask code will always set a target duration before reporting actual durations.
- // So this is just a placeholder value that's never used.
- int64_t dummyTargetDurationNanos = 16666667;
- mHintSession =
- gAPH_createSessionFn(manager, tids.data(), tids.size(), dummyTargetDurationNanos);
-}
-
-DrawFrameTask::HintSessionWrapper::~HintSessionWrapper() {
- if (mHintSession) {
- gAPH_closeSessionFn(mHintSession);
- }
-}
-
-void DrawFrameTask::HintSessionWrapper::updateTargetWorkDuration(long targetDurationNanos) {
- if (mHintSession) {
- gAPH_updateTargetWorkDurationFn(mHintSession, targetDurationNanos);
- }
-}
-
-void DrawFrameTask::HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) {
- if (mHintSession) {
- gAPH_reportActualWorkDurationFn(mHintSession, actualDurationNanos);
- }
-}
-
} /* namespace renderthread */
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/renderthread/DrawFrameTask.h b/libs/hwui/renderthread/DrawFrameTask.h
index b135a215d180..4130d4abe09e 100644
--- a/libs/hwui/renderthread/DrawFrameTask.h
+++ b/libs/hwui/renderthread/DrawFrameTask.h
@@ -16,7 +16,6 @@
#ifndef DRAWFRAMETASK_H
#define DRAWFRAMETASK_H
-#include <android/performance_hint.h>
#include <utils/Condition.h>
#include <utils/Mutex.h>
#include <utils/StrongPointer.h>
@@ -28,8 +27,16 @@
#include "../Rect.h"
#include "../TreeInfo.h"
#include "RenderTask.h"
+#include "SkColorSpace.h"
+#include "SwapBehavior.h"
+#include "utils/TimeUtils.h"
+#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
+#include <android/hardware_buffer.h>
+#endif
+#include "HardwareBufferRenderParams.h"
namespace android {
+
namespace uirenderer {
class DeferredLayerUpdater;
@@ -61,8 +68,7 @@ public:
DrawFrameTask();
virtual ~DrawFrameTask();
- void setContext(RenderThread* thread, CanvasContext* context, RenderNode* targetNode,
- int32_t uiThreadId, int32_t renderThreadId);
+ void setContext(RenderThread* thread, CanvasContext* context, RenderNode* targetNode);
void setContentDrawBounds(int left, int top, int right, int bottom) {
mContentDrawBounds.set(left, top, right, bottom);
}
@@ -90,21 +96,13 @@ public:
void forceDrawNextFrame() { mForceDrawFrame = true; }
- void createHintSession(pid_t uiThreadId, pid_t renderThreadId);
-
-private:
- class HintSessionWrapper {
- public:
- HintSessionWrapper(int32_t uiThreadId, int32_t renderThreadId);
- ~HintSessionWrapper();
-
- void updateTargetWorkDuration(long targetDurationNanos);
- void reportActualWorkDuration(long actualDurationNanos);
+ void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) {
+ mHardwareBufferParams = params;
+ }
- private:
- APerformanceHintSession* mHintSession = nullptr;
- };
+ void setRenderSdrHdrRatio(float ratio) { mRenderSdrHdrRatio = ratio; }
+private:
void postAndWait();
bool syncFrameState(TreeInfo& info);
void unblockUiThread();
@@ -115,9 +113,8 @@ private:
RenderThread* mRenderThread;
CanvasContext* mContext;
RenderNode* mTargetNode = nullptr;
- int32_t mUiThreadId = -1;
- int32_t mRenderThreadId = -1;
Rect mContentDrawBounds;
+ float mRenderSdrHdrRatio = 1.f;
/*********************************************
* Single frame data
@@ -129,14 +126,11 @@ private:
int64_t mFrameInfo[UI_THREAD_FRAME_INFO_SIZE];
+ HardwareBufferRenderParams mHardwareBufferParams;
std::function<std::function<void(bool)>(int32_t, int64_t)> mFrameCallback;
std::function<void(bool)> mFrameCommitCallback;
std::function<void()> mFrameCompleteCallback;
- nsecs_t mLastDequeueBufferDuration = 0;
- nsecs_t mLastTargetWorkDuration = 0;
- std::optional<HintSessionWrapper> mHintSessionWrapper;
-
bool mForceDrawFrame = false;
};
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 02257db9df6a..94f35fd9eaf2 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -423,6 +423,7 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window,
EGLint attribs[] = {EGL_NONE, EGL_NONE, EGL_NONE};
EGLConfig config = mEglConfig;
+ bool overrideWindowDataSpaceForHdr = false;
if (colorMode == ColorMode::A8) {
// A8 doesn't use a color space
if (!mEglConfigA8) {
@@ -450,6 +451,13 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window,
case ColorMode::Default:
attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR;
break;
+ // We don't have an EGL colorspace for extended range P3 that's used for HDR
+ // So override it after configuring the EGL context
+ case ColorMode::Hdr:
+ case ColorMode::Hdr10:
+ overrideWindowDataSpaceForHdr = true;
+ attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT;
+ break;
case ColorMode::WideColorGamut: {
skcms_Matrix3x3 colorGamut;
LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut),
@@ -466,14 +474,6 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window,
}
break;
}
- case ColorMode::Hdr:
- config = mEglConfigF16;
- attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
- break;
- case ColorMode::Hdr10:
- config = mEglConfig1010102;
- attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
- break;
case ColorMode::A8:
LOG_ALWAYS_FATAL("Unreachable: A8 doesn't use a color space");
break;
@@ -493,6 +493,16 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window,
(void*)window, eglErrorString());
}
+ if (overrideWindowDataSpaceForHdr) {
+ // This relies on knowing that EGL will not re-set the dataspace after the call to
+ // eglCreateWindowSurface. Since the handling of the colorspace extension is largely
+ // implemented in libEGL in the platform, we can safely assume this is the case
+ int32_t err = ANativeWindow_setBuffersDataSpace(
+ window,
+ static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED));
+ LOG_ALWAYS_FATAL_IF(err, "Failed to ANativeWindow_setBuffersDataSpace %d", err);
+ }
+
return surface;
}
diff --git a/libs/hwui/renderthread/EglManager.h b/libs/hwui/renderthread/EglManager.h
index fc6b28d2e1ad..b8f8c9267ad8 100644
--- a/libs/hwui/renderthread/EglManager.h
+++ b/libs/hwui/renderthread/EglManager.h
@@ -18,6 +18,7 @@
#include <EGL/egl.h>
#include <EGL/eglext.h>
+#include <SkColorSpace.h>
#include <SkImageInfo.h>
#include <SkRect.h>
#include <cutils/compiler.h>
diff --git a/libs/hwui/renderthread/HardwareBufferRenderParams.h b/libs/hwui/renderthread/HardwareBufferRenderParams.h
new file mode 100644
index 000000000000..8c942d0fa102
--- /dev/null
+++ b/libs/hwui/renderthread/HardwareBufferRenderParams.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+#ifndef HARDWAREBUFFERRENDERER_H_
+#define HARDWAREBUFFERRENDERER_H_
+
+#include <android-base/unique_fd.h>
+#include <android/hardware_buffer.h>
+
+#include "SkColorSpace.h"
+#include "SkMatrix.h"
+#include "SkSurface.h"
+
+namespace android {
+namespace uirenderer {
+namespace renderthread {
+
+using namespace android::uirenderer::renderthread;
+
+using RenderCallback = std::function<void(android::base::unique_fd&&, int)>;
+
+class RenderProxy;
+
+class HardwareBufferRenderParams {
+public:
+ HardwareBufferRenderParams() = default;
+ HardwareBufferRenderParams(int32_t logicalWidth, int32_t logicalHeight,
+ const SkMatrix& transform, const sk_sp<SkColorSpace>& colorSpace,
+ RenderCallback&& callback)
+ : mLogicalWidth(logicalWidth)
+ , mLogicalHeight(logicalHeight)
+ , mTransform(transform)
+ , mColorSpace(colorSpace)
+ , mRenderCallback(std::move(callback)) {}
+ const SkMatrix& getTransform() const { return mTransform; }
+ sk_sp<SkColorSpace> getColorSpace() const { return mColorSpace; }
+
+ void invokeRenderCallback(android::base::unique_fd&& fenceFd, int status) {
+ if (mRenderCallback) {
+ std::invoke(mRenderCallback, std::move(fenceFd), status);
+ }
+ }
+
+ int32_t getLogicalWidth() { return mLogicalWidth; }
+ int32_t getLogicalHeight() { return mLogicalHeight; }
+
+private:
+ int32_t mLogicalWidth;
+ int32_t mLogicalHeight;
+ SkMatrix mTransform = SkMatrix::I();
+ sk_sp<SkColorSpace> mColorSpace = SkColorSpace::MakeSRGB();
+ RenderCallback mRenderCallback = nullptr;
+};
+
+} // namespace renderthread
+} // namespace uirenderer
+} // namespace android
+#endif // HARDWAREBUFFERRENDERER_H_
diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp
new file mode 100644
index 000000000000..814ac4d90028
--- /dev/null
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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.
+ */
+
+#include "HintSessionWrapper.h"
+
+#include <dlfcn.h>
+#include <private/performance_hint_private.h>
+#include <utils/Log.h>
+
+#include <chrono>
+#include <vector>
+
+#include "../Properties.h"
+#include "thread/CommonPool.h"
+
+using namespace std::chrono_literals;
+
+namespace android {
+namespace uirenderer {
+namespace renderthread {
+
+namespace {
+
+typedef APerformanceHintManager* (*APH_getManager)();
+typedef APerformanceHintSession* (*APH_createSession)(APerformanceHintManager*, const int32_t*,
+ size_t, int64_t);
+typedef void (*APH_closeSession)(APerformanceHintSession* session);
+typedef void (*APH_updateTargetWorkDuration)(APerformanceHintSession*, int64_t);
+typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t);
+typedef void (*APH_sendHint)(APerformanceHintSession* session, int32_t);
+
+bool gAPerformanceHintBindingInitialized = false;
+APH_getManager gAPH_getManagerFn = nullptr;
+APH_createSession gAPH_createSessionFn = nullptr;
+APH_closeSession gAPH_closeSessionFn = nullptr;
+APH_updateTargetWorkDuration gAPH_updateTargetWorkDurationFn = nullptr;
+APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr;
+APH_sendHint gAPH_sendHintFn = nullptr;
+
+void ensureAPerformanceHintBindingInitialized() {
+ if (gAPerformanceHintBindingInitialized) return;
+
+ void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
+ LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!");
+
+ gAPH_getManagerFn = (APH_getManager)dlsym(handle_, "APerformanceHint_getManager");
+ LOG_ALWAYS_FATAL_IF(gAPH_getManagerFn == nullptr,
+ "Failed to find required symbol APerformanceHint_getManager!");
+
+ gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession");
+ LOG_ALWAYS_FATAL_IF(gAPH_createSessionFn == nullptr,
+ "Failed to find required symbol APerformanceHint_createSession!");
+
+ gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession");
+ LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr,
+ "Failed to find required symbol APerformanceHint_closeSession!");
+
+ gAPH_updateTargetWorkDurationFn = (APH_updateTargetWorkDuration)dlsym(
+ handle_, "APerformanceHint_updateTargetWorkDuration");
+ LOG_ALWAYS_FATAL_IF(
+ gAPH_updateTargetWorkDurationFn == nullptr,
+ "Failed to find required symbol APerformanceHint_updateTargetWorkDuration!");
+
+ gAPH_reportActualWorkDurationFn = (APH_reportActualWorkDuration)dlsym(
+ handle_, "APerformanceHint_reportActualWorkDuration");
+ LOG_ALWAYS_FATAL_IF(
+ gAPH_reportActualWorkDurationFn == nullptr,
+ "Failed to find required symbol APerformanceHint_reportActualWorkDuration!");
+
+ gAPH_sendHintFn = (APH_sendHint)dlsym(handle_, "APerformanceHint_sendHint");
+ LOG_ALWAYS_FATAL_IF(gAPH_sendHintFn == nullptr,
+ "Failed to find required symbol APerformanceHint_sendHint!");
+
+ gAPerformanceHintBindingInitialized = true;
+}
+
+} // namespace
+
+HintSessionWrapper::HintSessionWrapper(pid_t uiThreadId, pid_t renderThreadId)
+ : mUiThreadId(uiThreadId), mRenderThreadId(renderThreadId) {}
+
+HintSessionWrapper::~HintSessionWrapper() {
+ if (mHintSession) {
+ gAPH_closeSessionFn(mHintSession);
+ }
+}
+
+bool HintSessionWrapper::init() {
+ if (mHintSession != nullptr) return true;
+
+ // If we're waiting for the session
+ if (mHintSessionFuture.valid()) {
+ // If the session is here
+ if (mHintSessionFuture.wait_for(0s) == std::future_status::ready) {
+ mHintSession = mHintSessionFuture.get();
+ if (mHintSession != nullptr) {
+ mSessionValid = true;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // If it broke last time we tried this, shouldn't be running, or
+ // has bad argument values, don't even bother
+ if (!mSessionValid || !Properties::useHintManager || !Properties::isDrawingEnabled() ||
+ mUiThreadId < 0 || mRenderThreadId < 0) {
+ return false;
+ }
+
+ // Assume that if we return before the end, it broke
+ mSessionValid = false;
+
+ ensureAPerformanceHintBindingInitialized();
+
+ APerformanceHintManager* manager = gAPH_getManagerFn();
+ if (!manager) return false;
+
+ std::vector<pid_t> tids = CommonPool::getThreadIds();
+ tids.push_back(mUiThreadId);
+ tids.push_back(mRenderThreadId);
+
+ // Use a placeholder target value to initialize,
+ // this will always be replaced elsewhere before it gets used
+ int64_t defaultTargetDurationNanos = 16666667;
+ mHintSessionFuture = CommonPool::async([=, tids = std::move(tids)] {
+ return gAPH_createSessionFn(manager, tids.data(), tids.size(), defaultTargetDurationNanos);
+ });
+ return false;
+}
+
+void HintSessionWrapper::updateTargetWorkDuration(long targetWorkDurationNanos) {
+ if (!init()) return;
+ targetWorkDurationNanos = targetWorkDurationNanos * Properties::targetCpuTimePercentage / 100;
+ if (targetWorkDurationNanos != mLastTargetWorkDuration &&
+ targetWorkDurationNanos > kSanityCheckLowerBound &&
+ targetWorkDurationNanos < kSanityCheckUpperBound) {
+ mLastTargetWorkDuration = targetWorkDurationNanos;
+ gAPH_updateTargetWorkDurationFn(mHintSession, targetWorkDurationNanos);
+ }
+ mLastFrameNotification = systemTime();
+}
+
+void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) {
+ if (!init()) return;
+ mResetsSinceLastReport = 0;
+ if (actualDurationNanos > kSanityCheckLowerBound &&
+ actualDurationNanos < kSanityCheckUpperBound) {
+ gAPH_reportActualWorkDurationFn(mHintSession, actualDurationNanos);
+ }
+}
+
+void HintSessionWrapper::sendLoadResetHint() {
+ static constexpr int kMaxResetsSinceLastReport = 2;
+ if (!init()) return;
+ nsecs_t now = systemTime();
+ if (now - mLastFrameNotification > kResetHintTimeout &&
+ mResetsSinceLastReport <= kMaxResetsSinceLastReport) {
+ ++mResetsSinceLastReport;
+ gAPH_sendHintFn(mHintSession, static_cast<int>(SessionHint::CPU_LOAD_RESET));
+ }
+ mLastFrameNotification = now;
+}
+
+void HintSessionWrapper::sendLoadIncreaseHint() {
+ if (!init()) return;
+ gAPH_sendHintFn(mHintSession, static_cast<int>(SessionHint::CPU_LOAD_UP));
+}
+
+} /* namespace renderthread */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h
new file mode 100644
index 000000000000..24b8150dd489
--- /dev/null
+++ b/libs/hwui/renderthread/HintSessionWrapper.h
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <android/performance_hint.h>
+
+#include <future>
+
+#include "utils/TimeUtils.h"
+
+namespace android {
+namespace uirenderer {
+
+namespace renderthread {
+
+class HintSessionWrapper {
+public:
+ HintSessionWrapper(pid_t uiThreadId, pid_t renderThreadId);
+ ~HintSessionWrapper();
+
+ void updateTargetWorkDuration(long targetDurationNanos);
+ void reportActualWorkDuration(long actualDurationNanos);
+ void sendLoadResetHint();
+ void sendLoadIncreaseHint();
+ bool init();
+
+private:
+ APerformanceHintSession* mHintSession = nullptr;
+ std::future<APerformanceHintSession*> mHintSessionFuture;
+
+ int mResetsSinceLastReport = 0;
+ nsecs_t mLastFrameNotification = 0;
+ nsecs_t mLastTargetWorkDuration = 0;
+
+ pid_t mUiThreadId;
+ pid_t mRenderThreadId;
+
+ bool mSessionValid = true;
+
+ static constexpr nsecs_t kResetHintTimeout = 100_ms;
+ static constexpr int64_t kSanityCheckLowerBound = 100_us;
+ static constexpr int64_t kSanityCheckUpperBound = 10_s;
+};
+
+} /* namespace renderthread */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h
index 54adec2cf6f6..6c2cb9d71c55 100644
--- a/libs/hwui/renderthread/IRenderPipeline.h
+++ b/libs/hwui/renderthread/IRenderPipeline.h
@@ -16,16 +16,19 @@
#pragma once
+#include <SkColorSpace.h>
+#include <SkRect.h>
+#include <android-base/unique_fd.h>
+#include <utils/RefBase.h>
+
+#include "ColorMode.h"
#include "DamageAccumulator.h"
#include "FrameInfoVisualizer.h"
+#include "HardwareBufferRenderParams.h"
#include "LayerUpdateQueue.h"
#include "Lighting.h"
#include "SwapBehavior.h"
#include "hwui/Bitmap.h"
-#include "ColorMode.h"
-
-#include <SkRect.h>
-#include <utils/RefBase.h>
class GrDirectContext;
@@ -63,10 +66,14 @@ public:
const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
const std::vector<sp<RenderNode>>& renderNodes,
- FrameInfoVisualizer* profiler) = 0;
+ FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams) = 0;
virtual bool swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) = 0;
virtual DeferredLayerUpdater* createTextureLayer() = 0;
+ [[nodiscard]] virtual android::base::unique_fd flush() = 0;
+ virtual void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) = 0;
+ virtual bool hasHardwareBuffer() = 0;
virtual bool setSurface(ANativeWindow* window, SwapBehavior swapBehavior) = 0;
virtual void onStop() = 0;
virtual bool isSurfaceReady() = 0;
@@ -88,6 +95,7 @@ public:
virtual void setPictureCapturedCallback(
const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0;
+ virtual void setTargetSdrHdrRatio(float ratio) = 0;
virtual const SkM44& getPixelSnapMatrix() const = 0;
virtual ~IRenderPipeline() {}
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index d9b650c1ff37..224c878bf43d 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -29,6 +29,10 @@
#include "utils/Macros.h"
#include "utils/TimeUtils.h"
+#include <SkBitmap.h>
+#include <SkImage.h>
+#include <SkPicture.h>
+
#include <pthread.h>
namespace android {
@@ -41,15 +45,14 @@ RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode,
pid_t uiThreadId = pthread_gettid_np(pthread_self());
pid_t renderThreadId = getRenderThreadTid();
mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* {
- CanvasContext* context =
- CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
+ CanvasContext* context = CanvasContext::create(mRenderThread, translucent, rootRenderNode,
+ contextFactory, uiThreadId, renderThreadId);
if (context != nullptr) {
- mRenderThread.queue().post(
- [=] { mDrawFrameTask.createHintSession(uiThreadId, renderThreadId); });
+ mRenderThread.queue().post([=] { context->startHintSession(); });
}
return context;
});
- mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode, uiThreadId, renderThreadId);
+ mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode);
}
RenderProxy::~RenderProxy() {
@@ -58,7 +61,7 @@ RenderProxy::~RenderProxy() {
void RenderProxy::destroyContext() {
if (mContext) {
- mDrawFrameTask.setContext(nullptr, nullptr, nullptr, -1, -1);
+ mDrawFrameTask.setContext(nullptr, nullptr, nullptr);
// This is also a fence as we need to be certain that there are no
// outstanding mDrawFrame tasks posted before it is destroyed
mRenderThread.queue().runSync([this]() { delete mContext; });
@@ -86,6 +89,18 @@ void RenderProxy::setName(const char* name) {
mRenderThread.queue().runSync([this, name]() { mContext->setName(std::string(name)); });
}
+void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ }
+ mRenderThread.queue().post([this, hardwareBuffer = buffer]() mutable {
+ mContext->setHardwareBuffer(hardwareBuffer);
+ if (hardwareBuffer) {
+ AHardwareBuffer_release(hardwareBuffer);
+ }
+ });
+}
+
void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) {
if (window) { ANativeWindow_acquire(window); }
mRenderThread.queue().post([this, win = window, enableTimeout]() mutable {
@@ -132,8 +147,20 @@ void RenderProxy::setOpaque(bool opaque) {
mRenderThread.queue().post([=]() { mContext->setOpaque(opaque); });
}
-void RenderProxy::setColorMode(ColorMode mode) {
- mRenderThread.queue().post([=]() { mContext->setColorMode(mode); });
+float RenderProxy::setColorMode(ColorMode mode) {
+ // We only need to figure out what the renderer supports for HDR, otherwise this can stay
+ // 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); });
+ } else {
+ mRenderThread.queue().post([=]() { mContext->setColorMode(mode); });
+ return 1.f;
+ }
+}
+
+void RenderProxy::setRenderSdrHdrRatio(float ratio) {
+ mDrawFrameTask.setRenderSdrHdrRatio(ratio);
}
int64_t* RenderProxy::frameInfo() {
@@ -199,7 +226,17 @@ void RenderProxy::trimMemory(int level) {
// Avoid creating a RenderThread to do a trimMemory.
if (RenderThread::hasInstance()) {
RenderThread& thread = RenderThread::getInstance();
- thread.queue().post([&thread, level]() { CanvasContext::trimMemory(thread, level); });
+ const auto trimLevel = static_cast<TrimLevel>(level);
+ thread.queue().post([&thread, trimLevel]() { thread.trimMemory(trimLevel); });
+ }
+}
+
+void RenderProxy::trimCaches(int level) {
+ // Avoid creating a RenderThread to do a trimMemory.
+ if (RenderThread::hasInstance()) {
+ RenderThread& thread = RenderThread::getInstance();
+ const auto trimLevel = static_cast<CacheTrimLevel>(level);
+ thread.queue().post([&thread, trimLevel]() { thread.trimCaches(trimLevel); });
}
}
@@ -208,7 +245,7 @@ void RenderProxy::purgeCaches() {
RenderThread& thread = RenderThread::getInstance();
thread.queue().post([&thread]() {
if (thread.getGrContext()) {
- thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete);
+ thread.cacheManager().trimMemory(TrimLevel::COMPLETE);
}
});
}
@@ -238,6 +275,14 @@ void RenderProxy::notifyFramePending() {
mRenderThread.queue().post([this]() { mContext->notifyFramePending(); });
}
+void RenderProxy::notifyCallbackPending() {
+ mRenderThread.queue().post([this]() { mContext->sendLoadResetHint(); });
+}
+
+void RenderProxy::notifyExpensiveFrame() {
+ mRenderThread.queue().post([this]() { mContext->sendLoadIncreaseHint(); });
+}
+
void RenderProxy::dumpProfileInfo(int fd, int dumpFlags) {
mRenderThread.queue().runSync([&]() {
std::lock_guard lock(mRenderThread.getJankDataMutex());
@@ -320,6 +365,10 @@ void RenderProxy::setContentDrawBounds(int left, int top, int right, int bottom)
mDrawFrameTask.setContentDrawBounds(left, top, right, bottom);
}
+void RenderProxy::setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) {
+ mDrawFrameTask.setHardwareBufferRenderParams(params);
+}
+
void RenderProxy::setPictureCapturedCallback(
const std::function<void(sk_sp<SkPicture>&&)>& callback) {
mRenderThread.queue().post(
@@ -367,12 +416,13 @@ void RenderProxy::setForceDark(bool enable) {
mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
}
-int RenderProxy::copySurfaceInto(ANativeWindow* window, int left, int top, int right, int bottom,
- SkBitmap* bitmap) {
+void RenderProxy::copySurfaceInto(ANativeWindow* window, std::shared_ptr<CopyRequest>&& request) {
auto& thread = RenderThread::getInstance();
- return static_cast<int>(thread.queue().runSync([&]() -> auto {
- return thread.readback().copySurfaceInto(window, Rect(left, top, right, bottom), bitmap);
- }));
+ ANativeWindow_acquire(window);
+ thread.queue().post([&thread, window, request = std::move(request)] {
+ thread.readback().copySurfaceInto(window, request);
+ ANativeWindow_release(window);
+ });
}
void RenderProxy::prepareToDraw(Bitmap& bitmap) {
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index ee9efd46e307..47c1b0cd28e5 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -17,19 +17,25 @@
#ifndef RENDERPROXY_H_
#define RENDERPROXY_H_
-#include <SkBitmap.h>
+#include <SkRefCnt.h>
+#include <android/hardware_buffer.h>
#include <android/native_window.h>
-#include <cutils/compiler.h>
#include <android/surface_control.h>
+#include <cutils/compiler.h>
#include <utils/Functor.h>
#include "../FrameMetricsObserver.h"
#include "../IContextFactory.h"
#include "ColorMode.h"
+#include "CopyRequest.h"
#include "DrawFrameTask.h"
#include "SwapBehavior.h"
#include "hwui/Bitmap.h"
+class SkBitmap;
+class SkPicture;
+class SkImage;
+
namespace android {
class GraphicBuffer;
class Surface;
@@ -71,7 +77,7 @@ public:
void setSwapBehavior(SwapBehavior swapBehavior);
bool loadSystemProperties();
void setName(const char* name);
-
+ void setHardwareBuffer(AHardwareBuffer* buffer);
void setSurface(ANativeWindow* window, bool enableTimeout = true);
void setSurfaceControl(ASurfaceControl* surfaceControl);
void allocateBuffers();
@@ -79,8 +85,10 @@ public:
void setStopped(bool stopped);
void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
void setLightGeometry(const Vector3& lightCenter, float lightRadius);
+ void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params);
void setOpaque(bool opaque);
- void setColorMode(ColorMode mode);
+ float setColorMode(ColorMode mode);
+ void setRenderSdrHdrRatio(float ratio);
int64_t* frameInfo();
void forceDrawNextFrame();
int syncAndDrawFrame();
@@ -97,6 +105,7 @@ public:
void destroyHardwareResources();
static void trimMemory(int level);
+ static void trimCaches(int level);
static void purgeCaches();
static void overrideProperty(const char* name, const char* value);
@@ -104,6 +113,8 @@ public:
static int maxTextureSize();
void stopDrawing();
void notifyFramePending();
+ void notifyCallbackPending();
+ void notifyExpensiveFrame();
void dumpProfileInfo(int fd, int dumpFlags);
// Not exported, only used for testing
@@ -133,8 +144,7 @@ public:
void removeFrameMetricsObserver(FrameMetricsObserver* observer);
void setForceDark(bool enable);
- static int copySurfaceInto(ANativeWindow* window, int left, int top, int right,
- int bottom, SkBitmap* bitmap);
+ static void copySurfaceInto(ANativeWindow* window, std::shared_ptr<CopyRequest>&& request);
static void prepareToDraw(Bitmap& bitmap);
static int copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap);
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 7952e70aeab6..0dea941c91f9 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -133,10 +133,23 @@ void RenderThread::frameCallback(int64_t vsyncId, int64_t frameDeadline, int64_t
if (timeLord().vsyncReceived(frameTimeNanos, frameTimeNanos, vsyncId, frameDeadline,
frameInterval) &&
!mFrameCallbackTaskPending) {
- ATRACE_NAME("queue mFrameCallbackTask");
mFrameCallbackTaskPending = true;
- nsecs_t runAt = (frameTimeNanos + mDispatchFrameDelay);
- queue().postAt(runAt, [=]() { dispatchFrameCallbacks(); });
+
+ using SteadyClock = std::chrono::steady_clock;
+ using Nanos = std::chrono::nanoseconds;
+ using toNsecs_t = std::chrono::duration<nsecs_t, std::nano>;
+ using toFloatMillis = std::chrono::duration<float, std::milli>;
+
+ const auto frameTimeTimePoint = SteadyClock::time_point(Nanos(frameTimeNanos));
+ const auto deadlineTimePoint = SteadyClock::time_point(Nanos(frameDeadline));
+
+ const auto timeUntilDeadline = deadlineTimePoint - frameTimeTimePoint;
+ const auto runAt = (frameTimeTimePoint + (timeUntilDeadline / 4));
+
+ ATRACE_FORMAT("queue mFrameCallbackTask to run after %.2fms",
+ toFloatMillis(runAt - SteadyClock::now()).count());
+ queue().postAt(toNsecs_t(runAt.time_since_epoch()).count(),
+ [=]() { dispatchFrameCallbacks(); });
}
}
@@ -251,13 +264,12 @@ void RenderThread::initThreadLocals() {
mEglManager = new EglManager();
mRenderState = new RenderState(*this);
mVkManager = VulkanManager::getInstance();
- mCacheManager = new CacheManager();
+ mCacheManager = new CacheManager(*this);
}
void RenderThread::setupFrameInterval() {
nsecs_t frameIntervalNanos = DeviceInfo::getVsyncPeriod();
mTimeLord.setFrameInterval(frameIntervalNanos);
- mDispatchFrameDelay = static_cast<nsecs_t>(frameIntervalNanos * .25f);
}
void RenderThread::requireGlContext() {
@@ -266,7 +278,7 @@ void RenderThread::requireGlContext() {
}
mEglManager->initialize();
- sk_sp<const GrGLInterface> glInterface(GrGLCreateNativeInterface());
+ sk_sp<const GrGLInterface> glInterface = GrGLMakeNativeInterface();
LOG_ALWAYS_FATAL_IF(!glInterface.get());
GrContextOptions options;
@@ -453,6 +465,8 @@ bool RenderThread::threadLoop() {
// next vsync (oops), so none of the callbacks are run.
requestVsync();
}
+
+ mCacheManager->onThreadIdle();
}
return false;
@@ -502,6 +516,16 @@ void RenderThread::preload() {
HardwareBitmapUploader::initialize();
}
+void RenderThread::trimMemory(TrimLevel level) {
+ ATRACE_CALL();
+ cacheManager().trimMemory(level);
+}
+
+void RenderThread::trimCaches(CacheTrimLevel level) {
+ ATRACE_CALL();
+ cacheManager().trimCaches(level);
+}
+
} /* namespace renderthread */
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index c1f6790b25b2..79e57de9d66f 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -17,11 +17,11 @@
#ifndef RENDERTHREAD_H_
#define RENDERTHREAD_H_
-#include <surface_control_private.h>
#include <GrDirectContext.h>
#include <SkBitmap.h>
#include <cutils/compiler.h>
#include <private/android/choreographer.h>
+#include <surface_control_private.h>
#include <thread/ThreadBase.h>
#include <utils/Looper.h>
#include <utils/Thread.h>
@@ -31,6 +31,7 @@
#include <set>
#include "CacheManager.h"
+#include "MemoryPolicy.h"
#include "ProfileDataContainer.h"
#include "RenderTask.h"
#include "TimeLord.h"
@@ -172,6 +173,9 @@ public:
return mASurfaceControlFunctions;
}
+ void trimMemory(TrimLevel level);
+ void trimCaches(CacheTrimLevel level);
+
/**
* isCurrent provides a way to query, if the caller is running on
* the render thread.
@@ -232,7 +236,6 @@ private:
bool mFrameCallbackTaskPending;
TimeLord mTimeLord;
- nsecs_t mDispatchFrameDelay = 4_ms;
RenderState* mRenderState;
EglManager* mEglManager;
WebViewFunctorManager& mFunctorManager;
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index 718d4a16d5c8..46698a6fdcc0 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -23,13 +23,11 @@
#include <GrDirectContext.h>
#include <GrTypes.h>
#include <android/sync.h>
+#include <gui/TraceUtils.h>
#include <ui/FatVector.h>
#include <vk/GrVkExtensions.h>
#include <vk/GrVkTypes.h>
-#include <cstring>
-
-#include <gui/TraceUtils.h>
#include "Properties.h"
#include "RenderThread.h"
#include "pipeline/skia/ShaderCache.h"
@@ -42,6 +40,38 @@ namespace android {
namespace uirenderer {
namespace renderthread {
+static std::array<std::string_view, 20> sEnableExtensions{
+ VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
+ VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
+ VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
+ VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME,
+ VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME,
+ VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME,
+ VK_KHR_MAINTENANCE1_EXTENSION_NAME,
+ VK_KHR_MAINTENANCE2_EXTENSION_NAME,
+ VK_KHR_MAINTENANCE3_EXTENSION_NAME,
+ VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME,
+ VK_KHR_SURFACE_EXTENSION_NAME,
+ VK_KHR_SWAPCHAIN_EXTENSION_NAME,
+ VK_EXT_BLEND_OPERATION_ADVANCED_EXTENSION_NAME,
+ VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME,
+ VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME,
+ VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME,
+ VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME,
+ VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
+ VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
+ VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME,
+};
+
+static bool shouldEnableExtension(const std::string_view& extension) {
+ for (const auto& it : sEnableExtensions) {
+ if (it == extension) {
+ return true;
+ }
+ }
+ return false;
+}
+
static void free_features_extensions_structs(const VkPhysicalDeviceFeatures2& features) {
// All Vulkan structs that could be part of the features chain will start with the
// structure type followed by the pNext pointer. We cast to the CommonVulkanHeader
@@ -59,28 +89,15 @@ static void free_features_extensions_structs(const VkPhysicalDeviceFeatures2& fe
}
}
-GrVkGetProc VulkanManager::sSkiaGetProp = [](const char* proc_name, VkInstance instance,
- VkDevice device) {
- if (device != VK_NULL_HANDLE) {
- if (strcmp("vkQueueSubmit", proc_name) == 0) {
- return (PFN_vkVoidFunction)VulkanManager::interceptedVkQueueSubmit;
- } else if (strcmp("vkQueueWaitIdle", proc_name) == 0) {
- return (PFN_vkVoidFunction)VulkanManager::interceptedVkQueueWaitIdle;
- }
- return vkGetDeviceProcAddr(device, proc_name);
- }
- return vkGetInstanceProcAddr(instance, proc_name);
-};
-
#define GET_PROC(F) m##F = (PFN_vk##F)vkGetInstanceProcAddr(VK_NULL_HANDLE, "vk" #F)
#define GET_INST_PROC(F) m##F = (PFN_vk##F)vkGetInstanceProcAddr(mInstance, "vk" #F)
#define GET_DEV_PROC(F) m##F = (PFN_vk##F)vkGetDeviceProcAddr(mDevice, "vk" #F)
-sp<VulkanManager> VulkanManager::getInstance() {
- // cache a weakptr to the context to enable a second thread to share the same vulkan state
- static wp<VulkanManager> sWeakInstance = nullptr;
- static std::mutex sLock;
+// cache a weakptr to the context to enable a second thread to share the same vulkan state
+static wp<VulkanManager> sWeakInstance = nullptr;
+static std::mutex sLock;
+sp<VulkanManager> VulkanManager::getInstance() {
std::lock_guard _lock{sLock};
sp<VulkanManager> vulkanManager = sWeakInstance.promote();
if (!vulkanManager.get()) {
@@ -91,6 +108,11 @@ sp<VulkanManager> VulkanManager::getInstance() {
return vulkanManager;
}
+sp<VulkanManager> VulkanManager::peekInstance() {
+ std::lock_guard _lock{sLock};
+ return sWeakInstance.promote();
+}
+
VulkanManager::~VulkanManager() {
if (mDevice != VK_NULL_HANDLE) {
mDeviceWaitIdle(mDevice);
@@ -102,6 +124,7 @@ VulkanManager::~VulkanManager() {
}
mGraphicsQueue = VK_NULL_HANDLE;
+ mAHBUploadQueue = VK_NULL_HANDLE;
mDevice = VK_NULL_HANDLE;
mPhysicalDevice = VK_NULL_HANDLE;
mInstance = VK_NULL_HANDLE;
@@ -139,6 +162,11 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
bool hasKHRSurfaceExtension = false;
bool hasKHRAndroidSurfaceExtension = false;
for (const VkExtensionProperties& extension : mInstanceExtensionsOwner) {
+ if (!shouldEnableExtension(extension.extensionName)) {
+ ALOGV("Not enabling instance extension %s", extension.extensionName);
+ continue;
+ }
+ ALOGV("Enabling instance extension %s", extension.extensionName);
mInstanceExtensions.push_back(extension.extensionName);
if (!strcmp(extension.extensionName, VK_KHR_SURFACE_EXTENSION_NAME)) {
hasKHRSurfaceExtension = true;
@@ -190,7 +218,7 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
mDriverVersion = physDeviceProperties.driverVersion;
// query to get the initial queue props size
- uint32_t queueCount;
+ uint32_t queueCount = 0;
mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, nullptr);
LOG_ALWAYS_FATAL_IF(!queueCount);
@@ -198,11 +226,14 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
std::unique_ptr<VkQueueFamilyProperties[]> queueProps(new VkQueueFamilyProperties[queueCount]);
mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, queueProps.get());
+ constexpr auto kRequestedQueueCount = 2;
+
// iterate to find the graphics queue
mGraphicsQueueIndex = queueCount;
for (uint32_t i = 0; i < queueCount; i++) {
if (queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
mGraphicsQueueIndex = i;
+ LOG_ALWAYS_FATAL_IF(queueProps[i].queueCount < kRequestedQueueCount);
break;
}
}
@@ -219,6 +250,11 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
LOG_ALWAYS_FATAL_IF(VK_SUCCESS != err);
bool hasKHRSwapchainExtension = false;
for (const VkExtensionProperties& extension : mDeviceExtensionsOwner) {
+ if (!shouldEnableExtension(extension.extensionName)) {
+ ALOGV("Not enabling device extension %s", extension.extensionName);
+ continue;
+ }
+ ALOGV("Enabling device extension %s", extension.extensionName);
mDeviceExtensions.push_back(extension.extensionName);
if (!strcmp(extension.extensionName, VK_KHR_SWAPCHAIN_EXTENSION_NAME)) {
hasKHRSwapchainExtension = true;
@@ -227,7 +263,14 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
LOG_ALWAYS_FATAL_IF(!hasKHRSwapchainExtension);
}
- grExtensions.init(sSkiaGetProp, mInstance, mPhysicalDevice, mInstanceExtensions.size(),
+ auto getProc = [](const char* proc_name, VkInstance instance, VkDevice device) {
+ if (device != VK_NULL_HANDLE) {
+ return vkGetDeviceProcAddr(device, proc_name);
+ }
+ return vkGetInstanceProcAddr(instance, proc_name);
+ };
+
+ grExtensions.init(getProc, mInstance, mPhysicalDevice, mInstanceExtensions.size(),
mInstanceExtensions.data(), mDeviceExtensions.size(),
mDeviceExtensions.data());
@@ -266,7 +309,7 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
// and we can't depend on it on all platforms
features.features.robustBufferAccess = VK_FALSE;
- float queuePriorities[1] = {0.0};
+ float queuePriorities[kRequestedQueueCount] = {0.0};
void* queueNextPtr = nullptr;
@@ -299,7 +342,7 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe
queueNextPtr, // pNext
0, // VkDeviceQueueCreateFlags
mGraphicsQueueIndex, // queueFamilyIndex
- 1, // queueCount
+ kRequestedQueueCount, // queueCount
queuePriorities, // pQueuePriorities
};
@@ -357,25 +400,43 @@ void VulkanManager::initialize() {
this->setupDevice(mExtensions, mPhysicalDeviceFeatures2);
mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 0, &mGraphicsQueue);
+ mGetDeviceQueue(mDevice, mGraphicsQueueIndex, 1, &mAHBUploadQueue);
if (Properties::enablePartialUpdates && Properties::useBufferAge) {
mSwapBehavior = SwapBehavior::BufferAge;
}
}
-sk_sp<GrDirectContext> VulkanManager::createContext(const GrContextOptions& options,
+static void onGrContextReleased(void* context) {
+ VulkanManager* manager = (VulkanManager*)context;
+ manager->decStrong((void*)onGrContextReleased);
+}
+
+sk_sp<GrDirectContext> VulkanManager::createContext(GrContextOptions& options,
ContextType contextType) {
+ auto getProc = [](const char* proc_name, VkInstance instance, VkDevice device) {
+ if (device != VK_NULL_HANDLE) {
+ return vkGetDeviceProcAddr(device, proc_name);
+ }
+ return vkGetInstanceProcAddr(instance, proc_name);
+ };
GrVkBackendContext backendContext;
backendContext.fInstance = mInstance;
backendContext.fPhysicalDevice = mPhysicalDevice;
backendContext.fDevice = mDevice;
- backendContext.fQueue = mGraphicsQueue;
+ backendContext.fQueue =
+ (contextType == ContextType::kRenderThread) ? mGraphicsQueue : mAHBUploadQueue;
backendContext.fGraphicsQueueIndex = mGraphicsQueueIndex;
backendContext.fMaxAPIVersion = mAPIVersion;
backendContext.fVkExtensions = &mExtensions;
backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2;
- backendContext.fGetProc = sSkiaGetProp;
+ backendContext.fGetProc = std::move(getProc);
+
+ LOG_ALWAYS_FATAL_IF(options.fContextDeleteProc != nullptr, "Conflicting fContextDeleteProcs!");
+ this->incStrong((void*)onGrContextReleased);
+ options.fContextDeleteContext = this;
+ options.fContextDeleteProc = onGrContextReleased;
return GrDirectContext::MakeVulkan(backendContext, options);
}
@@ -581,8 +642,6 @@ void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect)
ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd");
} else {
ALOGE("VulkanManager::swapBuffers(): Semaphore submission failed");
-
- std::lock_guard<std::mutex> lock(mGraphicsQueueMutex);
mQueueWaitIdle(mGraphicsQueue);
}
if (mDestroySemaphoreContext) {
@@ -597,10 +656,8 @@ void VulkanManager::swapBuffers(VulkanSurface* surface, const SkRect& dirtyRect)
void VulkanManager::destroySurface(VulkanSurface* surface) {
// Make sure all submit commands have finished before starting to destroy objects.
if (VK_NULL_HANDLE != mGraphicsQueue) {
- std::lock_guard<std::mutex> lock(mGraphicsQueueMutex);
mQueueWaitIdle(mGraphicsQueue);
}
- mDeviceWaitIdle(mDevice);
delete surface;
}
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index b8c2bdf112f8..2be1ffdbc423 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -17,10 +17,6 @@
#ifndef VULKANMANAGER_H
#define VULKANMANAGER_H
-#include <functional>
-#include <mutex>
-
-#include "vulkan/vulkan_core.h"
#if !defined(VK_USE_PLATFORM_ANDROID_KHR)
#define VK_USE_PLATFORM_ANDROID_KHR
#endif
@@ -51,7 +47,8 @@ typedef void(VKAPI_PTR* PFN_vkFrameBoundaryANDROID)(VkDevice device, VkSemaphore
#include "VulkanSurface.h"
#include "private/hwui/DrawVkInfo.h"
-class GrVkExtensions;
+#include <SkColorSpace.h>
+#include <SkRefCnt.h>
namespace android {
namespace uirenderer {
@@ -65,6 +62,7 @@ class RenderThread;
class VulkanManager final : public RefBase {
public:
static sp<VulkanManager> getInstance();
+ static sp<VulkanManager> peekInstance();
// Sets up the vulkan context that is shared amonst all clients of the VulkanManager. This must
// be call once before use of the VulkanManager. Multiple calls after the first will simiply
@@ -108,7 +106,7 @@ public:
};
// returns a Skia graphic context used to draw content on the specified thread
- sk_sp<GrDirectContext> createContext(const GrContextOptions& options,
+ sk_sp<GrDirectContext> createContext(GrContextOptions& options,
ContextType contextType = ContextType::kRenderThread);
uint32_t getDriverVersion() const { return mDriverVersion; }
@@ -184,25 +182,8 @@ private:
VkDevice mDevice = VK_NULL_HANDLE;
uint32_t mGraphicsQueueIndex;
-
- std::mutex mGraphicsQueueMutex;
VkQueue mGraphicsQueue = VK_NULL_HANDLE;
-
- static VKAPI_ATTR VkResult interceptedVkQueueSubmit(VkQueue queue, uint32_t submitCount,
- const VkSubmitInfo* pSubmits,
- VkFence fence) {
- sp<VulkanManager> manager = VulkanManager::getInstance();
- std::lock_guard<std::mutex> lock(manager->mGraphicsQueueMutex);
- return manager->mQueueSubmit(queue, submitCount, pSubmits, fence);
- }
-
- static VKAPI_ATTR VkResult interceptedVkQueueWaitIdle(VkQueue queue) {
- sp<VulkanManager> manager = VulkanManager::getInstance();
- std::lock_guard<std::mutex> lock(manager->mGraphicsQueueMutex);
- return manager->mQueueWaitIdle(queue);
- }
-
- static GrVkGetProc sSkiaGetProp;
+ VkQueue mAHBUploadQueue = VK_NULL_HANDLE;
// Variables saved to populate VkFunctorInitParams.
static const uint32_t mAPIVersion = VK_MAKE_VERSION(1, 1, 0);
diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp
index 666f32939206..3168cb09291b 100644
--- a/libs/hwui/renderthread/VulkanSurface.cpp
+++ b/libs/hwui/renderthread/VulkanSurface.cpp
@@ -213,7 +213,14 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode
outWindowInfo->bufferFormat = ColorTypeToBufferFormat(colorType);
outWindowInfo->colorspace = colorSpace;
- outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType);
+ outWindowInfo->colorMode = colorMode;
+
+ if (colorMode == ColorMode::Hdr || colorMode == ColorMode::Hdr10) {
+ outWindowInfo->dataspace =
+ static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED);
+ } else {
+ outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType);
+ }
LOG_ALWAYS_FATAL_IF(
outWindowInfo->dataspace == HAL_DATASPACE_UNKNOWN && colorType != kAlpha_8_SkColorType,
"Unsupported colorspace");
@@ -368,6 +375,14 @@ void VulkanSurface::releaseBuffers() {
}
}
+void VulkanSurface::invalidateBuffers() {
+ for (uint32_t i = 0; i < mWindowInfo.bufferCount; i++) {
+ VulkanSurface::NativeBufferInfo& bufferInfo = mNativeBuffers[i];
+ bufferInfo.hasValidContents = false;
+ bufferInfo.lastPresentedCount = 0;
+ }
+}
+
VulkanSurface::NativeBufferInfo* VulkanSurface::dequeueNativeBuffer() {
// Set the mCurrentBufferInfo to invalid in case of error and only reset it to the correct
// value at the end of the function if everything dequeued correctly.
@@ -400,6 +415,10 @@ VulkanSurface::NativeBufferInfo* VulkanSurface::dequeueNativeBuffer() {
// new NativeBufferInfo storage will be populated lazily as we dequeue each new buffer.
mWindowInfo.actualSize = actualSize;
releaseBuffers();
+ } else {
+ // A change in transform means we need to repaint the entire buffer area as the damage
+ // rects have just moved about.
+ invalidateBuffers();
}
if (transformHint != mWindowInfo.transform) {
@@ -446,9 +465,15 @@ VulkanSurface::NativeBufferInfo* VulkanSurface::dequeueNativeBuffer() {
VulkanSurface::NativeBufferInfo* bufferInfo = &mNativeBuffers[idx];
if (bufferInfo->skSurface.get() == nullptr) {
+ SkSurfaceProps surfaceProps;
+ if (mWindowInfo.colorMode != ColorMode::Default) {
+ surfaceProps = SkSurfaceProps(SkSurfaceProps::kAlwaysDither_Flag | surfaceProps.flags(),
+ surfaceProps.pixelGeometry());
+ }
bufferInfo->skSurface = SkSurface::MakeFromAHardwareBuffer(
mGrContext, ANativeWindowBuffer_getHardwareBuffer(bufferInfo->buffer.get()),
- kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, nullptr, /*from_window=*/true);
+ kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, &surfaceProps,
+ /*from_window=*/true);
if (bufferInfo->skSurface.get() == nullptr) {
ALOGE("SkSurface::MakeFromAHardwareBuffer failed");
mNativeWindow->cancelBuffer(mNativeWindow.get(), buffer,
@@ -511,6 +536,33 @@ int VulkanSurface::getCurrentBuffersAge() {
return currentBuffer.hasValidContents ? (mPresentCount - currentBuffer.lastPresentedCount) : 0;
}
+void VulkanSurface::setColorSpace(sk_sp<SkColorSpace> colorSpace) {
+ mWindowInfo.colorspace = std::move(colorSpace);
+ for (int i = 0; i < kNumBufferSlots; i++) {
+ mNativeBuffers[i].skSurface.reset();
+ }
+
+ if (mWindowInfo.colorMode == ColorMode::Hdr || mWindowInfo.colorMode == ColorMode::Hdr10) {
+ mWindowInfo.dataspace =
+ static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED);
+ } else {
+ mWindowInfo.dataspace = ColorSpaceToADataSpace(
+ mWindowInfo.colorspace.get(), BufferFormatToColorType(mWindowInfo.bufferFormat));
+ }
+ LOG_ALWAYS_FATAL_IF(mWindowInfo.dataspace == HAL_DATASPACE_UNKNOWN &&
+ mWindowInfo.bufferFormat != AHARDWAREBUFFER_FORMAT_R8_UNORM,
+ "Unsupported colorspace");
+
+ if (mNativeWindow) {
+ int err = ANativeWindow_setBuffersDataSpace(mNativeWindow.get(), mWindowInfo.dataspace);
+ if (err != 0) {
+ ALOGE("VulkanSurface::setColorSpace() native_window_set_buffers_data_space(%d) "
+ "failed: %s (%d)",
+ mWindowInfo.dataspace, strerror(-err), err);
+ }
+ }
+}
+
} /* namespace renderthread */
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h
index b8ccf7810b5d..116075c16eba 100644
--- a/libs/hwui/renderthread/VulkanSurface.h
+++ b/libs/hwui/renderthread/VulkanSurface.h
@@ -20,6 +20,7 @@
#include <system/window.h>
#include <vulkan/vulkan.h>
+#include <SkColorSpace.h>
#include <SkRefCnt.h>
#include <SkSize.h>
@@ -45,6 +46,7 @@ public:
}
const SkMatrix& getCurrentPreTransform() { return mWindowInfo.preTransform; }
+ void setColorSpace(sk_sp<SkColorSpace> colorSpace);
const SkM44& getPixelSnapMatrix() const { return mWindowInfo.pixelSnapMatrix; }
private:
@@ -95,6 +97,7 @@ private:
uint32_t bufferFormat;
android_dataspace dataspace;
sk_sp<SkColorSpace> colorspace;
+ ColorMode colorMode;
int transform;
size_t bufferCount;
uint64_t windowUsageFlags;
@@ -113,6 +116,7 @@ private:
WindowInfo* outWindowInfo);
static bool UpdateWindow(ANativeWindow* window, const WindowInfo& windowInfo);
void releaseBuffers();
+ void invalidateBuffers();
// TODO: This number comes from ui/BufferQueueDefs. We're not pulling the
// header in so that we don't need to depend on libui, but we should share
diff --git a/libs/hwui/tests/common/BitmapAllocationTestUtils.h b/libs/hwui/tests/common/BitmapAllocationTestUtils.h
index 312b60bbc067..bbdd98e0c0d1 100644
--- a/libs/hwui/tests/common/BitmapAllocationTestUtils.h
+++ b/libs/hwui/tests/common/BitmapAllocationTestUtils.h
@@ -56,10 +56,11 @@ public:
template <class BaseScene>
static bool registerBitmapAllocationScene(std::string name, std::string description) {
- TestScene::registerScene({name + "GlTex", description + " (GlTex version).",
+ TestScene::registerScene({name + "Texture", description + " (Texture version).",
createBitmapAllocationScene<BaseScene, &allocateHeapBitmap>});
- TestScene::registerScene({name + "EglImage", description + " (EglImage version).",
+ TestScene::registerScene({name + "HardwareBuffer",
+ description + " (HardwareBuffer version).",
createBitmapAllocationScene<BaseScene, &allocateHardwareBitmap>});
return true;
}
diff --git a/libs/hwui/tests/common/CallCountingCanvas.h b/libs/hwui/tests/common/CallCountingCanvas.h
index d3c41191eef1..dc36a2e01815 100644
--- a/libs/hwui/tests/common/CallCountingCanvas.h
+++ b/libs/hwui/tests/common/CallCountingCanvas.h
@@ -19,6 +19,8 @@
#include <SkCanvasVirtualEnforcer.h>
#include <SkNoDrawCanvas.h>
+enum class SkBlendMode;
+
namespace android {
namespace uirenderer {
namespace test {
diff --git a/libs/hwui/tests/common/TestContext.cpp b/libs/hwui/tests/common/TestContext.cpp
index 898c64bd4159..fd596d998dfd 100644
--- a/libs/hwui/tests/common/TestContext.cpp
+++ b/libs/hwui/tests/common/TestContext.cpp
@@ -28,10 +28,11 @@ const ui::StaticDisplayInfo& getDisplayInfo() {
#if HWUI_NULL_GPU
info.density = 2.f;
#else
- const sp<IBinder> token = SurfaceComposerClient::getInternalDisplayToken();
- LOG_ALWAYS_FATAL_IF(!token, "%s: No internal display", __FUNCTION__);
+ const std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds();
+ LOG_ALWAYS_FATAL_IF(ids.empty(), "%s: No displays", __FUNCTION__);
- const status_t status = SurfaceComposerClient::getStaticDisplayInfo(token, &info);
+ const status_t status =
+ SurfaceComposerClient::getStaticDisplayInfo(ids.front().value, &info);
LOG_ALWAYS_FATAL_IF(status, "%s: Failed to get display info", __FUNCTION__);
#endif
return info;
@@ -48,7 +49,10 @@ const ui::DisplayMode& getActiveDisplayMode() {
config.xDpi = config.yDpi = 320.f;
config.refreshRate = 60.f;
#else
- const sp<IBinder> token = SurfaceComposerClient::getInternalDisplayToken();
+ const std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds();
+ LOG_ALWAYS_FATAL_IF(ids.empty(), "%s: No displays", __FUNCTION__);
+
+ const sp<IBinder> token = SurfaceComposerClient::getPhysicalDisplayToken(ids.front());
LOG_ALWAYS_FATAL_IF(!token, "%s: No internal display", __FUNCTION__);
const status_t status = SurfaceComposerClient::getActiveDisplayMode(token, &config);
diff --git a/libs/hwui/tests/common/TestListViewSceneBase.cpp b/libs/hwui/tests/common/TestListViewSceneBase.cpp
index 43df4a0b1576..e70d44c9c60a 100644
--- a/libs/hwui/tests/common/TestListViewSceneBase.cpp
+++ b/libs/hwui/tests/common/TestListViewSceneBase.cpp
@@ -19,6 +19,8 @@
#include "TestContext.h"
#include "TestUtils.h"
+#include <SkBlendMode.h>
+
#include <utils/Color.h>
namespace android {
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index 491af4336f97..a4890ede8faa 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -26,7 +26,13 @@
#include <renderthread/VulkanManager.h>
#include <utils/Unicode.h>
+#include "SkCanvas.h"
#include "SkColorData.h"
+#include "SkMatrix.h"
+#include "SkPath.h"
+#include "SkPixmap.h"
+#include "SkRect.h"
+#include "SkSurface.h"
#include "SkUnPreMultiply.h"
namespace android {
diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h
index fcaa745e9fc6..81ecfe59d3bc 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -28,10 +28,20 @@
#include <renderstate/RenderState.h>
#include <renderthread/RenderThread.h>
+#include <SkBitmap.h>
+#include <SkColor.h>
+#include <SkImageInfo.h>
+#include <SkRefCnt.h>
+
#include <gtest/gtest.h>
#include <memory>
#include <unordered_map>
+class SkCanvas;
+class SkMatrix;
+class SkPath;
+struct SkRect;
+
namespace android {
namespace uirenderer {
@@ -51,12 +61,12 @@ namespace uirenderer {
ADD_FAILURE() << "ClipState not a rect"; \
}
-#define INNER_PIPELINE_TEST(test_case_name, test_name, pipeline, functionCall) \
- TEST(test_case_name, test_name##_##pipeline) { \
- RenderPipelineType oldType = Properties::getRenderPipelineType(); \
- Properties::overrideRenderPipelineType(RenderPipelineType::pipeline, true); \
- functionCall; \
- Properties::overrideRenderPipelineType(oldType, true); \
+#define INNER_PIPELINE_TEST(test_case_name, test_name, pipeline, functionCall) \
+ TEST(test_case_name, test_name##_##pipeline) { \
+ RenderPipelineType oldType = Properties::getRenderPipelineType(); \
+ Properties::overrideRenderPipelineType(RenderPipelineType::pipeline); \
+ functionCall; \
+ Properties::overrideRenderPipelineType(oldType); \
};
#define INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, pipeline) \
@@ -68,27 +78,29 @@ namespace uirenderer {
* Like gtest's TEST, but runs on the RenderThread, and 'renderThread' is passed, in top level scope
* (for e.g. accessing its RenderState)
*/
-#define RENDERTHREAD_TEST(test_case_name, test_name) \
- class test_case_name##_##test_name##_RenderThreadTest { \
- public: \
- static void doTheThing(renderthread::RenderThread& renderThread); \
- }; \
- INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \
- INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); \
- void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \
+#define RENDERTHREAD_TEST(test_case_name, test_name) \
+ class test_case_name##_##test_name##_RenderThreadTest { \
+ public: \
+ static void doTheThing(renderthread::RenderThread& renderThread); \
+ }; \
+ INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \
+ /* Temporarily disabling Vulkan until we can figure out a way to stub out the driver */ \
+ /* INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); */ \
+ void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \
renderthread::RenderThread& renderThread)
/**
* Like RENDERTHREAD_TEST, but only runs with the Skia RenderPipelineTypes
*/
-#define RENDERTHREAD_SKIA_PIPELINE_TEST(test_case_name, test_name) \
- class test_case_name##_##test_name##_RenderThreadTest { \
- public: \
- static void doTheThing(renderthread::RenderThread& renderThread); \
- }; \
- INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \
- INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); \
- void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \
+#define RENDERTHREAD_SKIA_PIPELINE_TEST(test_case_name, test_name) \
+ class test_case_name##_##test_name##_RenderThreadTest { \
+ public: \
+ static void doTheThing(renderthread::RenderThread& renderThread); \
+ }; \
+ INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaGL); \
+ /* Temporarily disabling Vulkan until we can figure out a way to stub out the driver */ \
+ /* INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name, SkiaVulkan); */ \
+ void test_case_name##_##test_name##_RenderThreadTest::doTheThing( \
renderthread::RenderThread& renderThread)
/**
diff --git a/libs/hwui/tests/common/scenes/BitmapFillrate.cpp b/libs/hwui/tests/common/scenes/BitmapFillrate.cpp
index 5af7d43d7f66..19e87f851827 100644
--- a/libs/hwui/tests/common/scenes/BitmapFillrate.cpp
+++ b/libs/hwui/tests/common/scenes/BitmapFillrate.cpp
@@ -19,6 +19,7 @@
#include "utils/Color.h"
#include <SkBitmap.h>
+#include <SkBlendMode.h>
using namespace android;
using namespace android::uirenderer;
diff --git a/libs/hwui/tests/common/scenes/BitmapShaders.cpp b/libs/hwui/tests/common/scenes/BitmapShaders.cpp
index 03aeb55f129b..a07cdf720b50 100644
--- a/libs/hwui/tests/common/scenes/BitmapShaders.cpp
+++ b/libs/hwui/tests/common/scenes/BitmapShaders.cpp
@@ -14,7 +14,17 @@
* limitations under the License.
*/
-#include <SkImagePriv.h>
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkCanvas.h>
+#include <SkImage.h>
+#include <SkImageInfo.h>
+#include <SkPaint.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
+#include <SkSamplingOptions.h>
+#include <SkShader.h>
+#include <SkTileMode.h>
#include "hwui/Paint.h"
#include "TestSceneBase.h"
#include "tests/common/BitmapAllocationTestUtils.h"
diff --git a/libs/hwui/tests/common/scenes/ClippingAnimation.cpp b/libs/hwui/tests/common/scenes/ClippingAnimation.cpp
index 2a016ac1b5bc..3a1ea8c29963 100644
--- a/libs/hwui/tests/common/scenes/ClippingAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ClippingAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class ClippingAnimation;
static TestScene::Registrar _RectGrid(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp b/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp
index 4271d2f04b88..484289a8ef1d 100644
--- a/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/GlyphStressAnimation.cpp
@@ -20,6 +20,8 @@
#include <hwui/Paint.h>
#include <minikin/Layout.h>
+#include <SkBlendMode.h>
+
#include <cstdio>
class GlyphStressAnimation;
diff --git a/libs/hwui/tests/common/scenes/HwBitmap565.cpp b/libs/hwui/tests/common/scenes/HwBitmap565.cpp
index cbdb756b8fa7..de0ef6d595f8 100644
--- a/libs/hwui/tests/common/scenes/HwBitmap565.cpp
+++ b/libs/hwui/tests/common/scenes/HwBitmap565.cpp
@@ -18,6 +18,12 @@
#include "tests/common/BitmapAllocationTestUtils.h"
#include "utils/Color.h"
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkCanvas.h>
+#include <SkPaint.h>
+#include <SkRefCnt.h>
+
class HwBitmap565;
static TestScene::Registrar _HwBitmap565(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp b/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp
index 564354f04674..dfdd0d8727b9 100644
--- a/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp
+++ b/libs/hwui/tests/common/scenes/HwBitmapInCompositeShader.cpp
@@ -17,6 +17,8 @@
#include "TestSceneBase.h"
#include "utils/Color.h"
+#include <SkBlendMode.h>
+#include <SkColorSpace.h>
#include <SkGradientShader.h>
#include <SkImagePriv.h>
#include <ui/PixelFormat.h>
diff --git a/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp b/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp
index cac2fb3d8d5c..2955fb25ec2c 100644
--- a/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/HwLayerAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class HwLayerAnimation;
static TestScene::Registrar _HwLayer(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp b/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp
index 77a59dfe6ba5..8c9a6147f47d 100644
--- a/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/HwLayerSizeAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class HwLayerSizeAnimation;
static TestScene::Registrar _HwLayerSize(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/JankyScene.cpp b/libs/hwui/tests/common/scenes/JankyScene.cpp
index f5e6b317529a..250b986e7e73 100644
--- a/libs/hwui/tests/common/scenes/JankyScene.cpp
+++ b/libs/hwui/tests/common/scenes/JankyScene.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
#include <unistd.h>
class JankyScene;
diff --git a/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp b/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp
index 5eaf1853233a..f669dbc9323e 100644
--- a/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ListOfFadedTextAnimation.cpp
@@ -17,6 +17,7 @@
#include "TestSceneBase.h"
#include "tests/common/TestListViewSceneBase.h"
#include "hwui/Paint.h"
+#include <SkBlendMode.h>
#include <SkGradientShader.h>
class ListOfFadedTextAnimation;
diff --git a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
index d031923a112b..4a5d9468cd88 100644
--- a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
@@ -17,7 +17,16 @@
#include "TestSceneBase.h"
#include "tests/common/TestListViewSceneBase.h"
#include "hwui/Paint.h"
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkColor.h>
#include <SkFont.h>
+#include <SkFontTypes.h>
+#include <SkPaint.h>
+#include <SkPoint.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
+#include <SkScalar.h>
#include <cstdio>
class ListViewAnimation;
@@ -48,7 +57,7 @@ class ListViewAnimation : public TestListViewSceneBase {
128 * 3;
paint.setColor(bgDark ? Color::White : Color::Grey_700);
- SkFont font;
+ SkFont font;
font.setSize(size / 2);
char charToShow = 'A' + (rand() % 26);
const SkPoint pos = {SkIntToScalar(size / 2),
diff --git a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp
index edadf78db051..13a438199ae5 100644
--- a/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/MagnifierAnimation.cpp
@@ -19,19 +19,57 @@
#include "utils/Color.h"
#include "hwui/Paint.h"
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkFont.h>
+
class MagnifierAnimation;
+using Rect = android::uirenderer::Rect;
+
static TestScene::Registrar _Magnifier(TestScene::Info{
"magnifier", "A sample magnifier using Readback",
TestScene::simpleCreateScene<MagnifierAnimation>});
+class BlockingCopyRequest : public CopyRequest {
+ sk_sp<Bitmap> mDestination;
+ std::mutex mLock;
+ std::condition_variable mCondVar;
+ CopyResult mResult;
+
+public:
+ BlockingCopyRequest(::Rect rect, sk_sp<Bitmap> bitmap)
+ : CopyRequest(rect), mDestination(bitmap) {}
+
+ virtual SkBitmap getDestinationBitmap(int srcWidth, int srcHeight) override {
+ SkBitmap bitmap;
+ mDestination->getSkBitmap(&bitmap);
+ return bitmap;
+ }
+
+ virtual void onCopyFinished(CopyResult result) override {
+ std::unique_lock _lock{mLock};
+ mResult = result;
+ mCondVar.notify_all();
+ }
+
+ CopyResult waitForResult() {
+ std::unique_lock _lock{mLock};
+ mCondVar.wait(_lock);
+ return mResult;
+ }
+};
+
class MagnifierAnimation : public TestScene {
public:
sp<RenderNode> card;
sp<RenderNode> zoomImageView;
+ sk_sp<Bitmap> magnifier;
+ std::shared_ptr<BlockingCopyRequest> copyRequest;
void createContent(int width, int height, Canvas& canvas) override {
magnifier = TestUtils::createBitmap(200, 100);
+ setupCopyRequest();
SkBitmap temp;
magnifier->getSkBitmap(&temp);
temp.eraseColor(Color::White);
@@ -61,19 +99,20 @@ public:
canvas.enableZ(false);
}
+ void setupCopyRequest() {
+ constexpr int x = 90;
+ constexpr int y = 325;
+ copyRequest = std::make_shared<BlockingCopyRequest>(
+ ::Rect(x, y, x + magnifier->width(), y + magnifier->height()), magnifier);
+ }
+
void doFrame(int frameNr) override {
int curFrame = frameNr % 150;
card->mutateStagingProperties().setTranslationX(curFrame);
card->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
if (renderTarget) {
- SkBitmap temp;
- magnifier->getSkBitmap(&temp);
- constexpr int x = 90;
- constexpr int y = 325;
- RenderProxy::copySurfaceInto(renderTarget.get(), x, y, x + magnifier->width(),
- y + magnifier->height(), &temp);
+ RenderProxy::copySurfaceInto(renderTarget.get(), copyRequest);
+ copyRequest->waitForResult();
}
}
-
- sk_sp<Bitmap> magnifier;
};
diff --git a/libs/hwui/tests/common/scenes/OvalAnimation.cpp b/libs/hwui/tests/common/scenes/OvalAnimation.cpp
index 402c1ece2146..1a2af8382ad7 100644
--- a/libs/hwui/tests/common/scenes/OvalAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/OvalAnimation.cpp
@@ -17,6 +17,8 @@
#include "TestSceneBase.h"
#include "utils/Color.h"
+#include <SkBlendMode.h>
+
class OvalAnimation;
static TestScene::Registrar _Oval(TestScene::Info{"oval", "Draws 1 oval.",
diff --git a/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp b/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp
index fb1b000a995e..25cf4d61bf9d 100644
--- a/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/PartialDamageAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class PartialDamageAnimation;
static TestScene::Registrar _PartialDamage(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp b/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp
index 1e343c1dd283..969514c50d14 100644
--- a/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/PathClippingAnimation.cpp
@@ -16,6 +16,8 @@
#include <vector>
+#include <SkBlendMode.h>
+
#include "TestSceneBase.h"
class PathClippingAnimation : public TestScene {
diff --git a/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp b/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp
index 716d3979bdcb..3caaf8236d8a 100644
--- a/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp
+++ b/libs/hwui/tests/common/scenes/ReadbackFromHardwareBitmap.cpp
@@ -16,6 +16,12 @@
#include "TestSceneBase.h"
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkPaint.h>
+#include <SkRect.h>
+#include <SkRefCnt.h>
+
class ReadbackFromHardware;
static TestScene::Registrar _SaveLayer(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp
index 1c2507867f6e..27948f8b4b43 100644
--- a/libs/hwui/tests/common/scenes/RecentsAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/RecentsAnimation.cpp
@@ -17,6 +17,11 @@
#include "TestSceneBase.h"
#include "utils/Color.h"
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkColor.h>
+#include <SkRefCnt.h>
+
class RecentsAnimation;
static TestScene::Registrar _Recents(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/RectGridAnimation.cpp b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp
index f37bcbc3ee1b..99e785887b16 100644
--- a/libs/hwui/tests/common/scenes/RectGridAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/RectGridAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class RectGridAnimation;
static TestScene::Registrar _RectGrid(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp
index e9f353d887f2..2c27969487d3 100644
--- a/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/RoundRectClippingAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
#include <vector>
class RoundRectClippingAnimation : public TestScene {
diff --git a/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp b/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp
index 252f539ffca9..ee30c131efbd 100644
--- a/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp
+++ b/libs/hwui/tests/common/scenes/SaveLayer2Animation.cpp
@@ -16,6 +16,7 @@
#include <hwui/Paint.h>
#include <minikin/Layout.h>
+#include <SkBlendMode.h>
#include <string>
#include "TestSceneBase.h"
diff --git a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp
index 31a8ae1d38cd..d5060c758f93 100644
--- a/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/SaveLayerAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class SaveLayerAnimation;
static TestScene::Registrar _SaveLayer(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp
index c13e80e8c204..827ddab118d9 100644
--- a/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp
+++ b/libs/hwui/tests/common/scenes/ShadowGrid2Animation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class ShadowGrid2Animation;
static TestScene::Registrar _ShadowGrid2(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp
index 772b98e32220..a4fb10c5081e 100644
--- a/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ShadowGridAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class ShadowGridAnimation;
static TestScene::Registrar _ShadowGrid(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp
index 0019da5fd80b..58c03727bc29 100644
--- a/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ShadowShaderAnimation.cpp
@@ -16,6 +16,8 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
+
class ShadowShaderAnimation;
static TestScene::Registrar _ShadowShader(TestScene::Info{
diff --git a/libs/hwui/tests/common/scenes/ShapeAnimation.cpp b/libs/hwui/tests/common/scenes/ShapeAnimation.cpp
index 70a1557dcf6a..c0c3dfd9a8c4 100644
--- a/libs/hwui/tests/common/scenes/ShapeAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ShapeAnimation.cpp
@@ -17,6 +17,8 @@
#include "TestSceneBase.h"
#include "utils/Color.h"
+#include <SkBlendMode.h>
+
#include <cstdio>
class ShapeAnimation;
diff --git a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp
index a0bc5aa245d5..40f2ed081626 100644
--- a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp
@@ -16,7 +16,9 @@
#include "TestSceneBase.h"
-#include <SkColorMatrixFilter.h>
+#include <SkBlendMode.h>
+#include <SkColorFilter.h>
+#include <SkColorMatrix.h>
#include <SkGradientShader.h>
class SimpleColorMatrixAnimation;
diff --git a/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp b/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp
index 57a260c8d234..a9e7a34b5b3f 100644
--- a/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/SimpleGradientAnimation.cpp
@@ -16,6 +16,7 @@
#include "TestSceneBase.h"
+#include <SkBlendMode.h>
#include <SkGradientShader.h>
class SimpleGradientAnimation;
diff --git a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp
index e677549b7894..bb95490c1d39 100644
--- a/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/StretchyListViewAnimation.cpp
@@ -14,7 +14,16 @@
* limitations under the License.
*/
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkCanvas.h>
+#include <SkColor.h>
#include <SkFont.h>
+#include <SkFontTypes.h>
+#include <SkPaint.h>
+#include <SkPoint.h>
+#include <SkRefCnt.h>
+#include <SkRRect.h>
#include <cstdio>
#include "TestSceneBase.h"
#include "hwui/Paint.h"
@@ -130,7 +139,7 @@ private:
roundRectPaint.setColor(Color::White);
if (addHolePunch) {
// Punch a hole but then cover it up, we don't want to actually see it
- canvas.punchHole(SkRRect::MakeRect(SkRect::MakeWH(itemWidth, itemHeight)));
+ canvas.punchHole(SkRRect::MakeRect(SkRect::MakeWH(itemWidth, itemHeight)), 1.f);
}
canvas.drawRoundRect(0, 0, itemWidth, itemHeight, dp(6), dp(6), roundRectPaint);
@@ -227,4 +236,4 @@ class StretchyUniformLayerListViewHolePunch : public StretchyListViewAnimation {
StretchEffectBehavior stretchBehavior() override { return StretchEffectBehavior::UniformScale; }
bool haveHolePunch() override { return true; }
bool forceLayer() override { return true; }
-}; \ No newline at end of file
+};
diff --git a/libs/hwui/tests/common/scenes/TextAnimation.cpp b/libs/hwui/tests/common/scenes/TextAnimation.cpp
index d30903679bce..78146b8cabf2 100644
--- a/libs/hwui/tests/common/scenes/TextAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/TextAnimation.cpp
@@ -17,6 +17,8 @@
#include "TestSceneBase.h"
#include "hwui/Paint.h"
+#include <SkBlendMode.h>
+
class TextAnimation;
static TestScene::Registrar _Text(TestScene::Info{"text", "Draws a bunch of text.",
diff --git a/libs/hwui/tests/common/scenes/TvApp.cpp b/libs/hwui/tests/common/scenes/TvApp.cpp
index c6219c485b85..aff8ca1e26c7 100644
--- a/libs/hwui/tests/common/scenes/TvApp.cpp
+++ b/libs/hwui/tests/common/scenes/TvApp.cpp
@@ -14,7 +14,12 @@
* limitations under the License.
*/
+#include "SkBitmap.h"
#include "SkBlendMode.h"
+#include "SkColorFilter.h"
+#include "SkFont.h"
+#include "SkImageInfo.h"
+#include "SkRefCnt.h"
#include "TestSceneBase.h"
#include "tests/common/BitmapAllocationTestUtils.h"
#include "hwui/Paint.h"
diff --git a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp
index 9cd10759a834..a55b72534924 100644
--- a/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp
+++ b/libs/hwui/tests/microbench/DisplayListCanvasBench.cpp
@@ -22,6 +22,8 @@
#include "pipeline/skia/SkiaDisplayList.h"
#include "tests/common/TestUtils.h"
+#include <SkBlendMode.h>
+
using namespace android;
using namespace android::uirenderer;
using namespace android::uirenderer::skiapipeline;
diff --git a/libs/hwui/tests/microbench/RenderNodeBench.cpp b/libs/hwui/tests/microbench/RenderNodeBench.cpp
index 6aed251481bf..72946c4abdf0 100644
--- a/libs/hwui/tests/microbench/RenderNodeBench.cpp
+++ b/libs/hwui/tests/microbench/RenderNodeBench.cpp
@@ -19,6 +19,8 @@
#include "hwui/Canvas.h"
#include "RenderNode.h"
+#include <SkBlendMode.h>
+
using namespace android;
using namespace android::uirenderer;
diff --git a/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp
index 2ec78a429481..138b3efd10ed 100644
--- a/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp
+++ b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp
@@ -29,7 +29,7 @@ AHardwareBuffer* allocHardwareBuffer() {
.height = 16,
.layers = 1,
.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
- .usage = AHARDWAREBUFFER_USAGE_CPU_READ_RARELY | AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY,
+ .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE,
};
constexpr int kSucceeded = 0;
int status = AHardwareBuffer_allocate(&desc, &buffer);
diff --git a/libs/hwui/tests/unit/CacheManagerTests.cpp b/libs/hwui/tests/unit/CacheManagerTests.cpp
index fc84abb1d605..2b90bda87ecd 100644
--- a/libs/hwui/tests/unit/CacheManagerTests.cpp
+++ b/libs/hwui/tests/unit/CacheManagerTests.cpp
@@ -21,6 +21,7 @@
#include "tests/common/TestUtils.h"
#include <SkImagePriv.h>
+#include "include/gpu/GpuTypes.h" // from Skia
using namespace android;
using namespace android::uirenderer;
@@ -45,7 +46,8 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, DISABLED_trimMemory) {
while (getCacheUsage(grContext) <= renderThread.cacheManager().getBackgroundCacheSize()) {
SkImageInfo info = SkImageInfo::MakeA8(width, height);
- sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kYes, info);
+ sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, skgpu::Budgeted::kYes,
+ info);
surface->getCanvas()->drawColor(SK_AlphaTRANSPARENT);
grContext->flushAndSubmit();
@@ -59,7 +61,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, DISABLED_trimMemory) {
ASSERT_TRUE(SkImage_pinAsTexture(image.get(), grContext));
// attempt to trim all memory while we still hold strong refs
- renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete);
+ renderThread.cacheManager().trimMemory(TrimLevel::COMPLETE);
ASSERT_TRUE(0 == grContext->getResourceCachePurgeableBytes());
// free the surfaces
@@ -76,11 +78,11 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, DISABLED_trimMemory) {
ASSERT_TRUE(renderThread.cacheManager().getBackgroundCacheSize() < purgeableBytes);
// UI hidden and make sure only some got purged (unique should remain)
- renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::UiHidden);
+ renderThread.cacheManager().trimMemory(TrimLevel::UI_HIDDEN);
ASSERT_TRUE(0 < grContext->getResourceCachePurgeableBytes());
ASSERT_TRUE(renderThread.cacheManager().getBackgroundCacheSize() > getCacheUsage(grContext));
// complete and make sure all get purged
- renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete);
+ renderThread.cacheManager().trimMemory(TrimLevel::COMPLETE);
ASSERT_TRUE(0 == grContext->getResourceCachePurgeableBytes());
}
diff --git a/libs/hwui/tests/unit/CanvasContextTests.cpp b/libs/hwui/tests/unit/CanvasContextTests.cpp
index 1771c3590e10..9e376e32f8ea 100644
--- a/libs/hwui/tests/unit/CanvasContextTests.cpp
+++ b/libs/hwui/tests/unit/CanvasContextTests.cpp
@@ -36,9 +36,9 @@ RENDERTHREAD_TEST(CanvasContext, create) {
auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0));
- ASSERT_FALSE(canvasContext->hasSurface());
+ ASSERT_FALSE(canvasContext->hasOutputTarget());
canvasContext->destroy();
}
diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp
index 2cf3456694b0..1f6edf36af25 100644
--- a/libs/hwui/tests/unit/CanvasOpTests.cpp
+++ b/libs/hwui/tests/unit/CanvasOpTests.cpp
@@ -23,9 +23,18 @@
#include <tests/common/CallCountingCanvas.h>
-#include "SkPictureRecorder.h"
+#include "SkBlendMode.h"
+#include "SkBitmap.h"
+#include "SkCanvas.h"
#include "SkColor.h"
+#include "SkImageInfo.h"
#include "SkLatticeIter.h"
+#include "SkPaint.h"
+#include "SkPath.h"
+#include "SkPictureRecorder.h"
+#include "SkRRect.h"
+#include "SkRect.h"
+#include "SkRegion.h"
#include "pipeline/skia/AnimatedDrawables.h"
#include <SkNoDrawCanvas.h>
diff --git a/libs/hwui/tests/unit/EglManagerTests.cpp b/libs/hwui/tests/unit/EglManagerTests.cpp
index 7f2e1589ae6c..ec9ab90fa46b 100644
--- a/libs/hwui/tests/unit/EglManagerTests.cpp
+++ b/libs/hwui/tests/unit/EglManagerTests.cpp
@@ -20,6 +20,8 @@
#include "renderthread/RenderEffectCapabilityQuery.h"
#include "tests/common/TestContext.h"
+#include <SkColorSpace.h>
+
using namespace android;
using namespace android::uirenderer;
using namespace android::uirenderer::renderthread;
diff --git a/libs/hwui/tests/unit/FatalTestCanvas.h b/libs/hwui/tests/unit/FatalTestCanvas.h
index 2a74afc5bb7a..96a0c6114682 100644
--- a/libs/hwui/tests/unit/FatalTestCanvas.h
+++ b/libs/hwui/tests/unit/FatalTestCanvas.h
@@ -19,6 +19,8 @@
#include <SkCanvas.h>
#include <gtest/gtest.h>
+class SkRRect;
+
namespace {
class TestCanvasBase : public SkCanvas {
diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
index ec949b80ea55..596bd37e4cf5 100644
--- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
@@ -17,6 +17,7 @@
#include <VectorDrawable.h>
#include <gtest/gtest.h>
+#include <SkBlendMode.h>
#include <SkClipStack.h>
#include <SkSurface_Base.h>
#include <string.h>
@@ -334,7 +335,7 @@ RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorder) {
"A");
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, parent.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0));
TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
@@ -398,7 +399,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, emptyReceiver) {
"A");
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, parent.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0));
TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
@@ -518,7 +519,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, projectionHwLayer) {
// prepareTree is required to find, which receivers have backward projected nodes
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, parent.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0));
TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
@@ -618,7 +619,7 @@ RENDERTHREAD_TEST(RenderNodeDrawable, projectionChildScroll) {
// prepareTree is required to find, which receivers have backward projected nodes
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, parent.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, parent.get(), &contextFactory, 0, 0));
TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
@@ -634,7 +635,7 @@ namespace {
static int drawNode(RenderThread& renderThread, const sp<RenderNode>& renderNode) {
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, renderNode.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, renderNode.get(), &contextFactory, 0, 0));
TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp
index 61bd646b0a76..80796f4a4111 100644
--- a/libs/hwui/tests/unit/RenderNodeTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeTests.cpp
@@ -274,7 +274,7 @@ RENDERTHREAD_TEST(RenderNode, prepareTree_nullableDisplayList) {
auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0));
TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
@@ -310,7 +310,7 @@ RENDERTHREAD_TEST(DISABLED_RenderNode, prepareTree_HwLayer_AVD_enqueueDamage) {
});
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0));
canvasContext->setSurface(nullptr);
TreeInfo info(TreeInfo::MODE_RT_ONLY, *canvasContext.get());
DamageAccumulator damageAccumulator;
diff --git a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
index dc1b2e668dd0..c1ddbd36bcfd 100644
--- a/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
+++ b/libs/hwui/tests/unit/SkiaBehaviorTests.cpp
@@ -16,9 +16,14 @@
#include "tests/common/TestUtils.h"
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkColor.h>
#include <SkColorMatrixFilter.h>
#include <SkColorSpace.h>
-#include <SkImagePriv.h>
+#include <SkImageInfo.h>
+#include <SkPaint.h>
+#include <SkPath.h>
#include <SkPathOps.h>
#include <SkShader.h>
#include <gtest/gtest.h>
diff --git a/libs/hwui/tests/unit/SkiaCanvasTests.cpp b/libs/hwui/tests/unit/SkiaCanvasTests.cpp
index dae3c9435712..87c52161d68e 100644
--- a/libs/hwui/tests/unit/SkiaCanvasTests.cpp
+++ b/libs/hwui/tests/unit/SkiaCanvasTests.cpp
@@ -17,9 +17,19 @@
#include "tests/common/TestUtils.h"
#include <hwui/Paint.h>
+#include <SkAlphaType.h>
+#include <SkBitmap.h>
+#include <SkBlendMode.h>
+#include <SkCanvas.h>
#include <SkCanvasStateUtils.h>
+#include <SkColor.h>
+#include <SkColorSpace.h>
+#include <SkColorType.h>
+#include <SkImageInfo.h>
#include <SkPicture.h>
#include <SkPictureRecorder.h>
+#include <SkRefCnt.h>
+#include <SkSurface.h>
#include <gtest/gtest.h>
using namespace android;
diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
index 3d5aca4bf05a..f825d7c5d9cc 100644
--- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
+++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
@@ -142,7 +142,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren) {
auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0));
TreeInfo info(TreeInfo::MODE_FULL, *canvasContext.get());
DamageAccumulator damageAccumulator;
info.damageAccumulator = &damageAccumulator;
@@ -201,7 +201,7 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaDisplayList, prepareListAndChildren_vdOffscr
auto rootNode = TestUtils::createNode(0, 0, 200, 400, nullptr);
ContextFactory contextFactory;
std::unique_ptr<CanvasContext> canvasContext(
- CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory));
+ CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0));
// Set up a Surface so that we can position the VectorDrawable offscreen.
test::TestContext testContext;
diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp
index 7419f8fd89f1..4d0595e03da6 100644
--- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp
+++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp
@@ -17,6 +17,7 @@
#include <VectorDrawable.h>
#include <gtest/gtest.h>
+#include <SkBlendMode.h>
#include <SkClipStack.h>
#include <SkSurface_Base.h>
#include <string.h>
diff --git a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp
index 15ecf5831f3a..ced667eb76e5 100644
--- a/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp
+++ b/libs/hwui/tests/unit/SkiaRenderPropertiesTests.cpp
@@ -17,6 +17,7 @@
#include <VectorDrawable.h>
#include <gtest/gtest.h>
+#include <SkCanvas.h>
#include <SkClipStack.h>
#include <SkSurface_Base.h>
#include <string.h>
diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp
index ab23448ab93f..499afa039d1f 100644
--- a/libs/hwui/tests/unit/TypefaceTests.cpp
+++ b/libs/hwui/tests/unit/TypefaceTests.cpp
@@ -21,8 +21,11 @@
#include <sys/stat.h>
#include <utils/Log.h>
+#include "SkData.h"
#include "SkFontMgr.h"
+#include "SkRefCnt.h"
#include "SkStream.h"
+#include "SkTypeface.h"
#include "hwui/MinikinSkia.h"
#include "hwui/Typeface.h"
@@ -61,7 +64,7 @@ std::shared_ptr<minikin::FontFamily> buildFamily(const char* fileName) {
std::vector<minikin::FontVariation>());
std::vector<std::shared_ptr<minikin::Font>> fonts;
fonts.push_back(minikin::Font::Builder(font).build());
- return std::make_shared<minikin::FontFamily>(std::move(fonts));
+ return minikin::FontFamily::create(std::move(fonts));
}
std::vector<std::shared_ptr<minikin::FontFamily>> makeSingleFamlyVector(const char* fileName) {
@@ -70,7 +73,8 @@ std::vector<std::shared_ptr<minikin::FontFamily>> makeSingleFamlyVector(const ch
TEST(TypefaceTest, resolveDefault_and_setDefaultTest) {
std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
- makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
+ nullptr /* fallback */));
EXPECT_EQ(regular.get(), Typeface::resolveDefault(regular.get()));
// Keep the original to restore it later.
@@ -348,24 +352,24 @@ TEST(TypefaceTest, createAbsolute) {
TEST(TypefaceTest, createFromFamilies_Single) {
// In Java, new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build();
- std::unique_ptr<Typeface> regular(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, false));
+ std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
+ makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */));
EXPECT_EQ(400, regular->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
// In Java, new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build();
- std::unique_ptr<Typeface> bold(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, false));
+ std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
+ makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */));
EXPECT_EQ(700, bold->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
// In Java, new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build();
- std::unique_ptr<Typeface> italic(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, true));
+ std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
+ makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */));
EXPECT_EQ(400, italic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
@@ -373,8 +377,8 @@ TEST(TypefaceTest, createFromFamilies_Single) {
// In Java,
// new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build();
- std::unique_ptr<Typeface> boldItalic(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, true));
+ std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
+ makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */));
EXPECT_EQ(700, boldItalic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
@@ -382,8 +386,8 @@ TEST(TypefaceTest, createFromFamilies_Single) {
// In Java,
// new
// Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build();
- std::unique_ptr<Typeface> over1000(
- Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 1100, false));
+ std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies(
+ makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */));
EXPECT_EQ(1000, over1000->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
@@ -391,30 +395,33 @@ TEST(TypefaceTest, createFromFamilies_Single) {
TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
// In Java, new Typeface.Builder("Family-Regular.ttf").build();
- std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
- makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ std::unique_ptr<Typeface> regular(
+ Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
+ RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
EXPECT_EQ(400, regular->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
// In Java, new Typeface.Builder("Family-Bold.ttf").build();
- std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
- makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ std::unique_ptr<Typeface> bold(
+ Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE,
+ RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
EXPECT_EQ(700, bold->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
// In Java, new Typeface.Builder("Family-Italic.ttf").build();
- std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
- makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ std::unique_ptr<Typeface> italic(
+ Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE,
+ RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
EXPECT_EQ(400, italic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
// In Java, new Typeface.Builder("Family-BoldItalic.ttf").build();
- std::unique_ptr<Typeface> boldItalic(
- Typeface::createFromFamilies(makeSingleFamlyVector(kBoldItalicFont),
- RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
+ makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
+ nullptr /* fallback */));
EXPECT_EQ(700, boldItalic->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
@@ -424,8 +431,9 @@ TEST(TypefaceTest, createFromFamilies_Family) {
std::vector<std::shared_ptr<minikin::FontFamily>> families = {
buildFamily(kRegularFont), buildFamily(kBoldFont), buildFamily(kItalicFont),
buildFamily(kBoldItalicFont)};
- std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies(
- std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ std::unique_ptr<Typeface> typeface(
+ Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
+ RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
EXPECT_EQ(400, typeface->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
}
@@ -433,10 +441,24 @@ TEST(TypefaceTest, createFromFamilies_Family) {
TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) {
std::vector<std::shared_ptr<minikin::FontFamily>> families = {
buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)};
- std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies(
- std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ std::unique_ptr<Typeface> typeface(
+ Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
+ RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
EXPECT_EQ(700, typeface->fStyle.weight());
EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
}
+TEST(TypefaceTest, createFromFamilies_Family_withFallback) {
+ std::vector<std::shared_ptr<minikin::FontFamily>> fallbackFamilies = {
+ buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)};
+ std::unique_ptr<Typeface> fallback(
+ Typeface::createFromFamilies(std::move(fallbackFamilies), RESOLVE_BY_FONT_TABLE,
+ RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
+ std::unique_ptr<Typeface> regular(
+ Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
+ RESOLVE_BY_FONT_TABLE, fallback.get()));
+ EXPECT_EQ(400, regular->fStyle.weight());
+ EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
+}
+
} // namespace
diff --git a/libs/hwui/tests/unit/VectorDrawableTests.cpp b/libs/hwui/tests/unit/VectorDrawableTests.cpp
index 6d4c57413f00..c1c21bd7dfbf 100644
--- a/libs/hwui/tests/unit/VectorDrawableTests.cpp
+++ b/libs/hwui/tests/unit/VectorDrawableTests.cpp
@@ -21,6 +21,12 @@
#include "utils/MathUtils.h"
#include "utils/VectorDrawableUtils.h"
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkPath.h>
+#include <SkRefCnt.h>
+#include <SkShader.h>
+
#include <functional>
namespace android {
diff --git a/libs/hwui/utils/AutoMalloc.h b/libs/hwui/utils/AutoMalloc.h
new file mode 100644
index 000000000000..05f5e9f24133
--- /dev/null
+++ b/libs/hwui/utils/AutoMalloc.h
@@ -0,0 +1,94 @@
+/**
+ * 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 <cstdlib>
+#include <memory>
+#include <type_traits>
+
+namespace android {
+namespace uirenderer {
+
+/** Manages an array of T elements, freeing the array in the destructor.
+ * Does NOT call any constructors/destructors on T (T must be POD).
+ */
+template <typename T,
+ typename = std::enable_if_t<std::is_trivially_default_constructible<T>::value &&
+ std::is_trivially_destructible<T>::value>>
+class AutoTMalloc {
+public:
+ /** Takes ownership of the ptr. The ptr must be a value which can be passed to std::free. */
+ explicit AutoTMalloc(T* ptr = nullptr) : fPtr(ptr) {}
+
+ /** Allocates space for 'count' Ts. */
+ explicit AutoTMalloc(size_t count) : fPtr(mallocIfCountThrowOnFail(count)) {}
+
+ AutoTMalloc(AutoTMalloc&&) = default;
+ AutoTMalloc& operator=(AutoTMalloc&&) = default;
+
+ /** Resize the memory area pointed to by the current ptr preserving contents. */
+ void realloc(size_t count) { fPtr.reset(reallocIfCountThrowOnFail(count)); }
+
+ /** Resize the memory area pointed to by the current ptr without preserving contents. */
+ T* reset(size_t count = 0) {
+ fPtr.reset(mallocIfCountThrowOnFail(count));
+ return this->get();
+ }
+
+ T* get() const { return fPtr.get(); }
+
+ operator T*() { return fPtr.get(); }
+
+ operator const T*() const { return fPtr.get(); }
+
+ T& operator[](int index) { return fPtr.get()[index]; }
+
+ const T& operator[](int index) const { return fPtr.get()[index]; }
+
+ /**
+ * Transfer ownership of the ptr to the caller, setting the internal
+ * pointer to NULL. Note that this differs from get(), which also returns
+ * the pointer, but it does not transfer ownership.
+ */
+ T* release() { return fPtr.release(); }
+
+private:
+ struct FreeDeleter {
+ void operator()(uint8_t* p) { std::free(p); }
+ };
+ std::unique_ptr<T, FreeDeleter> fPtr;
+
+ T* mallocIfCountThrowOnFail(size_t count) {
+ T* newPtr = nullptr;
+ if (count) {
+ newPtr = (T*)std::malloc(count * sizeof(T));
+ LOG_ALWAYS_FATAL_IF(!newPtr, "failed to malloc %zu bytes", count * sizeof(T));
+ }
+ return newPtr;
+ }
+ T* reallocIfCountThrowOnFail(size_t count) {
+ T* newPtr = nullptr;
+ if (count) {
+ newPtr = (T*)std::realloc(fPtr.release(), count * sizeof(T));
+ LOG_ALWAYS_FATAL_IF(!newPtr, "failed to realloc %zu bytes", count * sizeof(T));
+ }
+ return newPtr;
+ }
+};
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
index 3afb419f9b8b..913af8ac3474 100644
--- a/libs/hwui/utils/Color.cpp
+++ b/libs/hwui/utils/Color.cpp
@@ -101,6 +101,26 @@ uint32_t ColorTypeToBufferFormat(SkColorType colorType) {
return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
}
}
+
+SkColorType BufferFormatToColorType(uint32_t format) {
+ switch (format) {
+ case AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM:
+ return kN32_SkColorType;
+ case AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM:
+ return kN32_SkColorType;
+ case AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM:
+ return kRGB_565_SkColorType;
+ case AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM:
+ return kRGBA_1010102_SkColorType;
+ case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT:
+ return kRGBA_F16_SkColorType;
+ case AHARDWAREBUFFER_FORMAT_R8_UNORM:
+ return kAlpha_8_SkColorType;
+ default:
+ ALOGV("Unsupported format: %d, return unknown by default", format);
+ return kUnknown_SkColorType;
+ }
+}
#endif
namespace {
@@ -155,23 +175,12 @@ android_dataspace ColorSpaceToADataSpace(SkColorSpace* colorSpace, SkColorType c
skcms_TransferFunction fn;
if (!colorSpace->isNumericalTransferFn(&fn)) {
- // pq with the default white point
- auto rec2020PQ = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020);
- if (SkColorSpace::Equals(colorSpace, rec2020PQ.get())) {
- return HAL_DATASPACE_BT2020_PQ;
- }
- // standard PQ
- rec2020PQ = SkColorSpace::MakeRGB(SkNamedTransferFn::kPQ, SkNamedGamut::kRec2020);
- if (SkColorSpace::Equals(colorSpace, rec2020PQ.get())) {
+ auto res = skcms_TransferFunction_getType(&fn);
+ if (res == skcms_TFType_PQish) {
return HAL_DATASPACE_BT2020_PQ;
}
- // HLG
- const auto hlgFn = GetHLGScaleTransferFunction();
- if (hlgFn.has_value()) {
- auto rec2020HLG = SkColorSpace::MakeRGB(hlgFn.value(), SkNamedGamut::kRec2020);
- if (SkColorSpace::Equals(colorSpace, rec2020HLG.get())) {
- return static_cast<android_dataspace>(HAL_DATASPACE_BT2020_HLG);
- }
+ if (res == skcms_TFType_HLGish) {
+ return static_cast<android_dataspace>(HAL_DATASPACE_BT2020_HLG);
}
LOG_ALWAYS_FATAL("Only select non-numerical transfer functions are supported");
}
@@ -275,7 +284,9 @@ sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace) {
case HAL_DATASPACE_TRANSFER_GAMMA2_8:
return SkColorSpace::MakeRGB({2.8f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, gamut);
case HAL_DATASPACE_TRANSFER_ST2084:
- return SkColorSpace::MakeRGB(SkNamedTransferFn::kPQ, gamut);
+ return SkColorSpace::MakeRGB({-2.0, -1.555223, 1.860454, 32 / 2523.0, 2413 / 128.0,
+ -2392 / 128.0, 8192 / 1305.0},
+ gamut);
case HAL_DATASPACE_TRANSFER_SMPTE_170M:
return SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, gamut);
case HAL_DATASPACE_TRANSFER_UNSPECIFIED:
@@ -396,11 +407,32 @@ skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) {
return fn;
}
+static skcms_TransferFunction trfn_apply_gain(const skcms_TransferFunction trfn, float gain) {
+ float pow_gain_ginv = sk_float_pow(gain, 1 / trfn.g);
+ skcms_TransferFunction result;
+ result.g = trfn.g;
+ result.a = trfn.a * pow_gain_ginv;
+ result.b = trfn.b * pow_gain_ginv;
+ result.c = trfn.c * gain;
+ result.d = trfn.d;
+ result.e = trfn.e * gain;
+ result.f = trfn.f * gain;
+ return result;
+}
+
+skcms_TransferFunction GetExtendedTransferFunction(float sdrHdrRatio) {
+ if (sdrHdrRatio <= 1.f) {
+ return SkNamedTransferFn::kSRGB;
+ }
+ // Scale the transfer by the sdrHdrRatio
+ return trfn_apply_gain(SkNamedTransferFn::kSRGB, sdrHdrRatio);
+}
+
// Skia skcms' default HLG maps encoded [0, 1] to linear [1, 12] in order to follow ARIB
-// but LinearEffect expects a decoded [0, 1] range instead to follow Rec 2100.
+// but LinearEffect expects to map 1.0 == 203 nits
std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction() {
skcms_TransferFunction hlgFn;
- if (skcms_TransferFunction_makeScaledHLGish(&hlgFn, 1.f / 12.f, 2.f, 2.f, 1.f / 0.17883277f,
+ if (skcms_TransferFunction_makeScaledHLGish(&hlgFn, 0.314509843, 2.f, 2.f, 1.f / 0.17883277f,
0.28466892f, 0.55991073f)) {
return std::make_optional<skcms_TransferFunction>(hlgFn);
}
diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h
index 00f910f45c38..0fd61c7b990b 100644
--- a/libs/hwui/utils/Color.h
+++ b/libs/hwui/utils/Color.h
@@ -100,6 +100,7 @@ SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc,
sk_sp<SkColorSpace> colorSpace);
uint32_t ColorTypeToBufferFormat(SkColorType colorType);
+SkColorType BufferFormatToColorType(uint32_t bufferFormat);
#endif
ANDROID_API sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace);
@@ -129,6 +130,7 @@ struct Lab {
Lab sRGBToLab(SkColor color);
SkColor LabToSRGB(const Lab& lab, SkAlpha alpha);
skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level = 0.f);
+skcms_TransferFunction GetExtendedTransferFunction(float sdrHdrRatio);
std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction();
} /* namespace uirenderer */
diff --git a/libs/hwui/utils/PaintUtils.h b/libs/hwui/utils/PaintUtils.h
index 94bcb1110e05..f44f9d0fe2d4 100644
--- a/libs/hwui/utils/PaintUtils.h
+++ b/libs/hwui/utils/PaintUtils.h
@@ -19,6 +19,7 @@
#include <GLES2/gl2.h>
#include <utils/Blur.h>
+#include <SkBlendMode.h>
#include <SkColorFilter.h>
#include <SkPaint.h>
#include <SkShader.h>
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index 45da008c3e8e..c3ad7670d473 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -22,14 +22,9 @@
#include "MouseCursorController.h"
+#include <input/Input.h>
#include <log/log.h>
-#include <SkBitmap.h>
-#include <SkBlendMode.h>
-#include <SkCanvas.h>
-#include <SkColor.h>
-#include <SkPaint.h>
-
namespace {
// Time to spend fading out the pointer completely.
const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms
@@ -43,6 +38,8 @@ MouseCursorController::MouseCursorController(PointerControllerContext& context)
: mContext(context) {
std::scoped_lock lock(mLock);
+ mLocked.stylusHoverMode = false;
+
mLocked.animationFrameIndex = 0;
mLocked.lastFrameUpdatedTime = 0;
@@ -52,11 +49,10 @@ MouseCursorController::MouseCursorController(PointerControllerContext& context)
mLocked.pointerAlpha = 0.0f; // pointer is initially faded
mLocked.pointerSprite = mContext.getSpriteController()->createSprite();
mLocked.updatePointerIcon = false;
- mLocked.requestedPointerType = mContext.getPolicy()->getDefaultPointerIconId();
+ mLocked.requestedPointerType = PointerIconStyle::TYPE_NOT_SPECIFIED;
+ mLocked.resolvedPointerType = PointerIconStyle::TYPE_NOT_SPECIFIED;
mLocked.resourcesLoaded = false;
-
- mLocked.buttonState = 0;
}
MouseCursorController::~MouseCursorController() {
@@ -65,24 +61,23 @@ MouseCursorController::~MouseCursorController() {
mLocked.pointerSprite.clear();
}
-bool MouseCursorController::getBounds(float* outMinX, float* outMinY, float* outMaxX,
- float* outMaxY) const {
+std::optional<FloatRect> MouseCursorController::getBounds() const {
std::scoped_lock lock(mLock);
- return getBoundsLocked(outMinX, outMinY, outMaxX, outMaxY);
+ return getBoundsLocked();
}
-bool MouseCursorController::getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX,
- float* outMaxY) const REQUIRES(mLock) {
+std::optional<FloatRect> MouseCursorController::getBoundsLocked() const REQUIRES(mLock) {
if (!mLocked.viewport.isValid()) {
- return false;
+ return {};
}
- *outMinX = mLocked.viewport.logicalLeft;
- *outMinY = mLocked.viewport.logicalTop;
- *outMaxX = mLocked.viewport.logicalRight - 1;
- *outMaxY = mLocked.viewport.logicalBottom - 1;
- return true;
+ return FloatRect{
+ static_cast<float>(mLocked.viewport.logicalLeft),
+ static_cast<float>(mLocked.viewport.logicalTop),
+ static_cast<float>(mLocked.viewport.logicalRight - 1),
+ static_cast<float>(mLocked.viewport.logicalBottom - 1),
+ };
}
void MouseCursorController::move(float deltaX, float deltaY) {
@@ -98,22 +93,6 @@ void MouseCursorController::move(float deltaX, float deltaY) {
setPositionLocked(mLocked.pointerX + deltaX, mLocked.pointerY + deltaY);
}
-void MouseCursorController::setButtonState(int32_t buttonState) {
-#if DEBUG_MOUSE_CURSOR_UPDATES
- ALOGD("Set button state 0x%08x", buttonState);
-#endif
- std::scoped_lock lock(mLock);
-
- if (mLocked.buttonState != buttonState) {
- mLocked.buttonState = buttonState;
- }
-}
-
-int32_t MouseCursorController::getButtonState() const {
- std::scoped_lock lock(mLock);
- return mLocked.buttonState;
-}
-
void MouseCursorController::setPosition(float x, float y) {
#if DEBUG_MOUSE_CURSOR_UPDATES
ALOGD("Set pointer position to x=%0.3f, y=%0.3f", x, y);
@@ -123,31 +102,19 @@ void MouseCursorController::setPosition(float x, float y) {
}
void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
- float minX, minY, maxX, maxY;
- if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
- if (x <= minX) {
- mLocked.pointerX = minX;
- } else if (x >= maxX) {
- mLocked.pointerX = maxX;
- } else {
- mLocked.pointerX = x;
- }
- if (y <= minY) {
- mLocked.pointerY = minY;
- } else if (y >= maxY) {
- mLocked.pointerY = maxY;
- } else {
- mLocked.pointerY = y;
- }
- updatePointerLocked();
- }
+ const auto bounds = getBoundsLocked();
+ if (!bounds) return;
+
+ mLocked.pointerX = std::max(bounds->left, std::min(bounds->right, x));
+ mLocked.pointerY = std::max(bounds->top, std::min(bounds->bottom, y));
+
+ updatePointerLocked();
}
-void MouseCursorController::getPosition(float* outX, float* outY) const {
+FloatPoint MouseCursorController::getPosition() const {
std::scoped_lock lock(mLock);
- *outX = mLocked.pointerX;
- *outY = mLocked.pointerY;
+ return {mLocked.pointerX, mLocked.pointerY};
}
int32_t MouseCursorController::getDisplayId() const {
@@ -189,6 +156,15 @@ void MouseCursorController::unfade(PointerControllerInterface::Transition transi
}
}
+void MouseCursorController::setStylusHoverMode(bool stylusHoverMode) {
+ std::scoped_lock lock(mLock);
+
+ if (mLocked.stylusHoverMode != stylusHoverMode) {
+ mLocked.stylusHoverMode = stylusHoverMode;
+ mLocked.updatePointerIcon = true;
+ }
+}
+
void MouseCursorController::reloadPointerResources(bool getAdditionalMouseResources) {
std::scoped_lock lock(mLock);
@@ -204,8 +180,7 @@ static void getNonRotatedSize(const DisplayViewport& viewport, int32_t& width, i
width = viewport.deviceWidth;
height = viewport.deviceHeight;
- if (viewport.orientation == DISPLAY_ORIENTATION_90 ||
- viewport.orientation == DISPLAY_ORIENTATION_270) {
+ if (viewport.orientation == ui::ROTATION_90 || viewport.orientation == ui::ROTATION_270) {
std::swap(width, height);
}
}
@@ -229,10 +204,9 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport,
// Reset cursor position to center if size or display changed.
if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth ||
oldDisplayHeight != newDisplayHeight) {
- float minX, minY, maxX, maxY;
- if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
- mLocked.pointerX = (minX + maxX) * 0.5f;
- mLocked.pointerY = (minY + maxY) * 0.5f;
+ if (const auto bounds = getBoundsLocked(); bounds) {
+ mLocked.pointerX = (bounds->left + bounds->right) * 0.5f;
+ mLocked.pointerY = (bounds->top + bounds->bottom) * 0.5f;
// Reload icon resources for density may be changed.
loadResourcesLocked(getAdditionalMouseResources);
} else {
@@ -249,38 +223,42 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport,
// Undo the previous rotation.
switch (oldViewport.orientation) {
- case DISPLAY_ORIENTATION_90:
+ case ui::ROTATION_90:
temp = x;
x = oldViewport.deviceHeight - y;
y = temp;
break;
- case DISPLAY_ORIENTATION_180:
+ case ui::ROTATION_180:
x = oldViewport.deviceWidth - x;
y = oldViewport.deviceHeight - y;
break;
- case DISPLAY_ORIENTATION_270:
+ case ui::ROTATION_270:
temp = x;
x = y;
y = oldViewport.deviceWidth - temp;
break;
+ case ui::ROTATION_0:
+ break;
}
// Perform the new rotation.
switch (viewport.orientation) {
- case DISPLAY_ORIENTATION_90:
+ case ui::ROTATION_90:
temp = x;
x = y;
y = viewport.deviceHeight - temp;
break;
- case DISPLAY_ORIENTATION_180:
+ case ui::ROTATION_180:
x = viewport.deviceWidth - x;
y = viewport.deviceHeight - y;
break;
- case DISPLAY_ORIENTATION_270:
+ case ui::ROTATION_270:
temp = x;
x = viewport.deviceWidth - y;
y = temp;
break;
+ case ui::ROTATION_0:
+ break;
}
// Apply offsets to convert from the pixel center to the pixel top-left corner position
@@ -292,7 +270,7 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport,
updatePointerLocked();
}
-void MouseCursorController::updatePointerIcon(int32_t iconId) {
+void MouseCursorController::updatePointerIcon(PointerIconStyle iconId) {
std::scoped_lock lock(mLock);
if (mLocked.requestedPointerType != iconId) {
@@ -305,7 +283,7 @@ void MouseCursorController::updatePointerIcon(int32_t iconId) {
void MouseCursorController::setCustomPointerIcon(const SpriteIcon& icon) {
std::scoped_lock lock(mLock);
- const int32_t iconId = mContext.getPolicy()->getCustomPointerIconId();
+ const PointerIconStyle iconId = mContext.getPolicy()->getCustomPointerIconId();
mLocked.additionalMouseResources[iconId] = icon;
mLocked.requestedPointerType = iconId;
mLocked.updatePointerIcon = true;
@@ -340,8 +318,8 @@ bool MouseCursorController::doFadingAnimationLocked(nsecs_t timestamp) REQUIRES(
}
bool MouseCursorController::doBitmapAnimationLocked(nsecs_t timestamp) REQUIRES(mLock) {
- std::map<int32_t, PointerAnimation>::const_iterator iter =
- mLocked.animationResources.find(mLocked.requestedPointerType);
+ std::map<PointerIconStyle, PointerAnimation>::const_iterator iter =
+ mLocked.animationResources.find(mLocked.resolvedPointerType);
if (iter == mLocked.animationResources.end()) {
return false;
}
@@ -383,14 +361,23 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) {
}
if (mLocked.updatePointerIcon) {
- if (mLocked.requestedPointerType == mContext.getPolicy()->getDefaultPointerIconId()) {
+ mLocked.resolvedPointerType = mLocked.requestedPointerType;
+ const PointerIconStyle defaultPointerIconId =
+ mContext.getPolicy()->getDefaultPointerIconId();
+ if (mLocked.resolvedPointerType == PointerIconStyle::TYPE_NOT_SPECIFIED) {
+ mLocked.resolvedPointerType = mLocked.stylusHoverMode
+ ? mContext.getPolicy()->getDefaultStylusIconId()
+ : defaultPointerIconId;
+ }
+
+ if (mLocked.resolvedPointerType == defaultPointerIconId) {
mLocked.pointerSprite->setIcon(mLocked.pointerIcon);
} else {
- std::map<int32_t, SpriteIcon>::const_iterator iter =
- mLocked.additionalMouseResources.find(mLocked.requestedPointerType);
+ std::map<PointerIconStyle, SpriteIcon>::const_iterator iter =
+ mLocked.additionalMouseResources.find(mLocked.resolvedPointerType);
if (iter != mLocked.additionalMouseResources.end()) {
- std::map<int32_t, PointerAnimation>::const_iterator anim_iter =
- mLocked.animationResources.find(mLocked.requestedPointerType);
+ std::map<PointerIconStyle, PointerAnimation>::const_iterator anim_iter =
+ mLocked.animationResources.find(mLocked.resolvedPointerType);
if (anim_iter != mLocked.animationResources.end()) {
mLocked.animationFrameIndex = 0;
mLocked.lastFrameUpdatedTime = systemTime(SYSTEM_TIME_MONOTONIC);
@@ -398,7 +385,7 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) {
}
mLocked.pointerSprite->setIcon(iter->second);
} else {
- ALOGW("Can't find the resource for icon id %d", mLocked.requestedPointerType);
+ ALOGW("Can't find the resource for icon id %d", mLocked.resolvedPointerType);
mLocked.pointerSprite->setIcon(mLocked.pointerIcon);
}
}
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index c0ab58bd2e7e..00dc0854440e 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -43,18 +43,17 @@ public:
MouseCursorController(PointerControllerContext& context);
~MouseCursorController();
- bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
+ std::optional<FloatRect> getBounds() const;
void move(float deltaX, float deltaY);
- void setButtonState(int32_t buttonState);
- int32_t getButtonState() const;
void setPosition(float x, float y);
- void getPosition(float* outX, float* outY) const;
+ FloatPoint getPosition() const;
int32_t getDisplayId() const;
void fade(PointerControllerInterface::Transition transition);
void unfade(PointerControllerInterface::Transition transition);
void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources);
+ void setStylusHoverMode(bool stylusHoverMode);
- void updatePointerIcon(int32_t iconId);
+ void updatePointerIcon(PointerIconStyle iconId);
void setCustomPointerIcon(const SpriteIcon& icon);
void reloadPointerResources(bool getAdditionalMouseResources);
@@ -74,6 +73,7 @@ private:
struct Locked {
DisplayViewport viewport;
+ bool stylusHoverMode;
size_t animationFrameIndex;
nsecs_t lastFrameUpdatedTime;
@@ -88,18 +88,17 @@ private:
bool resourcesLoaded;
- std::map<int32_t, SpriteIcon> additionalMouseResources;
- std::map<int32_t, PointerAnimation> animationResources;
+ std::map<PointerIconStyle, SpriteIcon> additionalMouseResources;
+ std::map<PointerIconStyle, PointerAnimation> animationResources;
- int32_t requestedPointerType;
-
- int32_t buttonState;
+ PointerIconStyle requestedPointerType;
+ PointerIconStyle resolvedPointerType;
bool animating{false};
} mLocked GUARDED_BY(mLock);
- bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
+ std::optional<FloatRect> getBoundsLocked() const;
void setPositionLocked(float x, float y);
void updatePointerLocked();
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 10ea6512c724..e21d6fb2fe14 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -22,10 +22,18 @@
#include <SkBlendMode.h>
#include <SkCanvas.h>
#include <SkColor.h>
+#include <android-base/stringprintf.h>
#include <android-base/thread_annotations.h>
+#include <ftl/enum.h>
+
+#include <mutex>
#include "PointerControllerContext.h"
+#define INDENT " "
+#define INDENT2 " "
+#define INDENT3 " "
+
namespace android {
namespace {
@@ -37,14 +45,13 @@ const ui::Transform kIdentityTransform;
// --- PointerController::DisplayInfoListener ---
void PointerController::DisplayInfoListener::onWindowInfosChanged(
- const std::vector<android::gui::WindowInfo>&,
- const std::vector<android::gui::DisplayInfo>& displayInfos) {
+ const gui::WindowInfosUpdate& update) {
std::scoped_lock lock(mLock);
if (mPointerController == nullptr) return;
// PointerController uses DisplayInfoListener's lock.
base::ScopedLockAssertion assumeLocked(mPointerController->getLock());
- mPointerController->onDisplayInfosChangedLocked(displayInfos);
+ mPointerController->onDisplayInfosChangedLocked(update.displayInfos);
}
void PointerController::DisplayInfoListener::onPointerControllerDestroyed() {
@@ -106,16 +113,15 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>&
PointerController::~PointerController() {
mDisplayInfoListener->onPointerControllerDestroyed();
mUnregisterWindowInfosListener(mDisplayInfoListener);
- mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, 0, 0);
+ mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0});
}
std::mutex& PointerController::getLock() const {
return mDisplayInfoListener->mLock;
}
-bool PointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX,
- float* outMaxY) const {
- return mCursorController.getBounds(outMinX, outMinY, outMaxX, outMaxY);
+std::optional<FloatRect> PointerController::getBounds() const {
+ return mCursorController.getBounds();
}
void PointerController::move(float deltaX, float deltaY) {
@@ -129,14 +135,6 @@ void PointerController::move(float deltaX, float deltaY) {
mCursorController.move(transformed.x, transformed.y);
}
-void PointerController::setButtonState(int32_t buttonState) {
- mCursorController.setButtonState(buttonState);
-}
-
-int32_t PointerController::getButtonState() const {
- return mCursorController.getButtonState();
-}
-
void PointerController::setPosition(float x, float y) {
const int32_t displayId = mCursorController.getDisplayId();
vec2 transformed;
@@ -148,15 +146,13 @@ void PointerController::setPosition(float x, float y) {
mCursorController.setPosition(transformed.x, transformed.y);
}
-void PointerController::getPosition(float* outX, float* outY) const {
+FloatPoint PointerController::getPosition() const {
const int32_t displayId = mCursorController.getDisplayId();
- mCursorController.getPosition(outX, outY);
+ const auto p = mCursorController.getPosition();
{
std::scoped_lock lock(getLock());
const auto& transform = getTransformForDisplayLocked(displayId);
- const auto xy = transform.inverse().transform(*outX, *outY);
- *outX = xy.x;
- *outY = xy.y;
+ return FloatPoint{transform.inverse().transform(p.x, p.y)};
}
}
@@ -187,7 +183,11 @@ void PointerController::setPresentation(Presentation presentation) {
return;
}
- if (presentation == Presentation::POINTER) {
+ if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) {
+ // For now, we support stylus hover using the mouse cursor implementation.
+ // TODO: Add proper support for stylus hover icons.
+ mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
+
mCursorController.getAdditionalMouseResources();
clearSpotsLocked();
}
@@ -223,7 +223,7 @@ void PointerController::clearSpots() {
}
void PointerController::clearSpotsLocked() {
- for (auto& [displayID, spotController] : mLocked.spotControllers) {
+ for (auto& [displayId, spotController] : mLocked.spotControllers) {
spotController.clearSpots();
}
}
@@ -235,13 +235,14 @@ void PointerController::setInactivityTimeout(InactivityTimeout inactivityTimeout
void PointerController::reloadPointerResources() {
std::scoped_lock lock(getLock());
- for (auto& [displayID, spotController] : mLocked.spotControllers) {
+ for (auto& [displayId, spotController] : mLocked.spotControllers) {
spotController.reloadSpotResources();
}
if (mCursorController.resourcesLoaded()) {
bool getAdditionalMouseResources = false;
- if (mLocked.presentation == PointerController::Presentation::POINTER) {
+ if (mLocked.presentation == PointerController::Presentation::POINTER ||
+ mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) {
getAdditionalMouseResources = true;
}
mCursorController.reloadPointerResources(getAdditionalMouseResources);
@@ -249,22 +250,35 @@ void PointerController::reloadPointerResources() {
}
void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
- std::scoped_lock lock(getLock());
+ struct PointerDisplayChangeArgs {
+ int32_t displayId;
+ FloatPoint cursorPosition;
+ };
+ std::optional<PointerDisplayChangeArgs> pointerDisplayChanged;
- bool getAdditionalMouseResources = false;
- if (mLocked.presentation == PointerController::Presentation::POINTER) {
- getAdditionalMouseResources = true;
- }
- mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
- if (viewport.displayId != mLocked.pointerDisplayId) {
- float xPos, yPos;
- mCursorController.getPosition(&xPos, &yPos);
- mContext.getPolicy()->onPointerDisplayIdChanged(viewport.displayId, xPos, yPos);
- mLocked.pointerDisplayId = viewport.displayId;
+ { // acquire lock
+ std::scoped_lock lock(getLock());
+
+ bool getAdditionalMouseResources = false;
+ if (mLocked.presentation == PointerController::Presentation::POINTER ||
+ mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) {
+ getAdditionalMouseResources = true;
+ }
+ mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
+ if (viewport.displayId != mLocked.pointerDisplayId) {
+ mLocked.pointerDisplayId = viewport.displayId;
+ pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()};
+ }
+ } // release lock
+
+ if (pointerDisplayChanged) {
+ // Notify the policy without holding the pointer controller lock.
+ mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId,
+ pointerDisplayChanged->cursorPosition);
}
}
-void PointerController::updatePointerIcon(int32_t iconId) {
+void PointerController::updatePointerIcon(PointerIconStyle iconId) {
std::scoped_lock lock(getLock());
mCursorController.updatePointerIcon(iconId);
}
@@ -286,13 +300,13 @@ void PointerController::onDisplayViewportsUpdated(std::vector<DisplayViewport>&
std::scoped_lock lock(getLock());
for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) {
- int32_t displayID = it->first;
- if (!displayIdSet.count(displayID)) {
+ int32_t displayId = it->first;
+ if (!displayIdSet.count(displayId)) {
/*
* Ensures that an in-progress animation won't dereference
* a null pointer to TouchSpotController.
*/
- mContext.removeAnimationCallback(displayID);
+ mContext.removeAnimationCallback(displayId);
it = mLocked.spotControllers.erase(it);
} else {
++it;
@@ -313,4 +327,20 @@ const ui::Transform& PointerController::getTransformForDisplayLocked(int display
return it != di.end() ? it->transform : kIdentityTransform;
}
+void PointerController::dump(std::string& dump) {
+ dump += INDENT "PointerController:\n";
+ std::scoped_lock lock(getLock());
+ dump += StringPrintf(INDENT2 "Presentation: %s\n",
+ ftl::enum_string(mLocked.presentation).c_str());
+ dump += StringPrintf(INDENT2 "Pointer Display ID: %" PRIu32 "\n", mLocked.pointerDisplayId);
+ dump += StringPrintf(INDENT2 "Viewports:\n");
+ for (const auto& info : mLocked.mDisplayInfos) {
+ info.dump(dump, INDENT3);
+ }
+ dump += INDENT2 "Spot Controllers:\n";
+ for (const auto& [_, spotController] : mLocked.spotControllers) {
+ spotController.dump(dump, INDENT3);
+ }
+}
+
} // namespace android
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index eab030f71e1a..62ee74331302 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -19,6 +19,7 @@
#include <PointerControllerInterface.h>
#include <gui/DisplayEventReceiver.h>
+#include <gui/WindowInfosUpdate.h>
#include <input/DisplayViewport.h>
#include <input/Input.h>
#include <utils/BitSet.h>
@@ -27,6 +28,7 @@
#include <map>
#include <memory>
+#include <string>
#include <vector>
#include "MouseCursorController.h"
@@ -49,23 +51,21 @@ public:
~PointerController() override;
- virtual bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
- virtual void move(float deltaX, float deltaY);
- virtual void setButtonState(int32_t buttonState);
- virtual int32_t getButtonState() const;
- virtual void setPosition(float x, float y);
- virtual void getPosition(float* outX, float* outY) const;
- virtual int32_t getDisplayId() const;
- virtual void fade(Transition transition);
- virtual void unfade(Transition transition);
- virtual void setDisplayViewport(const DisplayViewport& viewport);
-
- virtual void setPresentation(Presentation presentation);
- virtual void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
- BitSet32 spotIdBits, int32_t displayId);
- virtual void clearSpots();
-
- void updatePointerIcon(int32_t iconId);
+ std::optional<FloatRect> getBounds() const override;
+ void move(float deltaX, float deltaY) override;
+ void setPosition(float x, float y) override;
+ FloatPoint getPosition() const override;
+ int32_t getDisplayId() const override;
+ void fade(Transition transition) override;
+ void unfade(Transition transition) override;
+ void setDisplayViewport(const DisplayViewport& viewport) override;
+
+ void setPresentation(Presentation presentation) override;
+ void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
+ BitSet32 spotIdBits, int32_t displayId) override;
+ void clearSpots() override;
+
+ void updatePointerIcon(PointerIconStyle iconId);
void setCustomPointerIcon(const SpriteIcon& icon);
void setInactivityTimeout(InactivityTimeout inactivityTimeout);
void doInactivityTimeout();
@@ -75,6 +75,8 @@ public:
void onDisplayInfosChangedLocked(const std::vector<gui::DisplayInfo>& displayInfos)
REQUIRES(getLock());
+ void dump(std::string& dump);
+
protected:
using WindowListenerConsumer =
std::function<void(const sp<android::gui::WindowInfosListener>&)>;
@@ -113,8 +115,7 @@ private:
class DisplayInfoListener : public gui::WindowInfosListener {
public:
explicit DisplayInfoListener(PointerController* pc) : mPointerController(pc){};
- void onWindowInfosChanged(const std::vector<android::gui::WindowInfo>&,
- const std::vector<android::gui::DisplayInfo>&) override;
+ void onWindowInfosChanged(const gui::WindowInfosUpdate&) override;
void onPointerControllerDestroyed();
// This lock is also used by PointerController. See PointerController::getLock().
diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h
index c2bc1e020279..f6f5d3bc51bd 100644
--- a/libs/input/PointerControllerContext.h
+++ b/libs/input/PointerControllerContext.h
@@ -75,11 +75,13 @@ public:
virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) = 0;
virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) = 0;
virtual void loadAdditionalMouseResources(
- std::map<int32_t, SpriteIcon>* outResources,
- std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) = 0;
- virtual int32_t getDefaultPointerIconId() = 0;
- virtual int32_t getCustomPointerIconId() = 0;
- virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0;
+ std::map<PointerIconStyle, SpriteIcon>* outResources,
+ std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
+ int32_t displayId) = 0;
+ virtual PointerIconStyle getDefaultPointerIconId() = 0;
+ virtual PointerIconStyle getDefaultStylusIconId() = 0;
+ virtual PointerIconStyle getCustomPointerIconId() = 0;
+ virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0;
};
/*
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index 2b809eab4ae4..130b204954b4 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -131,8 +131,9 @@ void SpriteController::doUpdateSprites() {
update.state.surfaceHeight = update.state.icon.height();
update.state.surfaceDrawn = false;
update.state.surfaceVisible = false;
- update.state.surfaceControl = obtainSurface(
- update.state.surfaceWidth, update.state.surfaceHeight);
+ update.state.surfaceControl =
+ obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight,
+ update.state.displayId);
if (update.state.surfaceControl != NULL) {
update.surfaceChanged = surfaceChanged = true;
}
@@ -168,8 +169,8 @@ void SpriteController::doUpdateSprites() {
}
}
- // If surface is a new one, we have to set right layer stack.
- if (update.surfaceChanged || update.state.dirty & DIRTY_DISPLAY_ID) {
+ // If surface has changed to a new display, we have to reparent it.
+ if (update.state.dirty & DIRTY_DISPLAY_ID) {
t.reparent(update.state.surfaceControl, mParentSurfaceProvider(update.state.displayId));
needApplyTransaction = true;
}
@@ -242,15 +243,14 @@ void SpriteController::doUpdateSprites() {
&& (becomingVisible
|| (update.state.dirty & (DIRTY_HOTSPOT | DIRTY_ICON_STYLE)))) {
Parcel p;
- p.writeInt32(update.state.icon.style);
+ p.writeInt32(static_cast<int32_t>(update.state.icon.style));
p.writeFloat(update.state.icon.hotSpotX);
p.writeFloat(update.state.icon.hotSpotY);
// Pass cursor metadata in the sprite surface so that when Android is running as a
// client OS (e.g. ARC++) the host OS can get the requested cursor metadata and
// update mouse cursor in the host OS.
- t.setMetadata(
- update.state.surfaceControl, METADATA_MOUSE_CURSOR, p);
+ t.setMetadata(update.state.surfaceControl, gui::METADATA_MOUSE_CURSOR, p);
}
int32_t surfaceLayer = mOverlayLayer + update.state.layer;
@@ -331,21 +331,28 @@ void SpriteController::ensureSurfaceComposerClient() {
}
}
-sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height) {
+sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height,
+ int32_t displayId) {
ensureSurfaceComposerClient();
- sp<SurfaceControl> surfaceControl = mSurfaceComposerClient->createSurface(
- String8("Sprite"), width, height, PIXEL_FORMAT_RGBA_8888,
- ISurfaceComposerClient::eHidden |
- ISurfaceComposerClient::eCursorWindow);
- if (surfaceControl == NULL || !surfaceControl->isValid()) {
+ const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId);
+ if (parent == nullptr) {
+ ALOGE("Failed to get the parent surface for pointers on display %d", displayId);
+ }
+
+ const sp<SurfaceControl> surfaceControl =
+ mSurfaceComposerClient->createSurface(String8("Sprite"), width, height,
+ PIXEL_FORMAT_RGBA_8888,
+ ISurfaceComposerClient::eHidden |
+ ISurfaceComposerClient::eCursorWindow,
+ parent ? parent->getHandle() : nullptr);
+ if (surfaceControl == nullptr || !surfaceControl->isValid()) {
ALOGE("Error creating sprite surface.");
- return NULL;
+ return nullptr;
}
return surfaceControl;
}
-
// --- SpriteController::SpriteImpl ---
SpriteController::SpriteImpl::SpriteImpl(const sp<SpriteController> controller) :
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 2e9cb9685c46..1f113c045360 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -265,7 +265,7 @@ private:
void doDisposeSurfaces();
void ensureSurfaceComposerClient();
- sp<SurfaceControl> obtainSurface(int32_t width, int32_t height);
+ sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId);
};
} // namespace android
diff --git a/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h
index a257d7e89ebc..5f085bbd2374 100644
--- a/libs/input/SpriteIcon.h
+++ b/libs/input/SpriteIcon.h
@@ -19,6 +19,7 @@
#include <android/graphics/bitmap.h>
#include <gui/Surface.h>
+#include <input/Input.h>
namespace android {
@@ -26,12 +27,13 @@ namespace android {
* Icon that a sprite displays, including its hotspot.
*/
struct SpriteIcon {
- inline SpriteIcon() : style(0), hotSpotX(0), hotSpotY(0) {}
- inline SpriteIcon(const graphics::Bitmap& bitmap, int32_t style, float hotSpotX, float hotSpotY)
+ inline SpriteIcon() : style(PointerIconStyle::TYPE_NULL), hotSpotX(0), hotSpotY(0) {}
+ inline SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX,
+ float hotSpotY)
: bitmap(bitmap), style(style), hotSpotX(hotSpotX), hotSpotY(hotSpotY) {}
graphics::Bitmap bitmap;
- int32_t style;
+ PointerIconStyle style;
float hotSpotX;
float hotSpotY;
@@ -41,7 +43,7 @@ struct SpriteIcon {
inline void reset() {
bitmap.reset();
- style = 0;
+ style = PointerIconStyle::TYPE_NULL;
hotSpotX = 0;
hotSpotY = 0;
}
diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp
index f7c685ff8ba6..d9fe5996bcff 100644
--- a/libs/input/TouchSpotController.cpp
+++ b/libs/input/TouchSpotController.cpp
@@ -21,13 +21,14 @@
#include "TouchSpotController.h"
+#include <android-base/stringprintf.h>
+#include <input/PrintTools.h>
#include <log/log.h>
-#include <SkBitmap.h>
-#include <SkBlendMode.h>
-#include <SkCanvas.h>
-#include <SkColor.h>
-#include <SkPaint.h>
+#include <mutex>
+
+#define INDENT " "
+#define INDENT2 " "
namespace {
// Time to spend fading out the spot completely.
@@ -59,6 +60,12 @@ void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float x, fl
}
}
+void TouchSpotController::Spot::dump(std::string& out, const char* prefix) const {
+ out += prefix;
+ base::StringAppendF(&out, "Spot{id=%" PRIx32 ", alpha=%f, scale=%f, pos=[%f, %f]}\n", id, alpha,
+ scale, x, y);
+}
+
// --- TouchSpotController ---
TouchSpotController::TouchSpotController(int32_t displayId, PointerControllerContext& context)
@@ -261,4 +268,22 @@ void TouchSpotController::startAnimationLocked() REQUIRES(mLock) {
mContext.addAnimationCallback(mDisplayId, func);
}
+void TouchSpotController::dump(std::string& out, const char* prefix) const {
+ using base::StringAppendF;
+ out += prefix;
+ out += "SpotController:\n";
+ out += prefix;
+ StringAppendF(&out, INDENT "DisplayId: %" PRId32 "\n", mDisplayId);
+ std::scoped_lock lock(mLock);
+ out += prefix;
+ StringAppendF(&out, INDENT "Animating: %s\n", toString(mLocked.animating));
+ out += prefix;
+ out += INDENT "Spots:\n";
+ std::string spotPrefix = prefix;
+ spotPrefix += INDENT2;
+ for (const auto& spot : mLocked.displaySpots) {
+ spot->dump(out, spotPrefix.c_str());
+ }
+}
+
} // namespace android
diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h
index 703de3603f48..5bbc75d9570b 100644
--- a/libs/input/TouchSpotController.h
+++ b/libs/input/TouchSpotController.h
@@ -38,6 +38,8 @@ public:
void reloadSpotResources();
bool doAnimations(nsecs_t timestamp);
+ void dump(std::string& out, const char* prefix = "") const;
+
private:
struct Spot {
static const uint32_t INVALID_ID = 0xffffffff;
@@ -58,6 +60,7 @@ private:
mLastIcon(nullptr) {}
void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId);
+ void dump(std::string& out, const char* prefix = "") const;
private:
const SpriteIcon* mLastIcon;
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index f9752ed155df..85747514aa03 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -14,17 +14,18 @@
* limitations under the License.
*/
-#include "mocks/MockSprite.h"
-#include "mocks/MockSpriteController.h"
-
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
#include <input/PointerController.h>
#include <input/SpriteController.h>
#include <atomic>
-#include <gmock/gmock.h>
-#include <gtest/gtest.h>
#include <thread>
+#include "input/Input.h"
+#include "mocks/MockSprite.h"
+#include "mocks/MockSpriteController.h"
+
namespace android {
enum TestCursorType {
@@ -34,12 +35,12 @@ enum TestCursorType {
CURSOR_TYPE_ANCHOR,
CURSOR_TYPE_ADDITIONAL,
CURSOR_TYPE_ADDITIONAL_ANIM,
+ CURSOR_TYPE_STYLUS,
CURSOR_TYPE_CUSTOM = -1,
};
using ::testing::AllOf;
using ::testing::Field;
-using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::Test;
@@ -52,11 +53,14 @@ class MockPointerControllerPolicyInterface : public PointerControllerPolicyInter
public:
virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) override;
virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) override;
- virtual void loadAdditionalMouseResources(std::map<int32_t, SpriteIcon>* outResources,
- std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) override;
- virtual int32_t getDefaultPointerIconId() override;
- virtual int32_t getCustomPointerIconId() override;
- virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override;
+ virtual void loadAdditionalMouseResources(
+ std::map<PointerIconStyle, SpriteIcon>* outResources,
+ std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
+ int32_t displayId) override;
+ virtual PointerIconStyle getDefaultPointerIconId() override;
+ virtual PointerIconStyle getDefaultStylusIconId() override;
+ virtual PointerIconStyle getCustomPointerIconId() override;
+ virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
bool allResourcesAreLoaded();
bool noResourcesAreLoaded();
@@ -85,34 +89,42 @@ void MockPointerControllerPolicyInterface::loadPointerResources(PointerResources
}
void MockPointerControllerPolicyInterface::loadAdditionalMouseResources(
- std::map<int32_t, SpriteIcon>* outResources,
- std::map<int32_t, PointerAnimation>* outAnimationResources,
- int32_t) {
+ std::map<PointerIconStyle, SpriteIcon>* outResources,
+ std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t) {
SpriteIcon icon;
PointerAnimation anim;
// CURSOR_TYPE_ADDITIONAL doesn't have animation resource.
int32_t cursorType = CURSOR_TYPE_ADDITIONAL;
loadPointerIconForType(&icon, cursorType);
- (*outResources)[cursorType] = icon;
+ (*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon;
// CURSOR_TYPE_ADDITIONAL_ANIM has animation resource.
cursorType = CURSOR_TYPE_ADDITIONAL_ANIM;
loadPointerIconForType(&icon, cursorType);
anim.animationFrames.push_back(icon);
anim.durationPerFrame = 10;
- (*outResources)[cursorType] = icon;
- (*outAnimationResources)[cursorType] = anim;
+ (*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon;
+ (*outAnimationResources)[static_cast<PointerIconStyle>(cursorType)] = anim;
+
+ // CURSOR_TYPE_STYLUS doesn't have animation resource.
+ cursorType = CURSOR_TYPE_STYLUS;
+ loadPointerIconForType(&icon, cursorType);
+ (*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon;
additionalMouseResourcesLoaded = true;
}
-int32_t MockPointerControllerPolicyInterface::getDefaultPointerIconId() {
- return CURSOR_TYPE_DEFAULT;
+PointerIconStyle MockPointerControllerPolicyInterface::getDefaultPointerIconId() {
+ return static_cast<PointerIconStyle>(CURSOR_TYPE_DEFAULT);
+}
+
+PointerIconStyle MockPointerControllerPolicyInterface::getDefaultStylusIconId() {
+ return static_cast<PointerIconStyle>(CURSOR_TYPE_STYLUS);
}
-int32_t MockPointerControllerPolicyInterface::getCustomPointerIconId() {
- return CURSOR_TYPE_CUSTOM;
+PointerIconStyle MockPointerControllerPolicyInterface::getCustomPointerIconId() {
+ return static_cast<PointerIconStyle>(CURSOR_TYPE_CUSTOM);
}
bool MockPointerControllerPolicyInterface::allResourcesAreLoaded() {
@@ -124,15 +136,15 @@ bool MockPointerControllerPolicyInterface::noResourcesAreLoaded() {
}
void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* icon, int32_t type) {
- icon->style = type;
+ icon->style = static_cast<PointerIconStyle>(type);
std::pair<float, float> hotSpot = getHotSpotCoordinatesForType(type);
icon->hotSpotX = hotSpot.first;
icon->hotSpotY = hotSpot.second;
}
void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId,
- float /*xPos*/,
- float /*yPos*/) {
+ const FloatPoint& /*position*/
+) {
latestPointerDisplayId = displayId;
}
@@ -205,11 +217,26 @@ TEST_F(PointerControllerTest, useDefaultCursorTypeByDefault) {
std::pair<float, float> hotspot = getHotSpotCoordinatesForType(CURSOR_TYPE_DEFAULT);
EXPECT_CALL(*mPointerSprite, setVisible(true));
EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
- EXPECT_CALL(*mPointerSprite, setIcon(
- AllOf(
- Field(&SpriteIcon::style, CURSOR_TYPE_DEFAULT),
- Field(&SpriteIcon::hotSpotX, hotspot.first),
- Field(&SpriteIcon::hotSpotY, hotspot.second))));
+ EXPECT_CALL(*mPointerSprite,
+ setIcon(AllOf(Field(&SpriteIcon::style,
+ static_cast<PointerIconStyle>(CURSOR_TYPE_DEFAULT)),
+ Field(&SpriteIcon::hotSpotX, hotspot.first),
+ Field(&SpriteIcon::hotSpotY, hotspot.second))));
+ mPointerController->reloadPointerResources();
+}
+
+TEST_F(PointerControllerTest, useStylusTypeForStylusHover) {
+ ensureDisplayViewportIsSet();
+ mPointerController->setPresentation(PointerController::Presentation::STYLUS_HOVER);
+ mPointerController->unfade(PointerController::Transition::IMMEDIATE);
+ std::pair<float, float> hotspot = getHotSpotCoordinatesForType(CURSOR_TYPE_STYLUS);
+ EXPECT_CALL(*mPointerSprite, setVisible(true));
+ EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
+ EXPECT_CALL(*mPointerSprite,
+ setIcon(AllOf(Field(&SpriteIcon::style,
+ static_cast<PointerIconStyle>(CURSOR_TYPE_STYLUS)),
+ Field(&SpriteIcon::hotSpotX, hotspot.first),
+ Field(&SpriteIcon::hotSpotY, hotspot.second))));
mPointerController->reloadPointerResources();
}
@@ -222,12 +249,11 @@ TEST_F(PointerControllerTest, updatePointerIcon) {
std::pair<float, float> hotspot = getHotSpotCoordinatesForType(type);
EXPECT_CALL(*mPointerSprite, setVisible(true));
EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
- EXPECT_CALL(*mPointerSprite, setIcon(
- AllOf(
- Field(&SpriteIcon::style, type),
- Field(&SpriteIcon::hotSpotX, hotspot.first),
- Field(&SpriteIcon::hotSpotY, hotspot.second))));
- mPointerController->updatePointerIcon(type);
+ EXPECT_CALL(*mPointerSprite,
+ setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(type)),
+ Field(&SpriteIcon::hotSpotX, hotspot.first),
+ Field(&SpriteIcon::hotSpotY, hotspot.second))));
+ mPointerController->updatePointerIcon(static_cast<PointerIconStyle>(type));
}
TEST_F(PointerControllerTest, setCustomPointerIcon) {
@@ -239,17 +265,16 @@ TEST_F(PointerControllerTest, setCustomPointerIcon) {
float hotSpotY = 20;
SpriteIcon icon;
- icon.style = style;
+ icon.style = static_cast<PointerIconStyle>(style);
icon.hotSpotX = hotSpotX;
icon.hotSpotY = hotSpotY;
EXPECT_CALL(*mPointerSprite, setVisible(true));
EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
- EXPECT_CALL(*mPointerSprite, setIcon(
- AllOf(
- Field(&SpriteIcon::style, style),
- Field(&SpriteIcon::hotSpotX, hotSpotX),
- Field(&SpriteIcon::hotSpotY, hotSpotY))));
+ EXPECT_CALL(*mPointerSprite,
+ setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(style)),
+ Field(&SpriteIcon::hotSpotX, hotSpotX),
+ Field(&SpriteIcon::hotSpotY, hotSpotY))));
mPointerController->setCustomPointerIcon(icon);
}
@@ -318,7 +343,7 @@ TEST_F(PointerControllerWindowInfoListenerTest,
localListenerCopy = registeredListener;
}
EXPECT_EQ(nullptr, registeredListener) << "WindowInfosListener was not unregistered";
- localListenerCopy->onWindowInfosChanged({}, {});
+ localListenerCopy->onWindowInfosChanged({{}, {}, 0, 0});
}
} // namespace android
diff --git a/libs/securebox/Android.bp b/libs/securebox/Android.bp
new file mode 100644
index 000000000000..a29c03cfdcca
--- /dev/null
+++ b/libs/securebox/Android.bp
@@ -0,0 +1,8 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library {
+ name: "securebox",
+ srcs: ["src/**/*.java"],
+}
diff --git a/libs/securebox/OWNERS b/libs/securebox/OWNERS
new file mode 100644
index 000000000000..e160799aa10d
--- /dev/null
+++ b/libs/securebox/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS
diff --git a/libs/securebox/src/com/android/security/SecureBox.java b/libs/securebox/src/com/android/security/SecureBox.java
new file mode 100644
index 000000000000..0ebaff4ac8e5
--- /dev/null
+++ b/libs/securebox/src/com/android/security/SecureBox.java
@@ -0,0 +1,461 @@
+/*
+ * 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.
+ */
+
+package com.android.security;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.math.BigInteger;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.EllipticCurve;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyAgreement;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Implementation of the SecureBox v2 crypto functions.
+ *
+ * <p>Securebox v2 provides a simple interface to perform encryptions by using any of the following
+ * credential types:
+ *
+ * <ul>
+ * <li>A public key owned by the recipient,
+ * <li>A secret shared between the sender and the recipient, or
+ * <li>Both a recipient's public key and a shared secret.
+ * </ul>
+ *
+ * @hide
+ */
+public class SecureBox {
+
+ private static final byte[] VERSION = new byte[] {(byte) 0x02, 0}; // LITTLE_ENDIAN_TWO_BYTES(2)
+ private static final byte[] HKDF_SALT =
+ ArrayUtils.concat("SECUREBOX".getBytes(StandardCharsets.UTF_8), VERSION);
+ private static final byte[] HKDF_INFO_WITH_PUBLIC_KEY =
+ "P256 HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] HKDF_INFO_WITHOUT_PUBLIC_KEY =
+ "SHARED HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CONSTANT_01 = {(byte) 0x01};
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+ private static final byte EC_PUBLIC_KEY_PREFIX = (byte) 0x04;
+
+ private static final String CIPHER_ALG = "AES";
+ private static final String EC_ALG = "EC";
+ private static final String EC_P256_COMMON_NAME = "secp256r1";
+ private static final String EC_P256_OPENSSL_NAME = "prime256v1";
+ private static final String ENC_ALG = "AES/GCM/NoPadding";
+ private static final String KA_ALG = "ECDH";
+ private static final String MAC_ALG = "HmacSHA256";
+
+ private static final int EC_COORDINATE_LEN_BYTES = 32;
+ private static final int EC_PUBLIC_KEY_LEN_BYTES = 2 * EC_COORDINATE_LEN_BYTES + 1;
+ private static final int GCM_NONCE_LEN_BYTES = 12;
+ private static final int GCM_KEY_LEN_BYTES = 16;
+ private static final int GCM_TAG_LEN_BYTES = 16;
+
+ private static final BigInteger BIG_INT_02 = BigInteger.valueOf(2);
+
+ private enum AesGcmOperation {
+ ENCRYPT,
+ DECRYPT
+ }
+
+ // Parameters for the NIST P-256 curve y^2 = x^3 + ax + b (mod p)
+ private static final BigInteger EC_PARAM_P =
+ new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16);
+ private static final BigInteger EC_PARAM_A = EC_PARAM_P.subtract(new BigInteger("3"));
+ private static final BigInteger EC_PARAM_B =
+ new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16);
+
+ @VisibleForTesting static final ECParameterSpec EC_PARAM_SPEC;
+
+ static {
+ EllipticCurve curveSpec =
+ new EllipticCurve(new ECFieldFp(EC_PARAM_P), EC_PARAM_A, EC_PARAM_B);
+ ECPoint generator =
+ new ECPoint(
+ new BigInteger(
+ "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296",
+ 16),
+ new BigInteger(
+ "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5",
+ 16));
+ BigInteger generatorOrder =
+ new BigInteger(
+ "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16);
+ EC_PARAM_SPEC = new ECParameterSpec(curveSpec, generator, generatorOrder, /* cofactor */ 1);
+ }
+
+ private SecureBox() {}
+
+ /**
+ * Randomly generates a public-key pair that can be used for the functions {@link #encrypt} and
+ * {@link #decrypt}.
+ *
+ * @return the randomly generated public-key pair
+ * @throws NoSuchAlgorithmException if the underlying crypto algorithm is not supported
+ * @hide
+ */
+ public static KeyPair genKeyPair() throws NoSuchAlgorithmException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_ALG);
+ try {
+ // Try using the OpenSSL provider first
+ keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME));
+ return keyPairGenerator.generateKeyPair();
+ } catch (InvalidAlgorithmParameterException ex) {
+ // Try another name for NIST P-256
+ }
+ try {
+ keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME));
+ return keyPairGenerator.generateKeyPair();
+ } catch (InvalidAlgorithmParameterException ex) {
+ throw new NoSuchAlgorithmException("Unable to find the NIST P-256 curve", ex);
+ }
+ }
+
+ /**
+ * Encrypts {@code payload} by using {@code theirPublicKey} and/or {@code sharedSecret}. At
+ * least one of {@code theirPublicKey} and {@code sharedSecret} must be non-null, and an empty
+ * {@code sharedSecret} is equivalent to null.
+ *
+ * <p>Note that {@code header} will be authenticated (but not encrypted) together with {@code
+ * payload}, and the same {@code header} has to be provided for {@link #decrypt}.
+ *
+ * @param theirPublicKey the recipient's public key, or null if the payload is to be encrypted
+ * only with the shared secret
+ * @param sharedSecret the secret shared between the sender and the recipient, or null if the
+ * payload is to be encrypted only with the recipient's public key
+ * @param header the data that will be authenticated with {@code payload} but not encrypted, or
+ * null if the data is empty
+ * @param payload the data to be encrypted, or null if the data is empty
+ * @return the encrypted payload
+ * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported
+ * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms
+ * @hide
+ */
+ public static byte[] encrypt(
+ @Nullable PublicKey theirPublicKey,
+ @Nullable byte[] sharedSecret,
+ @Nullable byte[] header,
+ @Nullable byte[] payload)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ sharedSecret = emptyByteArrayIfNull(sharedSecret);
+ if (theirPublicKey == null && sharedSecret.length == 0) {
+ throw new IllegalArgumentException("Both the public key and shared secret are empty");
+ }
+ header = emptyByteArrayIfNull(header);
+ payload = emptyByteArrayIfNull(payload);
+
+ KeyPair senderKeyPair;
+ byte[] dhSecret;
+ byte[] hkdfInfo;
+ if (theirPublicKey == null) {
+ senderKeyPair = null;
+ dhSecret = EMPTY_BYTE_ARRAY;
+ hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY;
+ } else {
+ senderKeyPair = genKeyPair();
+ dhSecret = dhComputeSecret(senderKeyPair.getPrivate(), theirPublicKey);
+ hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY;
+ }
+
+ byte[] randNonce = genRandomNonce();
+ byte[] keyingMaterial = ArrayUtils.concat(dhSecret, sharedSecret);
+ SecretKey encryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo);
+ byte[] ciphertext = aesGcmEncrypt(encryptionKey, randNonce, payload, header);
+ if (senderKeyPair == null) {
+ return ArrayUtils.concat(VERSION, randNonce, ciphertext);
+ } else {
+ return ArrayUtils.concat(
+ VERSION, encodePublicKey(senderKeyPair.getPublic()), randNonce, ciphertext);
+ }
+ }
+
+ /**
+ * Decrypts {@code encryptedPayload} by using {@code ourPrivateKey} and/or {@code sharedSecret}.
+ * At least one of {@code ourPrivateKey} and {@code sharedSecret} must be non-null, and an empty
+ * {@code sharedSecret} is equivalent to null.
+ *
+ * <p>Note that {@code header} should be the same data used for {@link #encrypt}, which is
+ * authenticated (but not encrypted) together with {@code payload}; otherwise, an {@code
+ * AEADBadTagException} will be thrown.
+ *
+ * @param ourPrivateKey the recipient's private key, or null if the payload was encrypted only
+ * with the shared secret
+ * @param sharedSecret the secret shared between the sender and the recipient, or null if the
+ * payload was encrypted only with the recipient's public key
+ * @param header the data that was authenticated with the original payload but not encrypted, or
+ * null if the data is empty
+ * @param encryptedPayload the data to be decrypted
+ * @return the original payload that was encrypted
+ * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported
+ * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms
+ * @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload}
+ * cannot be validated, or if the payload is not a valid SecureBox V2 payload.
+ * @hide
+ */
+ public static byte[] decrypt(
+ @Nullable PrivateKey ourPrivateKey,
+ @Nullable byte[] sharedSecret,
+ @Nullable byte[] header,
+ byte[] encryptedPayload)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ sharedSecret = emptyByteArrayIfNull(sharedSecret);
+ if (ourPrivateKey == null && sharedSecret.length == 0) {
+ throw new IllegalArgumentException("Both the private key and shared secret are empty");
+ }
+ header = emptyByteArrayIfNull(header);
+ if (encryptedPayload == null) {
+ throw new NullPointerException("Encrypted payload must not be null.");
+ }
+
+ ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload);
+ byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length);
+ if (!Arrays.equals(version, VERSION)) {
+ throw new AEADBadTagException("The payload was not encrypted by SecureBox v2");
+ }
+
+ byte[] senderPublicKeyBytes;
+ byte[] dhSecret;
+ byte[] hkdfInfo;
+ if (ourPrivateKey == null) {
+ dhSecret = EMPTY_BYTE_ARRAY;
+ hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY;
+ } else {
+ senderPublicKeyBytes = readEncryptedPayload(ciphertextBuffer, EC_PUBLIC_KEY_LEN_BYTES);
+ dhSecret = dhComputeSecret(ourPrivateKey, decodePublicKey(senderPublicKeyBytes));
+ hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY;
+ }
+
+ byte[] randNonce = readEncryptedPayload(ciphertextBuffer, GCM_NONCE_LEN_BYTES);
+ byte[] ciphertext = readEncryptedPayload(ciphertextBuffer, ciphertextBuffer.remaining());
+ byte[] keyingMaterial = ArrayUtils.concat(dhSecret, sharedSecret);
+ SecretKey decryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo);
+ return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header);
+ }
+
+ private static byte[] readEncryptedPayload(ByteBuffer buffer, int length)
+ throws AEADBadTagException {
+ byte[] output = new byte[length];
+ try {
+ buffer.get(output);
+ } catch (BufferUnderflowException ex) {
+ throw new AEADBadTagException("The encrypted payload is too short");
+ }
+ return output;
+ }
+
+ private static byte[] dhComputeSecret(PrivateKey ourPrivateKey, PublicKey theirPublicKey)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ KeyAgreement agreement = KeyAgreement.getInstance(KA_ALG);
+ try {
+ agreement.init(ourPrivateKey);
+ } catch (RuntimeException ex) {
+ // Rethrow the RuntimeException as InvalidKeyException
+ throw new InvalidKeyException(ex);
+ }
+ agreement.doPhase(theirPublicKey, /*lastPhase=*/ true);
+ return agreement.generateSecret();
+ }
+
+ /** Derives a 128-bit AES key. */
+ private static SecretKey hkdfDeriveKey(byte[] secret, byte[] salt, byte[] info)
+ throws NoSuchAlgorithmException {
+ Mac mac = Mac.getInstance(MAC_ALG);
+ try {
+ mac.init(new SecretKeySpec(salt, MAC_ALG));
+ } catch (InvalidKeyException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ byte[] pseudorandomKey = mac.doFinal(secret);
+
+ try {
+ mac.init(new SecretKeySpec(pseudorandomKey, MAC_ALG));
+ } catch (InvalidKeyException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ mac.update(info);
+ // Hashing just one block will yield 256 bits, which is enough to construct the AES key
+ byte[] hkdfOutput = mac.doFinal(CONSTANT_01);
+
+ return new SecretKeySpec(Arrays.copyOf(hkdfOutput, GCM_KEY_LEN_BYTES), CIPHER_ALG);
+ }
+
+ private static byte[] aesGcmEncrypt(SecretKey key, byte[] nonce, byte[] plaintext, byte[] aad)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ try {
+ return aesGcmInternal(AesGcmOperation.ENCRYPT, key, nonce, plaintext, aad);
+ } catch (AEADBadTagException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private static byte[] aesGcmDecrypt(SecretKey key, byte[] nonce, byte[] ciphertext, byte[] aad)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return aesGcmInternal(AesGcmOperation.DECRYPT, key, nonce, ciphertext, aad);
+ }
+
+ private static byte[] aesGcmInternal(
+ AesGcmOperation operation, SecretKey key, byte[] nonce, byte[] text, byte[] aad)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(ENC_ALG);
+ } catch (NoSuchPaddingException ex) {
+ // This should never happen because AES-GCM doesn't use padding
+ throw new RuntimeException(ex);
+ }
+ GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BYTES * 8, nonce);
+ try {
+ if (operation == AesGcmOperation.DECRYPT) {
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ } else {
+ cipher.init(Cipher.ENCRYPT_MODE, key, spec);
+ }
+ } catch (InvalidAlgorithmParameterException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ try {
+ cipher.updateAAD(aad);
+ return cipher.doFinal(text);
+ } catch (AEADBadTagException ex) {
+ // Catch and rethrow AEADBadTagException first because it's a subclass of
+ // BadPaddingException
+ throw ex;
+ } catch (IllegalBlockSizeException | BadPaddingException ex) {
+ // This should never happen because AES-GCM can handle inputs of any length without
+ // padding
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Encodes public key in format expected by the secure hardware module. This is used as part
+ * of the vault params.
+ *
+ * @param publicKey The public key.
+ * @return The key packed into a 65-byte array.
+ */
+ public static byte[] encodePublicKey(PublicKey publicKey) {
+ ECPoint point = ((ECPublicKey) publicKey).getW();
+ byte[] x = point.getAffineX().toByteArray();
+ byte[] y = point.getAffineY().toByteArray();
+
+ byte[] output = new byte[EC_PUBLIC_KEY_LEN_BYTES];
+ // The order of arraycopy() is important, because the coordinates may have a one-byte
+ // leading 0 for the sign bit of two's complement form
+ System.arraycopy(y, 0, output, EC_PUBLIC_KEY_LEN_BYTES - y.length, y.length);
+ System.arraycopy(x, 0, output, 1 + EC_COORDINATE_LEN_BYTES - x.length, x.length);
+ output[0] = EC_PUBLIC_KEY_PREFIX;
+ return output;
+ }
+
+ /**
+ * Decodes byte[] encoded public key.
+ *
+ * @param keyBytes encoded public key
+ * @return the public key
+ */
+ public static PublicKey decodePublicKey(byte[] keyBytes)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ BigInteger x =
+ new BigInteger(
+ /*signum=*/ 1,
+ Arrays.copyOfRange(keyBytes, 1, 1 + EC_COORDINATE_LEN_BYTES));
+ BigInteger y =
+ new BigInteger(
+ /*signum=*/ 1,
+ Arrays.copyOfRange(
+ keyBytes, 1 + EC_COORDINATE_LEN_BYTES, EC_PUBLIC_KEY_LEN_BYTES));
+
+ // Checks if the point is indeed on the P-256 curve for security considerations
+ validateEcPoint(x, y);
+
+ KeyFactory keyFactory = KeyFactory.getInstance(EC_ALG);
+ try {
+ return keyFactory.generatePublic(new ECPublicKeySpec(new ECPoint(x, y), EC_PARAM_SPEC));
+ } catch (InvalidKeySpecException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private static void validateEcPoint(BigInteger x, BigInteger y) throws InvalidKeyException {
+ if (x.compareTo(EC_PARAM_P) >= 0
+ || y.compareTo(EC_PARAM_P) >= 0
+ || x.signum() == -1
+ || y.signum() == -1) {
+ throw new InvalidKeyException("Point lies outside of the expected curve");
+ }
+
+ // Points on the curve satisfy y^2 = x^3 + ax + b (mod p)
+ BigInteger lhs = y.modPow(BIG_INT_02, EC_PARAM_P);
+ BigInteger rhs =
+ x.modPow(BIG_INT_02, EC_PARAM_P) // x^2
+ .add(EC_PARAM_A) // x^2 + a
+ .mod(EC_PARAM_P) // This will speed up the next multiplication
+ .multiply(x) // (x^2 + a) * x = x^3 + ax
+ .add(EC_PARAM_B) // x^3 + ax + b
+ .mod(EC_PARAM_P);
+ if (!lhs.equals(rhs)) {
+ throw new InvalidKeyException("Point lies outside of the expected curve");
+ }
+ }
+
+ private static byte[] genRandomNonce() throws NoSuchAlgorithmException {
+ byte[] nonce = new byte[GCM_NONCE_LEN_BYTES];
+ new SecureRandom().nextBytes(nonce);
+ return nonce;
+ }
+
+ private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) {
+ return input == null ? EMPTY_BYTE_ARRAY : input;
+ }
+}
diff --git a/libs/securebox/tests/Android.bp b/libs/securebox/tests/Android.bp
new file mode 100644
index 000000000000..7df546ae0ff6
--- /dev/null
+++ b/libs/securebox/tests/Android.bp
@@ -0,0 +1,46 @@
+// 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.
+
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "SecureBoxTests",
+ srcs: [
+ "**/*.java",
+ ],
+ static_libs: [
+ "securebox",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "frameworks-base-testutils",
+ "junit",
+ "mockito-target-extended-minus-junit4",
+ "platform-test-annotations",
+ "testables",
+ "testng",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.mock",
+ "android.test.base",
+ "android.test.runner",
+ ],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+}
diff --git a/libs/securebox/tests/AndroidManifest.xml b/libs/securebox/tests/AndroidManifest.xml
new file mode 100644
index 000000000000..3dc956394362
--- /dev/null
+++ b/libs/securebox/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.security.tests">
+
+ <application android:debuggable="true" android:largeHeap="true">
+ <uses-library android:name="android.test.mock" />
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Tests for SecureBox"
+ android:targetPackage="com.android.security.tests">
+ </instrumentation>
+
+</manifest>
diff --git a/libs/securebox/tests/AndroidTest.xml b/libs/securebox/tests/AndroidTest.xml
new file mode 100644
index 000000000000..54abd13515b4
--- /dev/null
+++ b/libs/securebox/tests/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+<configuration description="Runs Tests for SecureBox">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="SecureBoxTests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="framework-base-presubmit" />
+ <option name="test-tag" value="SecureBoxTests" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.security.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/libs/securebox/tests/src/com/android/security/SecureBoxTest.java b/libs/securebox/tests/src/com/android/security/SecureBoxTest.java
new file mode 100644
index 000000000000..b6e2365038dc
--- /dev/null
+++ b/libs/securebox/tests/src/com/android/security/SecureBoxTest.java
@@ -0,0 +1,371 @@
+/*
+ * 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.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.ECPrivateKeySpec;
+
+import javax.crypto.AEADBadTagException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SecureBoxTest {
+
+ private static final int EC_PUBLIC_KEY_LEN_BYTES = 65;
+ private static final int NUM_TEST_ITERATIONS = 100;
+ private static final int VERSION_LEN_BYTES = 2;
+
+ // The following fixtures were produced by the C implementation of SecureBox v2. We use these to
+ // cross-verify the two implementations.
+ private static final byte[] VAULT_PARAMS =
+ new byte[] {
+ (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18, (byte) 0x98,
+ (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4, (byte) 0x94, (byte) 0xfe,
+ (byte) 0x86, (byte) 0xda, (byte) 0x1c, (byte) 0x07, (byte) 0x8d, (byte) 0x01,
+ (byte) 0xb4, (byte) 0x3a, (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61,
+ (byte) 0xd0, (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10,
+ (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, (byte) 0x3f,
+ (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79, (byte) 0x20, (byte) 0x1d,
+ (byte) 0x91, (byte) 0x55, (byte) 0xb0, (byte) 0xe5, (byte) 0xbd, (byte) 0x7a,
+ (byte) 0x8b, (byte) 0x32, (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2,
+ (byte) 0xfc, (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21,
+ (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa, (byte) 0x31,
+ (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x78, (byte) 0x56, (byte) 0x34, (byte) 0x12, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0a, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00
+ };
+ private static final byte[] VAULT_CHALLENGE = getBytes("Not a real vault challenge");
+ private static final byte[] THM_KF_HASH = getBytes("12345678901234567890123456789012");
+ private static final byte[] ENCRYPTED_RECOVERY_KEY =
+ new byte[] {
+ (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0xe3, (byte) 0xa8, (byte) 0xd0,
+ (byte) 0x32, (byte) 0x3c, (byte) 0xc7, (byte) 0xe5, (byte) 0xe8, (byte) 0xc1,
+ (byte) 0x73, (byte) 0x4c, (byte) 0x75, (byte) 0x20, (byte) 0x2e, (byte) 0xb7,
+ (byte) 0xba, (byte) 0xef, (byte) 0x3e, (byte) 0x3e, (byte) 0xa6, (byte) 0x93,
+ (byte) 0xe9, (byte) 0xde, (byte) 0xa7, (byte) 0x00, (byte) 0x09, (byte) 0xba,
+ (byte) 0xa8, (byte) 0x9c, (byte) 0xac, (byte) 0x72, (byte) 0xff, (byte) 0xf6,
+ (byte) 0x84, (byte) 0x16, (byte) 0xb0, (byte) 0xff, (byte) 0x47, (byte) 0x98,
+ (byte) 0x53, (byte) 0xc4, (byte) 0xa3, (byte) 0x4a, (byte) 0x54, (byte) 0x21,
+ (byte) 0x8e, (byte) 0x00, (byte) 0x4b, (byte) 0xfa, (byte) 0xce, (byte) 0xe3,
+ (byte) 0x79, (byte) 0x8e, (byte) 0x20, (byte) 0x7c, (byte) 0x9b, (byte) 0xc4,
+ (byte) 0x7c, (byte) 0xd5, (byte) 0x33, (byte) 0x70, (byte) 0x96, (byte) 0xdc,
+ (byte) 0xa0, (byte) 0x1f, (byte) 0x6e, (byte) 0xbb, (byte) 0x5d, (byte) 0x0c,
+ (byte) 0x64, (byte) 0x5f, (byte) 0xed, (byte) 0xbf, (byte) 0x79, (byte) 0x8a,
+ (byte) 0x0e, (byte) 0xd6, (byte) 0x4b, (byte) 0x93, (byte) 0xc9, (byte) 0xcd,
+ (byte) 0x25, (byte) 0x06, (byte) 0x73, (byte) 0x5e, (byte) 0xdb, (byte) 0xac,
+ (byte) 0xa8, (byte) 0xeb, (byte) 0x6e, (byte) 0x26, (byte) 0x77, (byte) 0x56,
+ (byte) 0xd1, (byte) 0x23, (byte) 0x48, (byte) 0xb6, (byte) 0x6a, (byte) 0x15,
+ (byte) 0xd4, (byte) 0x3e, (byte) 0x38, (byte) 0x7d, (byte) 0x6f, (byte) 0x6f,
+ (byte) 0x7c, (byte) 0x0b, (byte) 0x93, (byte) 0x4e, (byte) 0xb3, (byte) 0x21,
+ (byte) 0x44, (byte) 0x86, (byte) 0xf3, (byte) 0x2e
+ };
+ private static final byte[] KEY_CLAIMANT = getBytes("asdfasdfasdfasdf");
+ private static final byte[] RECOVERY_CLAIM =
+ new byte[] {
+ (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0x16, (byte) 0x75, (byte) 0x5b,
+ (byte) 0xa2, (byte) 0xdc, (byte) 0x2b, (byte) 0x58, (byte) 0xb9, (byte) 0x66,
+ (byte) 0xcb, (byte) 0x6f, (byte) 0xb1, (byte) 0xc1, (byte) 0xb0, (byte) 0x1d,
+ (byte) 0x82, (byte) 0x29, (byte) 0x97, (byte) 0xec, (byte) 0x65, (byte) 0x5e,
+ (byte) 0xef, (byte) 0x14, (byte) 0xc7, (byte) 0xf0, (byte) 0xf1, (byte) 0x83,
+ (byte) 0x15, (byte) 0x0b, (byte) 0xcb, (byte) 0x33, (byte) 0x2d, (byte) 0x05,
+ (byte) 0x20, (byte) 0xdc, (byte) 0xc7, (byte) 0x0d, (byte) 0xc8, (byte) 0xc0,
+ (byte) 0xc9, (byte) 0xa8, (byte) 0x67, (byte) 0xc8, (byte) 0x16, (byte) 0xfe,
+ (byte) 0xfb, (byte) 0xb0, (byte) 0x28, (byte) 0x8e, (byte) 0x4f, (byte) 0xd5,
+ (byte) 0x31, (byte) 0xa7, (byte) 0x94, (byte) 0x33, (byte) 0x23, (byte) 0x15,
+ (byte) 0x04, (byte) 0xbf, (byte) 0x13, (byte) 0x6a, (byte) 0x28, (byte) 0x8f,
+ (byte) 0xa6, (byte) 0xfc, (byte) 0x01, (byte) 0xd5, (byte) 0x69, (byte) 0x3d,
+ (byte) 0x96, (byte) 0x0c, (byte) 0x37, (byte) 0xb4, (byte) 0x1e, (byte) 0x13,
+ (byte) 0x40, (byte) 0xcc, (byte) 0x44, (byte) 0x19, (byte) 0xf2, (byte) 0xdb,
+ (byte) 0x49, (byte) 0x80, (byte) 0x9f, (byte) 0xef, (byte) 0xee, (byte) 0x41,
+ (byte) 0xe6, (byte) 0x3f, (byte) 0xa8, (byte) 0xea, (byte) 0x89, (byte) 0xfe,
+ (byte) 0x56, (byte) 0x20, (byte) 0xba, (byte) 0x90, (byte) 0x9a, (byte) 0xba,
+ (byte) 0x0e, (byte) 0x30, (byte) 0xa7, (byte) 0x2b, (byte) 0x0a, (byte) 0x12,
+ (byte) 0x0b, (byte) 0x03, (byte) 0xd1, (byte) 0x0c, (byte) 0x8e, (byte) 0x82,
+ (byte) 0x03, (byte) 0xa1, (byte) 0x7f, (byte) 0xc8, (byte) 0xd0, (byte) 0xa9,
+ (byte) 0x86, (byte) 0x55, (byte) 0x63, (byte) 0xdc, (byte) 0x70, (byte) 0x34,
+ (byte) 0x21, (byte) 0x2a, (byte) 0x41, (byte) 0x3f, (byte) 0xbb, (byte) 0x82,
+ (byte) 0x82, (byte) 0xf9, (byte) 0x2b, (byte) 0xd2, (byte) 0x33, (byte) 0x03,
+ (byte) 0x50, (byte) 0xd2, (byte) 0x27, (byte) 0xeb, (byte) 0x1a
+ };
+
+ private static final byte[] TEST_SHARED_SECRET = getBytes("TEST_SHARED_SECRET");
+ private static final byte[] TEST_HEADER = getBytes("TEST_HEADER");
+ private static final byte[] TEST_PAYLOAD = getBytes("TEST_PAYLOAD");
+
+ private static final PublicKey THM_PUBLIC_KEY;
+ private static final PrivateKey THM_PRIVATE_KEY;
+
+ static {
+ try {
+ THM_PUBLIC_KEY =
+ SecureBox.decodePublicKey(
+ new byte[] {
+ (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18,
+ (byte) 0x98, (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4,
+ (byte) 0x94, (byte) 0xfe, (byte) 0x86, (byte) 0xda, (byte) 0x1c,
+ (byte) 0x07, (byte) 0x8d, (byte) 0x01, (byte) 0xb4, (byte) 0x3a,
+ (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61, (byte) 0xd0,
+ (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10,
+ (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0,
+ (byte) 0x3f, (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79,
+ (byte) 0x20, (byte) 0x1d, (byte) 0x91, (byte) 0x55, (byte) 0xb0,
+ (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, (byte) 0x8b, (byte) 0x32,
+ (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2, (byte) 0xfc,
+ (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21,
+ (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa
+ });
+ THM_PRIVATE_KEY =
+ decodePrivateKey(
+ new byte[] {
+ (byte) 0x70, (byte) 0x01, (byte) 0xc7, (byte) 0x87, (byte) 0x32,
+ (byte) 0x2f, (byte) 0x1c, (byte) 0x9a, (byte) 0x6e, (byte) 0xb1,
+ (byte) 0x91, (byte) 0xca, (byte) 0x4e, (byte) 0xb5, (byte) 0x44,
+ (byte) 0xba, (byte) 0xc8, (byte) 0x68, (byte) 0xc6, (byte) 0x0a,
+ (byte) 0x76, (byte) 0xcb, (byte) 0xd3, (byte) 0x63, (byte) 0x67,
+ (byte) 0x7c, (byte) 0xb0, (byte) 0x11, (byte) 0x82, (byte) 0x65,
+ (byte) 0x77, (byte) 0x01
+ });
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Test
+ public void genKeyPair_alwaysReturnsANewKeyPair() throws Exception {
+ KeyPair keyPair1 = SecureBox.genKeyPair();
+ KeyPair keyPair2 = SecureBox.genKeyPair();
+ assertThat(keyPair1).isNotEqualTo(keyPair2);
+ }
+
+ @Test
+ public void decryptRecoveryClaim() throws Exception {
+ byte[] claimContent =
+ SecureBox.decrypt(
+ THM_PRIVATE_KEY,
+ /*sharedSecret=*/ null,
+ ArrayUtils.concat(getBytes("V1 KF_claim"), VAULT_PARAMS, VAULT_CHALLENGE),
+ RECOVERY_CLAIM);
+ assertThat(claimContent).isEqualTo(ArrayUtils.concat(THM_KF_HASH, KEY_CLAIMANT));
+ }
+
+ @Test
+ public void decryptRecoveryKey_doesNotThrowForValidAuthenticationTag() throws Exception {
+ SecureBox.decrypt(
+ THM_PRIVATE_KEY,
+ THM_KF_HASH,
+ ArrayUtils.concat(getBytes("V1 THM_encrypted_recovery_key"), VAULT_PARAMS),
+ ENCRYPTED_RECOVERY_KEY);
+ }
+
+ @Test
+ public void encryptThenDecrypt() throws Exception {
+ byte[] state = TEST_PAYLOAD;
+ // Iterate multiple times to amplify any errors
+ for (int i = 0; i < NUM_TEST_ITERATIONS; i++) {
+ state = SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, state);
+ }
+ for (int i = 0; i < NUM_TEST_ITERATIONS; i++) {
+ state = SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, state);
+ }
+ assertThat(state).isEqualTo(TEST_PAYLOAD);
+ }
+
+ @Test
+ public void encryptThenDecrypt_nullPublicPrivateKeys() throws Exception {
+ byte[] encrypted =
+ SecureBox.encrypt(
+ /*theirPublicKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD);
+ byte[] decrypted =
+ SecureBox.decrypt(
+ /*ourPrivateKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, encrypted);
+ assertThat(decrypted).isEqualTo(TEST_PAYLOAD);
+ }
+
+ @Test
+ public void encryptThenDecrypt_nullSharedSecret() throws Exception {
+ byte[] encrypted =
+ SecureBox.encrypt(
+ THM_PUBLIC_KEY, /*sharedSecret=*/ null, TEST_HEADER, TEST_PAYLOAD);
+ byte[] decrypted =
+ SecureBox.decrypt(THM_PRIVATE_KEY, /*sharedSecret=*/ null, TEST_HEADER, encrypted);
+ assertThat(decrypted).isEqualTo(TEST_PAYLOAD);
+ }
+
+ @Test
+ public void encryptThenDecrypt_nullHeader() throws Exception {
+ byte[] encrypted =
+ SecureBox.encrypt(
+ THM_PUBLIC_KEY, TEST_SHARED_SECRET, /*header=*/ null, TEST_PAYLOAD);
+ byte[] decrypted =
+ SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, /*header=*/ null, encrypted);
+ assertThat(decrypted).isEqualTo(TEST_PAYLOAD);
+ }
+
+ @Test
+ public void encryptThenDecrypt_nullPayload() throws Exception {
+ byte[] encrypted =
+ SecureBox.encrypt(
+ THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, /*payload=*/ null);
+ byte[] decrypted =
+ SecureBox.decrypt(
+ THM_PRIVATE_KEY,
+ TEST_SHARED_SECRET,
+ TEST_HEADER,
+ /*encryptedPayload=*/ encrypted);
+ assertThat(decrypted.length).isEqualTo(0);
+ }
+
+ @Test
+ public void encrypt_nullPublicKeyAndSharedSecret() throws Exception {
+ IllegalArgumentException expected =
+ expectThrows(
+ IllegalArgumentException.class,
+ () ->
+ SecureBox.encrypt(
+ /*theirPublicKey=*/ null,
+ /*sharedSecret=*/ null,
+ TEST_HEADER,
+ TEST_PAYLOAD));
+ assertThat(expected.getMessage()).contains("public key and shared secret");
+ }
+
+ @Test
+ public void decrypt_nullPrivateKeyAndSharedSecret() throws Exception {
+ IllegalArgumentException expected =
+ expectThrows(
+ IllegalArgumentException.class,
+ () ->
+ SecureBox.decrypt(
+ /*ourPrivateKey=*/ null,
+ /*sharedSecret=*/ null,
+ TEST_HEADER,
+ TEST_PAYLOAD));
+ assertThat(expected.getMessage()).contains("private key and shared secret");
+ }
+
+ @Test
+ public void decrypt_nullEncryptedPayload() throws Exception {
+ NullPointerException expected =
+ expectThrows(
+ NullPointerException.class,
+ () ->
+ SecureBox.decrypt(
+ THM_PRIVATE_KEY,
+ TEST_SHARED_SECRET,
+ TEST_HEADER,
+ /*encryptedPayload=*/ null));
+ assertThat(expected.getMessage()).contains("payload");
+ }
+
+ @Test
+ public void decrypt_badAuthenticationTag() throws Exception {
+ byte[] encrypted =
+ SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD);
+ encrypted[encrypted.length - 1] ^= (byte) 1;
+
+ assertThrows(
+ AEADBadTagException.class,
+ () ->
+ SecureBox.decrypt(
+ THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, encrypted));
+ }
+
+ @Test
+ public void encrypt_invalidPublicKey() throws Exception {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ PublicKey publicKey = keyGen.genKeyPair().getPublic();
+
+ assertThrows(
+ InvalidKeyException.class,
+ () -> SecureBox.encrypt(publicKey, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD));
+ }
+
+ @Test
+ public void decrypt_invalidPrivateKey() throws Exception {
+ byte[] encrypted =
+ SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD);
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ PrivateKey privateKey = keyGen.genKeyPair().getPrivate();
+
+ assertThrows(
+ InvalidKeyException.class,
+ () -> SecureBox.decrypt(privateKey, TEST_SHARED_SECRET, TEST_HEADER, encrypted));
+ }
+
+ @Test
+ public void decrypt_publicKeyOutsideCurve() throws Exception {
+ byte[] encrypted =
+ SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD);
+ // Flip the least significant bit of the encoded public key
+ encrypted[VERSION_LEN_BYTES + EC_PUBLIC_KEY_LEN_BYTES - 1] ^= (byte) 1;
+
+ InvalidKeyException expected =
+ expectThrows(
+ InvalidKeyException.class,
+ () ->
+ SecureBox.decrypt(
+ THM_PRIVATE_KEY,
+ TEST_SHARED_SECRET,
+ TEST_HEADER,
+ encrypted));
+ assertThat(expected.getMessage()).contains("expected curve");
+ }
+
+ @Test
+ public void encodeThenDecodePublicKey() throws Exception {
+ for (int i = 0; i < NUM_TEST_ITERATIONS; i++) {
+ PublicKey originalKey = SecureBox.genKeyPair().getPublic();
+ byte[] encodedKey = SecureBox.encodePublicKey(originalKey);
+ PublicKey decodedKey = SecureBox.decodePublicKey(encodedKey);
+ assertThat(originalKey).isEqualTo(decodedKey);
+ }
+ }
+
+ private static byte[] getBytes(String str) {
+ return str.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static PrivateKey decodePrivateKey(byte[] keyBytes) throws Exception {
+ assertThat(keyBytes.length).isEqualTo(32);
+ BigInteger priv = new BigInteger(/*signum=*/ 1, keyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance("EC");
+ return keyFactory.generatePrivate(new ECPrivateKeySpec(priv, SecureBox.EC_PARAM_SPEC));
+ }
+}
diff --git a/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java b/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java
index 18cfce528205..c019a8ce0b44 100644
--- a/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java
+++ b/libs/usb/tests/AccessoryChat/src/com/android/accessorychat/AccessoryChat.java
@@ -83,7 +83,9 @@ public class AccessoryChat extends Activity implements Runnable, TextView.OnEdit
super.onCreate(savedInstanceState);
mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
- mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE_UNAUDITED);
+ mPermissionIntent = PendingIntent.getBroadcast(this, 0,
+ new Intent(ACTION_USB_PERMISSION).setPackage(this.getPackageName()),
+ PendingIntent.FLAG_MUTABLE);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);