From 76aa855f1e839350f628e6eae929e525191120bb Mon Sep 17 00:00:00 2001 From: MrSluffy Date: Sat, 2 Nov 2024 10:45:09 +0800 Subject: [PATCH] fixes more error --- .github/workflows/ci.yml | 2 +- build.gradle | 21 +- flags/build.gradle | 1 + .../android/systemui/CustomFeatureFlags.java | 1102 +++++ .../systemui/FakeFeatureFlagsImpl.java | 49 + .../com/android/systemui/FeatureFlags.java | 308 ++ .../android/systemui/FeatureFlagsImpl.java | 3157 ++++++++++++++ flags/src/com/android/systemui/Flags.java | 914 ++++ .../systemui/shared/CustomFeatureFlags.java | 94 + .../systemui/shared/FakeFeatureFlagsImpl.java | 49 + .../android/systemui/shared/FeatureFlags.java | 24 + .../systemui/shared/FeatureFlagsImpl.java | 194 + .../com/android/systemui/shared/Flags.java | 50 + .../allapps/views/SearchResultIcon.kt | 5 +- .../nexuslauncher/OverlayCallbackImpl.kt | 54 +- platform_frameworks_libs_systemui | 2 +- settings.gradle | 8 +- systemUIAnim/Android.bp | 73 + systemUIAnim/AndroidManifest.xml | 22 + systemUIAnim/build.gradle | 31 + systemUIAnim/res/anim/launch_dialog_enter.xml | 23 + systemUIAnim/res/anim/launch_dialog_exit.xml | 22 + systemUIAnim/res/values/ids.xml | 29 + systemUIAnim/res/values/styles.xml | 24 + .../animation/ActivityTransitionAnimator.kt | 1370 ++++++ .../animation/AnimationFeatureFlags.kt | 6 + .../DelegateTransitionAnimatorController.kt | 26 + .../animation/DialogTransitionAnimator.kt | 1100 +++++ .../android/systemui/animation/Expandable.kt | 102 + .../systemui/animation/FontInterpolator.kt | 238 ++ .../systemui/animation/FontVariationUtils.kt | 59 + ...GhostedViewTransitionAnimatorController.kt | 532 +++ .../systemui/animation/LaunchableView.kt | 96 + .../animation/RemoteAnimationDelegate.kt | 30 + .../RemoteAnimationRunnerCompat.java | 239 ++ .../RemoteAnimationTargetCompat.java | 86 + .../systemui/animation/ShadeInterpolation.kt | 39 + .../systemui/animation/TextAnimator.kt | 409 ++ .../systemui/animation/TextInterpolator.kt | 531 +++ .../systemui/animation/TransitionAnimator.kt | 576 +++ .../ViewDialogTransitionAnimatorController.kt | 128 + .../animation/ViewHierarchyAnimator.kt | 1160 +++++ .../systemui/animation/ViewRootSync.kt | 43 + .../animation/back/BackAnimationSpec.kt | 74 + .../back/BackAnimationSpecForSysUi.kt | 83 + .../animation/back/BackTransformation.kt | 63 + .../back/BottomsheetBackAnimationSpec.kt | 42 + .../back/OnBackAnimationCallbackExtension.kt | 103 + .../animation/view/LaunchableFrameLayout.kt | 55 + .../animation/view/LaunchableImageView.kt | 56 + .../animation/view/LaunchableLinearLayout.kt | 55 + .../animation/view/LaunchableTextView.kt | 49 + .../surfaceeffects/PaintDrawCallback.kt | 53 + .../RenderEffectDrawCallback.kt | 39 + .../glowboxeffect/GlowBoxConfig.kt | 46 + .../glowboxeffect/GlowBoxEffect.kt | 185 + .../glowboxeffect/GlowBoxShader.kt | 58 + .../loadingeffect/LoadingEffect.kt | 366 ++ .../loadingeffect/LoadingEffectView.kt | 51 + .../ripple/MultiRippleController.kt | 49 + .../surfaceeffects/ripple/MultiRippleView.kt | 61 + .../surfaceeffects/ripple/RippleAnimation.kt | 93 + .../ripple/RippleAnimationConfig.kt | 29 + .../surfaceeffects/ripple/RippleShader.kt | 460 ++ .../surfaceeffects/ripple/RippleView.kt | 252 ++ .../shaders/SolidColorShader.kt | 36 + .../surfaceeffects/shaders/SparkleShader.kt | 115 + .../shaderutil/SdfShaderLibrary.kt | 128 + .../shaderutil/ShaderUtilLibrary.kt | 201 + .../TurbulenceNoiseAnimationConfig.kt | 89 + .../TurbulenceNoiseController.kt | 115 + .../turbulencenoise/TurbulenceNoiseShader.kt | 294 ++ .../turbulencenoise/TurbulenceNoiseView.kt | 236 + .../surfaceeffects/utils/MathUtils.kt | 24 + .../systemui/util/AnimatorExtensions.kt | 80 + .../src/com/android/systemui/util/Dialog.kt | 162 + .../com/android/systemui/util/Dimension.kt | 32 + systemUICommon/.gitignore | 9 + systemUICommon/Android.bp | 41 + systemUICommon/AndroidManifest.xml | 21 + systemUICommon/OWNERS | 2 + systemUICommon/README.md | 5 + systemUICommon/build.gradle | 17 + .../systemui/common/buffer/RingBuffer.kt | 118 + systemUILog/.gitignore | 9 + systemUILog/Android.bp | 38 + systemUILog/AndroidManifest.xml | 22 + systemUILog/build.gradle | 27 + .../systemui/log/ConstantStringsLogger.kt | 33 + .../systemui/log/ConstantStringsLoggerImpl.kt | 30 + .../src/com/android/systemui/log/LogBuffer.kt | 267 ++ .../android/systemui/log/LogMessageImpl.kt | 96 + .../android/systemui/log/LogcatEchoTracker.kt | 28 + .../com/android/systemui/log/core/LogLevel.kt | 29 + .../android/systemui/log/core/LogMessage.kt | 101 + .../log/core/LogcatOnlyMessageBuffer.kt | 76 + .../com/android/systemui/log/core/Logger.kt | 224 + .../systemui/log/core/MessageBuffer.kt | 42 + systemUIPlugin/Android.bp | 62 + systemUIPlugin/AndroidManifest.xml | 21 + systemUIPlugin/ExamplePlugin/Android.bp | 26 + .../ExamplePlugin/AndroidManifest.xml | 41 + .../res/layout/colored_overlay.xml | 22 + .../res/layout/plugin_settings.xml | 24 + .../ExamplePlugin/res/values/strings.xml | 24 + .../plugin/testoverlayplugin/CustomView.java | 46 + .../testoverlayplugin/PluginSettings.java | 32 + .../SampleOverlayPlugin.java | 104 + .../plugins/BcSmartspaceConfigPlugin.kt | 24 + .../plugins/BcSmartspaceDataPlugin.java | 246 ++ systemUIPlugin/build.gradle | 28 + .../systemui/plugins/ActivityStarter.java | 185 + .../systemui/plugins/DarkIconDispatcher.java | 204 + .../systemui/plugins/DozeServicePlugin.java | 21 + .../systemui/plugins/FalsingManager.java | 176 + .../systemui/plugins/FalsingPlugin.java | 52 + .../systemui/plugins/FragmentBase.java | 33 + .../systemui/plugins/GlobalActions.java | 45 + .../plugins/GlobalActionsPanelPlugin.java | 115 + .../plugins/IntentButtonProvider.java | 45 + .../plugins/NavigationEdgeBackPlugin.java | 72 + .../NotificationListenerController.java | 83 + .../NotificationPersonExtractorPlugin.java | 80 + .../systemui/plugins/OverlayPlugin.java | 56 + .../systemui/plugins/PluginDependency.java | 31 + .../android/systemui/plugins/PluginUtils.java | 26 + .../systemui/plugins/SensorManagerPlugin.java | 119 + .../android/systemui/plugins/ToastPlugin.java | 112 + .../systemui/plugins/ViewProvider.java | 21 + .../systemui/plugins/VolumeDialog.java | 41 + .../plugins/VolumeDialogController.java | 236 + .../systemui/plugins/clocks/AlarmData.kt | 6 + .../plugins/clocks/ClockProviderPlugin.kt | 122 + .../systemui/plugins/clocks/WeatherData.kt | 123 + .../systemui/plugins/clocks/ZenData.kt | 22 + .../plugins/log/TableLogBufferBase.kt | 58 + .../com/android/systemui/plugins/qs/QS.java | 183 + .../plugins/qs/QSContainerController.kt | 11 + .../systemui/plugins/qs/QSFactory.java | 34 + .../systemui/plugins/qs/QSIconView.java | 35 + .../android/systemui/plugins/qs/QSTile.java | 314 ++ .../systemui/plugins/qs/QSTileView.java | 83 + .../plugins/statusbar/DozeParameters.java | 32 + .../statusbar/NotificationMenuRowPlugin.java | 255 ++ .../NotificationSwipeActionHelper.java | 70 + .../statusbar/StatusBarStateController.java | 135 + systemUIPlugin/update_plugin_lib.sh | 15 + systemUIPluginCore/Android.bp | 35 + systemUIPluginCore/AndroidManifest.xml | 24 + systemUIPluginCore/build.gradle | 21 + systemUIPluginCore/proguard.flags | 11 + .../com/android/systemui/plugins/Plugin.java | 129 + .../systemui/plugins/PluginFragment.java | 45 + .../plugins/PluginLifecycleManager.java | 61 + .../systemui/plugins/PluginListener.java | 123 + .../systemui/plugins/PluginManager.java | 58 + .../plugins/annotations/Dependencies.java | 27 + .../plugins/annotations/DependsOn.java | 32 + .../annotations/ProvidesInterface.java | 30 + .../plugins/annotations/Requirements.java | 27 + .../plugins/annotations/Requires.java | 33 + systemUIShared/build.gradle | 9 +- systemUIViewCapture/.gitignore | 13 + systemUIViewCapture/Android.bp | 73 + systemUIViewCapture/AndroidManifest.xml | 23 + systemUIViewCapture/OWNERS | 2 + systemUIViewCapture/README.md | 11 + systemUIViewCapture/TEST_MAPPING | 15 + systemUIViewCapture/build.gradle | 23 + .../app/viewcapture/LooperExecutor.java | 59 + .../viewcapture/SettingsAwareViewCapture.kt | 107 + .../app/viewcapture/SimpleViewCapture.kt | 6 + .../android/app/viewcapture/ViewCapture.java | 623 +++ .../app/viewcapture/proto/view_capture.proto | 83 + systemUIViewCapture/tests/AndroidManifest.xml | 36 + .../SettingsAwareViewCaptureTest.kt | 108 + .../android/app/viewcapture/TestActivity.kt | 45 + .../app/viewcapture/ViewCaptureTest.kt | 111 + wmshell/Android.bp | 234 + wmshell/AndroidManifest.xml | 26 + wmshell/OWNERS | 5 + wmshell/aconfig/Android.bp | 13 + wmshell/aconfig/OWNERS | 3 + wmshell/aconfig/multitasking.aconfig | 123 + wmshell/build.gradle | 30 + wmshell/multivalentTests/Android.bp | 99 + wmshell/multivalentTests/AndroidManifest.xml | 13 + .../AndroidManifestRobolectric.xml | 3 + wmshell/multivalentTests/AndroidTest.xml | 31 + wmshell/multivalentTests/OWNERS | 4 + .../robolectric/config/robolectric.properties | 2 + .../wm/shell/bubbles/BubblePositionerTest.kt | 670 +++ .../wm/shell/bubbles/BubbleStackViewTest.kt | 462 ++ .../wm/shell/bubbles/BubbleTaskViewTest.kt | 92 + .../BubbleExpandedViewPinControllerTest.kt | 459 ++ wmshell/multivalentTestsForDevice/Android.bp | 99 + .../AndroidManifest.xml | 13 + .../AndroidManifestRobolectric.xml | 3 + .../multivalentTestsForDevice/AndroidTest.xml | 31 + wmshell/multivalentTestsForDevice/OWNERS | 4 + .../robolectric/config/robolectric.properties | 2 + .../wm/shell/bubbles/BubblePositionerTest.kt | 670 +++ .../wm/shell/bubbles/BubbleStackViewTest.kt | 462 ++ .../wm/shell/bubbles/BubbleTaskViewTest.kt | 92 + .../BubbleExpandedViewPinControllerTest.kt | 459 ++ .../multivalentTestsForDeviceless/Android.bp | 99 + .../AndroidManifest.xml | 13 + .../AndroidManifestRobolectric.xml | 3 + .../AndroidTest.xml | 31 + wmshell/multivalentTestsForDeviceless/OWNERS | 4 + .../robolectric/config/robolectric.properties | 2 + .../wm/shell/bubbles/BubblePositionerTest.kt | 670 +++ .../wm/shell/bubbles/BubbleStackViewTest.kt | 462 ++ .../wm/shell/bubbles/BubbleTaskViewTest.kt | 92 + .../BubbleExpandedViewPinControllerTest.kt | 459 ++ wmshell/proto/wm_shell_trace.proto | 27 + wmshell/proto/wm_shell_transition_trace.proto | 58 + wmshell/res/anim/forced_resizable_enter.xml | 21 + wmshell/res/anim/forced_resizable_exit.xml | 22 + .../tv_window_menu_action_button_animator.xml | 67 + .../bubble_drop_target_background_color.xml | 20 + .../res/color/compat_background_ripple.xml | 19 + wmshell/res/color/decor_button_dark_color.xml | 21 + .../res/color/decor_button_light_color.xml | 21 + wmshell/res/color/decor_title_color.xml | 23 + ...ode_caption_button_color_selector_dark.xml | 21 + ...de_caption_button_color_selector_light.xml | 21 + ...de_maximize_menu_button_color_selector.xml | 26 + ...ation_dismiss_button_background_ripple.xml | 19 + ...erbox_restart_button_background_ripple.xml | 19 + ...start_dismiss_button_background_ripple.xml | 19 + .../one_handed_tutorial_background_color.xml | 21 + wmshell/res/color/taskbar_background_dark.xml | 20 + .../res/color/tv_window_menu_close_icon.xml | 19 + .../color/tv_window_menu_close_icon_bg.xml | 21 + wmshell/res/color/tv_window_menu_icon.xml | 23 + wmshell/res/color/tv_window_menu_icon_bg.xml | 21 + wmshell/res/color/unfold_background.xml | 4 + .../reachability_education_ic_left_hand.xml | 27 + .../reachability_education_ic_right_hand.xml | 25 + .../bubble_drop_target_background.xml | 26 + .../res/drawable/bubble_ic_create_bubble.xml | 25 + .../bubble_ic_empty_overflow_dark.xml | 162 + .../bubble_ic_empty_overflow_light.xml | 162 + .../drawable/bubble_ic_overflow_button.xml | 24 + .../res/drawable/bubble_ic_stop_bubble.xml | 25 + wmshell/res/drawable/bubble_manage_btn_bg.xml | 31 + .../res/drawable/bubble_manage_menu_bg.xml | 27 + .../res/drawable/bubble_manage_menu_row.xml | 21 + .../drawable/bubble_manage_menu_section.xml | 24 + .../bubble_stack_user_education_bg.xml | 23 + .../bubble_stack_user_education_bg_rtl.xml | 23 + .../drawable/camera_compat_dismiss_button.xml | 33 + .../drawable/camera_compat_dismiss_ripple.xml | 20 + ...camera_compat_treatment_applied_button.xml | 32 + ...camera_compat_treatment_applied_ripple.xml | 20 + ...mera_compat_treatment_suggested_button.xml | 53 + ...mera_compat_treatment_suggested_ripple.xml | 20 + wmshell/res/drawable/caption_decor_title.xml | 22 + wmshell/res/drawable/circular_progress.xml | 33 + wmshell/res/drawable/compat_hint_bubble.xml | 21 + wmshell/res/drawable/compat_hint_point.xml | 25 + .../res/drawable/decor_back_button_dark.xml | 32 + .../res/drawable/decor_close_button_dark.xml | 30 + ...ecor_desktop_mode_maximize_button_dark.xml | 26 + wmshell/res/drawable/decor_handle_dark.xml | 32 + .../drawable/decor_maximize_button_dark.xml | 36 + .../drawable/decor_minimize_button_dark.xml | 24 + ...ktop_mode_decor_handle_menu_background.xml | 22 + .../drawable/desktop_mode_header_ic_close.xml | 26 + .../desktop_mode_ic_handle_menu_close.xml | 26 + .../desktop_mode_ic_handle_menu_desktop.xml | 26 + .../desktop_mode_ic_handle_menu_floating.xml | 26 + ...desktop_mode_ic_handle_menu_fullscreen.xml | 26 + ...desktop_mode_ic_handle_menu_screenshot.xml | 34 + ...esktop_mode_ic_handle_menu_splitscreen.xml | 26 + .../desktop_mode_maximize_menu_background.xml | 22 + ...p_mode_maximize_menu_button_background.xml | 23 + ...p_mode_maximize_menu_layout_background.xml | 25 + ...ximize_menu_layout_background_on_hover.xml | 24 + ...esktop_windowing_transition_background.xml | 32 + .../drawable/dismiss_circle_background.xml | 27 + .../drawable/floating_dismiss_gradient.xml | 25 + .../floating_dismiss_gradient_transition.xml | 20 + .../res/drawable/handle_menu_background.xml | 30 + wmshell/res/drawable/home_icon.xml | 45 + .../drawable/ic_baseline_expand_more_24.xml | 27 + .../drawable/ic_bubbles_shortcut_widget.xml | 19 + .../ic_bubbles_shortcut_widget_background.xml | 24 + .../ic_bubbles_shortcut_widget_foreground.xml | 36 + wmshell/res/drawable/ic_expand_less.xml | 25 + .../res/drawable/ic_floating_landscape.xml | 26 + wmshell/res/drawable/ic_remove_no_shadow.xml | 28 + .../letterbox_education_dialog_background.xml | 22 + ...ation_dismiss_button_background_ripple.xml | 44 + .../letterbox_education_ic_light_bulb.xml | 27 + .../letterbox_education_ic_reposition.xml | 30 + .../letterbox_education_ic_split_screen.xml | 26 + ...erbox_restart_button_background_ripple.xml | 44 + .../letterbox_restart_checkbox_button.xml | 25 + .../letterbox_restart_checkbox_checked.xml | 32 + .../letterbox_restart_checkbox_unchecked.xml | 32 + .../letterbox_restart_dialog_background.xml | 22 + ...start_dismiss_button_background_ripple.xml | 46 + .../letterbox_restart_header_ic_arrows.xml | 32 + .../drawable/letterbox_restart_ic_arrows.xml | 31 + .../res/drawable/one_handed_tutorial_icon.xml | 14 + wmshell/res/drawable/pip_custom_close_bg.xml | 24 + wmshell/res/drawable/pip_expand.xml | 28 + wmshell/res/drawable/pip_ic_close_white.xml | 25 + wmshell/res/drawable/pip_ic_collapse.xml | 25 + wmshell/res/drawable/pip_ic_expand.xml | 25 + .../res/drawable/pip_ic_fullscreen_white.xml | 25 + wmshell/res/drawable/pip_ic_move_white.xml | 27 + wmshell/res/drawable/pip_ic_pause_white.xml | 26 + .../res/drawable/pip_ic_play_arrow_white.xml | 26 + wmshell/res/drawable/pip_ic_settings.xml | 28 + .../res/drawable/pip_ic_skip_next_white.xml | 28 + .../drawable/pip_ic_skip_previous_white.xml | 28 + wmshell/res/drawable/pip_icon.xml | 25 + wmshell/res/drawable/pip_menu_background.xml | 22 + wmshell/res/drawable/pip_resize_handle.xml | 29 + .../reachability_education_ic_left_hand.xml | 27 + .../reachability_education_ic_right_hand.xml | 25 + wmshell/res/drawable/rounded_button.xml | 19 + .../drawable/size_compat_restart_button.xml | 34 + .../size_compat_restart_button_ripple.xml | 20 + .../res/drawable/tv_pip_menu_background.xml | 23 + wmshell/res/drawable/tv_pip_menu_border.xml | 33 + .../res/drawable/tv_split_menu_ic_focus.xml | 26 + .../res/drawable/tv_split_menu_ic_swap.xml | 25 + wmshell/res/drawable/tv_window_button_bg.xml | 21 + .../user_aspect_ratio_settings_button.xml | 34 + ...er_aspect_ratio_settings_button_ripple.xml | 20 + wmshell/res/layout/background_panel.xml | 26 + wmshell/res/layout/badged_image_view.xml | 55 + wmshell/res/layout/bubble_bar_drop_target.xml | 22 + .../res/layout/bubble_bar_expanded_view.xml | 30 + .../layout/bubble_bar_manage_education.xml | 56 + wmshell/res/layout/bubble_bar_menu_item.xml | 41 + wmshell/res/layout/bubble_bar_menu_view.xml | 77 + .../res/layout/bubble_bar_stack_education.xml | 56 + wmshell/res/layout/bubble_expanded_view.xml | 32 + wmshell/res/layout/bubble_flyout.xml | 69 + wmshell/res/layout/bubble_manage_button.xml | 33 + wmshell/res/layout/bubble_manage_menu.xml | 106 + wmshell/res/layout/bubble_overflow_button.xml | 22 + .../res/layout/bubble_overflow_container.xml | 76 + wmshell/res/layout/bubble_overflow_view.xml | 42 + .../layout/bubble_stack_user_education.xml | 54 + wmshell/res/layout/bubble_view.xml | 21 + .../bubbles_manage_button_education.xml | 85 + wmshell/res/layout/caption_window_decor.xml | 56 + wmshell/res/layout/compat_mode_hint.xml | 50 + wmshell/res/layout/compat_ui_layout.xml | 73 + .../res/layout/desktop_mode_app_handle.xml | 36 + .../res/layout/desktop_mode_app_header.xml | 99 + .../res/layout/desktop_mode_resize_veil.xml | 27 + .../desktop_mode_window_decor_handle_menu.xml | 139 + ...esktop_mode_window_decor_maximize_menu.xml | 101 + wmshell/res/layout/divider.xml | 21 + .../res/layout/forced_resizable_activity.xml | 26 + wmshell/res/layout/global_drop_target.xml | 18 + ...tterbox_education_dialog_action_layout.xml | 43 + .../letterbox_education_dialog_layout.xml | 122 + .../letterbox_restart_dialog_layout.xml | 140 + wmshell/res/layout/maximize_menu_button.xml | 44 + wmshell/res/layout/one_handed_tutorial.xml | 66 + wmshell/res/layout/pip_menu.xml | 104 + wmshell/res/layout/pip_menu_action.xml | 39 + wmshell/res/layout/reachability_ui_layout.xml | 67 + wmshell/res/layout/split_decor.xml | 31 + wmshell/res/layout/split_divider.xml | 42 + wmshell/res/layout/split_outline.xml | 27 + wmshell/res/layout/tv_pip_menu.xml | 134 + wmshell/res/layout/tv_pip_menu_background.xml | 29 + wmshell/res/layout/tv_split_menu_view.xml | 125 + .../layout/tv_window_menu_action_button.xml | 41 + .../user_aspect_ratio_settings_layout.xml | 41 + wmshell/res/values-af/strings.xml | 124 + wmshell/res/values-af/strings_tv.xml | 34 + wmshell/res/values-am/strings.xml | 124 + wmshell/res/values-am/strings_tv.xml | 34 + wmshell/res/values-ar/strings.xml | 124 + wmshell/res/values-ar/strings_tv.xml | 34 + wmshell/res/values-as/strings.xml | 124 + wmshell/res/values-as/strings_tv.xml | 34 + wmshell/res/values-az/strings.xml | 124 + wmshell/res/values-az/strings_tv.xml | 34 + wmshell/res/values-b+sr+Latn/strings.xml | 124 + wmshell/res/values-b+sr+Latn/strings_tv.xml | 34 + wmshell/res/values-be/strings.xml | 124 + wmshell/res/values-be/strings_tv.xml | 34 + wmshell/res/values-bg/strings.xml | 124 + wmshell/res/values-bg/strings_tv.xml | 34 + wmshell/res/values-bn/strings.xml | 124 + wmshell/res/values-bn/strings_tv.xml | 34 + wmshell/res/values-bs/strings.xml | 124 + wmshell/res/values-bs/strings_tv.xml | 34 + wmshell/res/values-ca/strings.xml | 124 + wmshell/res/values-ca/strings_tv.xml | 34 + wmshell/res/values-cs/strings.xml | 124 + wmshell/res/values-cs/strings_tv.xml | 34 + wmshell/res/values-da/strings.xml | 124 + wmshell/res/values-da/strings_tv.xml | 34 + wmshell/res/values-de/strings.xml | 124 + wmshell/res/values-de/strings_tv.xml | 34 + wmshell/res/values-el/strings.xml | 124 + wmshell/res/values-el/strings_tv.xml | 34 + wmshell/res/values-en-rAU/strings.xml | 124 + wmshell/res/values-en-rAU/strings_tv.xml | 34 + wmshell/res/values-en-rCA/strings.xml | 124 + wmshell/res/values-en-rCA/strings_tv.xml | 34 + wmshell/res/values-en-rGB/strings.xml | 124 + wmshell/res/values-en-rGB/strings_tv.xml | 34 + wmshell/res/values-en-rIN/strings.xml | 124 + wmshell/res/values-en-rIN/strings_tv.xml | 34 + wmshell/res/values-en-rXC/strings.xml | 124 + wmshell/res/values-en-rXC/strings_tv.xml | 34 + wmshell/res/values-es-rUS/strings.xml | 124 + wmshell/res/values-es-rUS/strings_tv.xml | 34 + wmshell/res/values-es/strings.xml | 124 + wmshell/res/values-es/strings_tv.xml | 34 + wmshell/res/values-et/strings.xml | 124 + wmshell/res/values-et/strings_tv.xml | 34 + wmshell/res/values-eu/strings.xml | 124 + wmshell/res/values-eu/strings_tv.xml | 34 + wmshell/res/values-fa/strings.xml | 124 + wmshell/res/values-fa/strings_tv.xml | 34 + wmshell/res/values-fi/strings.xml | 124 + wmshell/res/values-fi/strings_tv.xml | 34 + wmshell/res/values-fr-rCA/strings.xml | 124 + wmshell/res/values-fr-rCA/strings_tv.xml | 34 + wmshell/res/values-fr/strings.xml | 124 + wmshell/res/values-fr/strings_tv.xml | 34 + wmshell/res/values-gl/strings.xml | 124 + wmshell/res/values-gl/strings_tv.xml | 34 + wmshell/res/values-gu/strings.xml | 124 + wmshell/res/values-gu/strings_tv.xml | 34 + wmshell/res/values-hi/strings.xml | 124 + wmshell/res/values-hi/strings_tv.xml | 34 + wmshell/res/values-hr/strings.xml | 124 + wmshell/res/values-hr/strings_tv.xml | 34 + wmshell/res/values-hu/strings.xml | 124 + wmshell/res/values-hu/strings_tv.xml | 34 + wmshell/res/values-hy/strings.xml | 124 + wmshell/res/values-hy/strings_tv.xml | 34 + wmshell/res/values-in/strings.xml | 124 + wmshell/res/values-in/strings_tv.xml | 34 + wmshell/res/values-is/strings.xml | 124 + wmshell/res/values-is/strings_tv.xml | 34 + wmshell/res/values-it/strings.xml | 124 + wmshell/res/values-it/strings_tv.xml | 34 + wmshell/res/values-iw/strings.xml | 124 + wmshell/res/values-iw/strings_tv.xml | 34 + wmshell/res/values-ja/strings.xml | 124 + wmshell/res/values-ja/strings_tv.xml | 34 + wmshell/res/values-ka/strings.xml | 124 + wmshell/res/values-ka/strings_tv.xml | 34 + wmshell/res/values-kk/strings.xml | 124 + wmshell/res/values-kk/strings_tv.xml | 34 + wmshell/res/values-km/strings.xml | 124 + wmshell/res/values-km/strings_tv.xml | 34 + wmshell/res/values-kn/strings.xml | 124 + wmshell/res/values-kn/strings_tv.xml | 34 + wmshell/res/values-ko/strings.xml | 124 + wmshell/res/values-ko/strings_tv.xml | 34 + wmshell/res/values-ky/strings.xml | 124 + wmshell/res/values-ky/strings_tv.xml | 34 + wmshell/res/values-land/dimens.xml | 22 + wmshell/res/values-lo/strings.xml | 124 + wmshell/res/values-lo/strings_tv.xml | 34 + wmshell/res/values-lt/strings.xml | 124 + wmshell/res/values-lt/strings_tv.xml | 34 + wmshell/res/values-lv/strings.xml | 124 + wmshell/res/values-lv/strings_tv.xml | 34 + wmshell/res/values-mk/strings.xml | 124 + wmshell/res/values-mk/strings_tv.xml | 34 + wmshell/res/values-ml/strings.xml | 124 + wmshell/res/values-ml/strings_tv.xml | 34 + wmshell/res/values-mn/strings.xml | 124 + wmshell/res/values-mn/strings_tv.xml | 34 + wmshell/res/values-mr/strings.xml | 124 + wmshell/res/values-mr/strings_tv.xml | 34 + wmshell/res/values-ms/strings.xml | 124 + wmshell/res/values-ms/strings_tv.xml | 34 + wmshell/res/values-my/strings.xml | 124 + wmshell/res/values-my/strings_tv.xml | 34 + wmshell/res/values-nb/strings.xml | 124 + wmshell/res/values-nb/strings_tv.xml | 34 + wmshell/res/values-ne/strings.xml | 124 + wmshell/res/values-ne/strings_tv.xml | 34 + wmshell/res/values-night/colors.xml | 22 + wmshell/res/values-night/styles.xml | 33 + wmshell/res/values-nl/strings.xml | 124 + wmshell/res/values-nl/strings_tv.xml | 34 + wmshell/res/values-or/strings.xml | 124 + wmshell/res/values-or/strings_tv.xml | 34 + wmshell/res/values-pa/strings.xml | 124 + wmshell/res/values-pa/strings_tv.xml | 34 + wmshell/res/values-pl/strings.xml | 124 + wmshell/res/values-pl/strings_tv.xml | 34 + wmshell/res/values-pt-rBR/strings.xml | 124 + wmshell/res/values-pt-rBR/strings_tv.xml | 34 + wmshell/res/values-pt-rPT/strings.xml | 124 + wmshell/res/values-pt-rPT/strings_tv.xml | 34 + wmshell/res/values-pt/strings.xml | 124 + wmshell/res/values-pt/strings_tv.xml | 34 + wmshell/res/values-ro/strings.xml | 124 + wmshell/res/values-ro/strings_tv.xml | 34 + wmshell/res/values-ru/strings.xml | 124 + wmshell/res/values-ru/strings_tv.xml | 34 + wmshell/res/values-si/strings.xml | 124 + wmshell/res/values-si/strings_tv.xml | 34 + wmshell/res/values-sk/strings.xml | 124 + wmshell/res/values-sk/strings_tv.xml | 34 + wmshell/res/values-sl/strings.xml | 124 + wmshell/res/values-sl/strings_tv.xml | 34 + wmshell/res/values-sq/strings.xml | 124 + wmshell/res/values-sq/strings_tv.xml | 34 + wmshell/res/values-sr/strings.xml | 124 + wmshell/res/values-sr/strings_tv.xml | 34 + wmshell/res/values-sv/strings.xml | 124 + wmshell/res/values-sv/strings_tv.xml | 34 + wmshell/res/values-sw/strings.xml | 124 + wmshell/res/values-sw/strings_tv.xml | 34 + wmshell/res/values-sw600dp/config.xml | 25 + wmshell/res/values-ta/strings.xml | 124 + wmshell/res/values-ta/strings_tv.xml | 34 + wmshell/res/values-te/strings.xml | 124 + wmshell/res/values-te/strings_tv.xml | 34 + wmshell/res/values-television/config.xml | 65 + wmshell/res/values-television/dimen.xml | 27 + wmshell/res/values-th/strings.xml | 124 + wmshell/res/values-th/strings_tv.xml | 34 + wmshell/res/values-tl/strings.xml | 124 + wmshell/res/values-tl/strings_tv.xml | 34 + wmshell/res/values-tr/strings.xml | 124 + wmshell/res/values-tr/strings_tv.xml | 34 + wmshell/res/values-tvdpi/dimen.xml | 52 + wmshell/res/values-uk/strings.xml | 124 + wmshell/res/values-uk/strings_tv.xml | 34 + wmshell/res/values-ur/strings.xml | 124 + wmshell/res/values-ur/strings_tv.xml | 34 + wmshell/res/values-uz/strings.xml | 124 + wmshell/res/values-uz/strings_tv.xml | 34 + wmshell/res/values-vi/strings.xml | 124 + wmshell/res/values-vi/strings_tv.xml | 34 + wmshell/res/values-watch/colors.xml | 22 + wmshell/res/values-watch/config.xml | 35 + wmshell/res/values-watch/dimen.xml | 22 + wmshell/res/values-zh-rCN/strings.xml | 124 + wmshell/res/values-zh-rCN/strings_tv.xml | 34 + wmshell/res/values-zh-rHK/strings.xml | 124 + wmshell/res/values-zh-rHK/strings_tv.xml | 34 + wmshell/res/values-zh-rTW/strings.xml | 124 + wmshell/res/values-zh-rTW/strings_tv.xml | 34 + wmshell/res/values-zu/strings.xml | 124 + wmshell/res/values-zu/strings_tv.xml | 34 + wmshell/res/values/attrs.xml | 26 + wmshell/res/values/colors.xml | 70 + wmshell/res/values/colors_tv.xml | 37 + wmshell/res/values/config.xml | 182 + wmshell/res/values/config_tv.xml | 22 + wmshell/res/values/dimen.xml | 600 +++ wmshell/res/values/ids.xml | 47 + wmshell/res/values/integers.xml | 25 + wmshell/res/values/strings.xml | 293 ++ wmshell/res/values/strings_tv.xml | 63 + wmshell/res/values/styles.xml | 137 + .../shell/shared/IHomeTransitionListener.aidl | 33 + .../wm/shell/shared/CounterRotator.java | 83 + .../wm/shell/shared/DesktopModeStatus.java | 184 + .../shell/shared/IHomeTransitionListener.aidl | 33 + .../wm/shell/shared/IShellTransitions.aidl | 62 + .../wm/shell/shared/ShellTransitions.java | 47 + .../wm/shell/shared/TransitionUtil.java | 374 ++ .../annotations/ChoreographerSfVsync.java | 34 + .../annotations/ExternalMainThread.java | 34 + .../shared/annotations/ExternalThread.java | 31 + .../annotations/ShellAnimationThread.java | 31 + .../annotations/ShellBackgroundThread.java | 33 + .../shared/annotations/ShellMainThread.java | 31 + .../annotations/ShellSplashscreenThread.java | 33 + .../android/wm/shell/ProtoLogController.java | 143 + .../wm/shell/RootDisplayAreaOrganizer.java | 164 + .../shell/RootTaskDisplayAreaOrganizer.java | 319 ++ .../android/wm/shell/ShellTaskOrganizer.java | 903 ++++ .../wm/shell/WindowManagerShellWrapper.java | 62 + .../ActivityEmbeddingAnimationAdapter.java | 236 + .../ActivityEmbeddingAnimationRunner.java | 632 +++ .../ActivityEmbeddingAnimationSpec.java | 286 ++ .../ActivityEmbeddingController.java | 238 ++ .../shell/animation/FlingAnimationUtils.java | 470 ++ .../wm/shell/animation/FloatProperties.kt | 141 + .../wm/shell/animation/Interpolators.java | 117 + .../android/wm/shell/back/BackAnimation.java | 110 + .../shell/back/BackAnimationBackground.java | 144 + .../wm/shell/back/BackAnimationConstants.java | 24 + .../shell/back/BackAnimationController.java | 1051 +++++ .../wm/shell/back/BackAnimationRunner.java | 138 + .../shell/back/CrossActivityBackAnimation.kt | 590 +++ .../wm/shell/back/CrossTaskBackAnimation.java | 392 ++ .../back/CustomCrossActivityBackAnimation.kt | 296 ++ .../back/DefaultCrossActivityBackAnimation.kt | 109 + .../android/wm/shell/back/IBackAnimation.aidl | 43 + wmshell/src/com/android/wm/shell/back/OWNERS | 5 + .../wm/shell/back/ShellBackAnimation.java | 57 + .../back/ShellBackAnimationRegistry.java | 176 + .../wm/shell/back/StatusBarCustomizer.java | 30 + .../com/android/wm/shell/back/TEST_MAPPING | 32 + .../wm/shell/bubbles/BadgedImageView.java | 384 ++ .../com/android/wm/shell/bubbles/Bubble.java | 1113 +++++ .../wm/shell/bubbles/BubbleController.java | 2820 ++++++++++++ .../android/wm/shell/bubbles/BubbleData.java | 1263 ++++++ .../wm/shell/bubbles/BubbleDataRepository.kt | 275 ++ .../wm/shell/bubbles/BubbleDebugConfig.java | 92 + .../bubbles/BubbleEducationController.kt | 86 + .../android/wm/shell/bubbles/BubbleEntry.java | 140 + .../wm/shell/bubbles/BubbleExpandedView.java | 1152 +++++ .../bubbles/BubbleExpandedViewManager.kt | 84 + .../wm/shell/bubbles/BubbleFlyoutView.java | 581 +++ .../wm/shell/bubbles/BubbleLogger.java | 159 + .../wm/shell/bubbles/BubbleOverflow.kt | 278 ++ .../bubbles/BubbleOverflowContainerView.java | 388 ++ .../wm/shell/bubbles/BubblePopupViewExt.kt | 47 + .../wm/shell/bubbles/BubblePositioner.java | 940 ++++ .../wm/shell/bubbles/BubbleStackView.java | 3806 +++++++++++++++++ .../shell/bubbles/BubbleStackViewManager.kt | 60 + .../wm/shell/bubbles/BubbleTaskView.kt | 109 + .../wm/shell/bubbles/BubbleTaskViewFactory.kt | 23 + .../shell/bubbles/BubbleTaskViewHelper.java | 275 ++ .../wm/shell/bubbles/BubbleViewInfoTask.java | 340 ++ .../wm/shell/bubbles/BubbleViewProvider.java | 79 + .../com/android/wm/shell/bubbles/Bubbles.java | 380 ++ .../bubbles/BubblesNavBarGestureTracker.java | 99 + .../BubblesNavBarInputEventReceiver.java | 51 + .../BubblesNavBarMotionEventHandler.java | 174 + .../bubbles/BubblesTransitionObserver.java | 83 + .../android/wm/shell/bubbles/DeviceConfig.kt | 67 + .../wm/shell/bubbles/DismissViewExt.kt | 34 + .../android/wm/shell/bubbles/IBubbles.aidl | 51 + .../wm/shell/bubbles/IBubblesListener.aidl | 36 + .../wm/shell/bubbles/ManageEducationView.kt | 229 + .../src/com/android/wm/shell/bubbles/OWNERS | 6 + .../wm/shell/bubbles/ObjectWrapper.java | 46 + .../wm/shell/bubbles/StackEducationView.kt | 208 + .../animation/AnimatableScaleMatrix.java | 144 + .../ExpandedAnimationController.java | 655 +++ .../ExpandedViewAnimationController.java | 94 + .../ExpandedViewAnimationControllerImpl.java | 420 ++ .../bubbles/animation/FlingToDismissUtils.kt | 42 + .../bubbles/animation/OneTimeEndListener.java | 34 + .../shell/bubbles/animation/OverScroll.java | 57 + .../animation/PhysicsAnimationLayout.java | 1211 ++++++ .../animation/StackAnimationController.java | 1078 +++++ .../bubbles/bar/BubbleBarAnimationHelper.java | 529 +++ .../bubbles/bar/BubbleBarExpandedView.java | 458 ++ .../BubbleBarExpandedViewDragController.kt | 176 + .../bubbles/bar/BubbleBarHandleView.java | 111 + .../shell/bubbles/bar/BubbleBarLayerView.java | 407 ++ .../bubbles/bar/BubbleBarMenuItemView.java | 77 + .../shell/bubbles/bar/BubbleBarMenuView.java | 145 + .../bar/BubbleBarMenuViewController.java | 247 ++ .../bar/BubbleEducationViewController.kt | 246 ++ .../bar/BubbleExpandedViewPinController.kt | 93 + .../bubbles/properties/BubbleProperties.kt | 35 + .../properties/ProdBubbleProperties.kt | 35 + .../bubbles/shortcut/BubbleShortcutHelper.kt | 40 + .../shortcut/CreateBubbleShortcutActivity.kt | 52 + .../bubbles/shortcut/ShowBubblesActivity.kt | 59 + .../wm/shell/bubbles/storage/BubbleEntity.kt | 32 + .../storage/BubblePersistentRepository.kt | 63 + .../storage/BubbleVolatileRepository.kt | 174 + .../shell/bubbles/storage/BubbleXmlHelper.kt | 157 + .../wm/shell/common/AlphaOptimizedButton.java | 50 + .../shell/common/DevicePostureController.java | 150 + .../shell/common/DisplayChangeController.java | 145 + .../wm/shell/common/DisplayController.java | 402 ++ .../wm/shell/common/DisplayImeController.java | 754 ++++ .../shell/common/DisplayInsetsController.java | 320 ++ .../wm/shell/common/DisplayLayout.java | 463 ++ .../wm/shell/common/DockStateReader.java | 57 + .../shell/common/ExternalInterfaceBinder.java | 74 + .../common/FloatingContentCoordinator.kt | 367 ++ .../wm/shell/common/HandlerExecutor.java | 57 + .../common/InteractionJankMonitorUtils.java | 84 + .../shell/common/LaunchAdjacentController.kt | 90 + .../wm/shell/common/MultiInstanceHelper.kt | 128 + .../src/com/android/wm/shell/common/OWNERS | 2 + .../wm/shell/common/RemoteCallable.java | 34 + .../wm/shell/common/ScreenshotUtils.java | 118 + .../wm/shell/common/ShellExecutor.java | 99 + .../common/SingleInstanceRemoteListener.java | 126 + .../android/wm/shell/common/SurfaceUtils.java | 44 + .../wm/shell/common/SyncTransactionQueue.java | 242 ++ .../wm/shell/common/SystemWindows.java | 415 ++ .../shell/common/TabletopModeController.java | 238 ++ .../common/TaskStackListenerCallback.java | 93 + .../shell/common/TaskStackListenerImpl.java | 432 ++ .../wm/shell/common/TransactionPool.java | 50 + .../wm/shell/common/TriangleShape.java | 77 + .../common/TvWindowMenuActionButton.java | 149 + .../common/bubbles/BaseBubblePinController.kt | 210 + .../common/bubbles/BubbleBarLocation.aidl | 19 + .../shell/common/bubbles/BubbleBarLocation.kt | 63 + .../shell/common/bubbles/BubbleBarUpdate.java | 186 + .../shell/common/bubbles/BubbleConstants.java | 26 + .../wm/shell/common/bubbles/BubbleInfo.java | 188 + .../common/bubbles/BubblePopupDrawable.kt | 232 + .../shell/common/bubbles/BubblePopupView.kt | 66 + .../common/bubbles/DismissCircleView.java | 76 + .../wm/shell/common/bubbles/DismissView.kt | 229 + .../android/wm/shell/common/bubbles/OWNERS | 6 + .../common/bubbles/RelativeTouchListener.kt | 180 + .../shell/common/bubbles/RemovedBubble.java | 70 + .../DesktopModeTransitionSource.aidl | 19 + .../DesktopModeTransitionSource.kt | 54 + .../common/magnetictarget/MagnetizedObject.kt | 701 +++ .../com/android/wm/shell/common/pip/IPip.aidl | 94 + .../common/pip/IPipAnimationListener.aidl | 41 + .../shell/common/pip/LegacySizeSpecSource.kt | 201 + .../pip/PhonePipKeepClearAlgorithm.java | 169 + .../shell/common/pip/PhoneSizeSpecSource.kt | 301 ++ .../wm/shell/common/pip/PipAppOpsListener.kt | 78 + .../shell/common/pip/PipBoundsAlgorithm.java | 475 ++ .../wm/shell/common/pip/PipBoundsState.java | 691 +++ .../common/pip/PipDisplayLayoutState.java | 142 + .../shell/common/pip/PipDoubleTapHelper.java | 121 + .../pip/PipKeepClearAlgorithmInterface.java | 53 + .../wm/shell/common/pip/PipMediaController.kt | 364 ++ .../shell/common/pip/PipMenuController.java | 122 + .../common/pip/PipPerfHintController.java | 166 + .../common/pip/PipPinchResizingAlgorithm.java | 134 + .../wm/shell/common/pip/PipSnapAlgorithm.java | 159 + .../wm/shell/common/pip/PipUiEventLogger.kt | 125 + .../android/wm/shell/common/pip/PipUtils.kt | 186 + .../wm/shell/common/pip/SizeSpecSource.kt | 51 + .../shell/common/split/DividerHandleView.java | 179 + .../common/split/DividerRoundedCorner.java | 160 + .../common/split/DividerSnapAlgorithm.java | 511 +++ .../wm/shell/common/split/DividerView.java | 514 +++ .../common/split/DockedDividerUtils.java | 140 + .../com/android/wm/shell/common/split/OWNERS | 4 + .../shell/common/split/SplitDecorManager.java | 489 +++ .../wm/shell/common/split/SplitLayout.java | 1297 ++++++ .../common/split/SplitScreenConstants.java | 144 + .../shell/common/split/SplitScreenUtils.java | 137 + .../common/split/SplitWindowManager.java | 213 + .../wm/shell/compatui/AppCompatUtils.kt | 24 + .../shell/compatui/CompatUIConfiguration.java | 269 ++ .../wm/shell/compatui/CompatUIController.java | 829 ++++ .../wm/shell/compatui/CompatUILayout.java | 152 + .../compatui/CompatUIShellCommandHandler.java | 103 + .../shell/compatui/CompatUIWindowManager.java | 289 ++ .../CompatUIWindowManagerAbstract.java | 435 ++ .../compatui/DialogAnimationController.java | 206 + .../compatui/DialogContainerSupplier.java | 36 + .../LetterboxEduDialogActionLayout.java | 67 + .../compatui/LetterboxEduDialogLayout.java | 97 + .../compatui/LetterboxEduWindowManager.java | 250 ++ .../shell/compatui/ReachabilityEduLayout.java | 269 ++ .../ReachabilityEduWindowManager.java | 321 ++ .../shell/compatui/RestartDialogLayout.java | 111 + .../compatui/RestartDialogWindowManager.java | 243 ++ .../UserAspectRatioSettingsLayout.java | 179 + .../UserAspectRatioSettingsWindowManager.java | 257 ++ .../wm/shell/dagger/DynamicOverride.java | 121 + .../wm/shell/dagger/ShellCreateTrigger.java | 38 + .../dagger/ShellCreateTriggerOverride.java | 38 + .../wm/shell/dagger/TvWMShellModule.java | 101 + .../wm/shell/dagger/WMShellBaseModule.java | 998 +++++ .../dagger/WMShellConcurrencyModule.java | 217 + .../wm/shell/dagger/WMShellModule.java | 671 +++ .../android/wm/shell/dagger/WMSingleton.java | 33 + .../dagger/back/ShellBackAnimationModule.java | 63 + .../com/android/wm/shell/dagger/pip/OWNERS | 1 + .../wm/shell/dagger/pip/Pip1Module.java | 230 + .../wm/shell/dagger/pip/Pip1SharedModule.java | 46 + .../wm/shell/dagger/pip/Pip2Module.java | 173 + .../wm/shell/dagger/pip/PipModule.java | 47 + .../wm/shell/dagger/pip/TvPipModule.java | 241 ++ .../wm/shell/desktopmode/DesktopMode.java | 60 + .../desktopmode/DesktopModeEventLogger.kt | 220 + .../DesktopModeLoggerTransitionObserver.kt | 394 ++ .../DesktopModeShellCommandHandler.kt | 95 + .../desktopmode/DesktopModeTaskRepository.kt | 425 ++ .../desktopmode/DesktopModeTransitionTypes.kt | 95 + .../desktopmode/DesktopModeUiEventLogger.kt | 93 + .../wm/shell/desktopmode/DesktopModeUtils.kt | 173 + .../DesktopModeVisualIndicator.java | 483 +++ .../desktopmode/DesktopTasksController.kt | 1565 +++++++ .../shell/desktopmode/DesktopTasksLimiter.kt | 218 + .../DesktopTasksTransitionObserver.kt | 96 + .../desktopmode/DesktopWallpaperActivity.kt | 57 + .../DragToDesktopTransitionHandler.kt | 791 ++++ .../EnterDesktopTaskTransitionHandler.java | 190 + .../ExitDesktopTaskTransitionHandler.java | 195 + .../wm/shell/desktopmode/IDesktopMode.aidl | 52 + .../desktopmode/IDesktopTaskListener.aidl | 30 + .../com/android/wm/shell/desktopmode/OWNERS | 7 + ...oggleResizeDesktopTaskTransitionHandler.kt | 151 + .../displayareahelper/DisplayAreaHelper.java | 39 + .../DisplayAreaHelperController.java | 45 + .../src/com/android/wm/shell/docs/README.md | 18 + .../src/com/android/wm/shell/docs/changes.md | 106 + .../src/com/android/wm/shell/docs/dagger.md | 50 + .../com/android/wm/shell/docs/debugging.md | 138 + .../com/android/wm/shell/docs/extending.md | 13 + .../src/com/android/wm/shell/docs/overview.md | 57 + .../wm/shell/docs/patterns/TEMPLATE.md | 30 + .../src/com/android/wm/shell/docs/sysui.md | 85 + .../src/com/android/wm/shell/docs/testing.md | 63 + .../com/android/wm/shell/docs/threading.md | 83 + .../draganddrop/DragAndDropConstants.java | 27 + .../draganddrop/DragAndDropController.java | 554 +++ .../draganddrop/DragAndDropEventLogger.java | 139 + .../shell/draganddrop/DragAndDropPolicy.java | 426 ++ .../wm/shell/draganddrop/DragLayout.java | 574 +++ .../wm/shell/draganddrop/DragSession.java | 101 + .../wm/shell/draganddrop/DragUtils.java | 108 + .../wm/shell/draganddrop/DropZoneView.java | 281 ++ .../shell/draganddrop/GlobalDragListener.kt | 131 + .../wm/shell/draganddrop/IDragAndDrop.aidl | 28 + .../wm/shell/freeform/FreeformComponents.java | 59 + .../shell/freeform/FreeformTaskListener.java | 205 + .../FreeformTaskTransitionHandler.java | 258 ++ .../FreeformTaskTransitionObserver.java | 182 + .../FreeformTaskTransitionStarter.java | 51 + .../src/com/android/wm/shell/freeform/OWNERS | 7 + .../fullscreen/FullscreenTaskListener.java | 209 + .../HideDisplayCutoutController.java | 124 + .../HideDisplayCutoutOrganizer.java | 362 ++ .../keyguard/KeyguardTransitionHandler.java | 389 ++ .../shell/keyguard/KeyguardTransitions.java | 47 + .../onehanded/BackgroundWindowManager.java | 242 ++ .../wm/shell/onehanded/IOneHanded.aidl | 33 + .../src/com/android/wm/shell/onehanded/OWNERS | 2 + .../android/wm/shell/onehanded/OneHanded.java | 66 + .../onehanded/OneHandedAccessibilityUtil.java | 91 + .../onehanded/OneHandedAnimationCallback.java | 52 + .../OneHandedAnimationController.java | 321 ++ .../shell/onehanded/OneHandedController.java | 797 ++++ .../OneHandedDisplayAreaOrganizer.java | 371 ++ .../onehanded/OneHandedEventCallback.java | 28 + .../onehanded/OneHandedSettingsUtil.java | 261 ++ .../wm/shell/onehanded/OneHandedState.java | 120 + .../OneHandedSurfaceTransactionHelper.java | 97 + .../onehanded/OneHandedTimeoutHandler.java | 130 + .../onehanded/OneHandedTouchHandler.java | 184 + .../OneHandedTransitionCallback.java | 43 + .../onehanded/OneHandedTutorialHandler.java | 314 ++ .../onehanded/OneHandedUiEventLogger.java | 291 ++ .../shell/performance/PerfHintController.kt | 56 + wmshell/src/com/android/wm/shell/pip/OWNERS | 2 + .../pip/PinnedStackListenerForwarder.java | 114 + wmshell/src/com/android/wm/shell/pip/Pip.java | 77 + .../wm/shell/pip/PipAnimationController.java | 790 ++++ .../wm/shell/pip/PipContentOverlay.java | 269 ++ .../shell/pip/PipParamsChangedForwarder.java | 125 + .../pip/PipSurfaceTransactionHelper.java | 274 ++ .../wm/shell/pip/PipTaskOrganizer.java | 2071 +++++++++ .../android/wm/shell/pip/PipTransition.java | 1326 ++++++ .../wm/shell/pip/PipTransitionController.java | 359 ++ .../wm/shell/pip/PipTransitionState.java | 165 + .../pip/phone/PhonePipMenuController.java | 579 +++ ...PipAccessibilityInteractionConnection.java | 391 ++ .../wm/shell/pip/phone/PipController.java | 1348 ++++++ .../pip/phone/PipDismissTargetHandler.java | 311 ++ .../wm/shell/pip/phone/PipInputConsumer.java | 185 + .../wm/shell/pip/phone/PipMenuActionView.java | 56 + .../pip/phone/PipMenuIconsAlgorithm.java | 71 + .../wm/shell/pip/phone/PipMenuView.java | 603 +++ .../wm/shell/pip/phone/PipMotionHelper.java | 734 ++++ .../pip/phone/PipResizeGestureHandler.java | 541 +++ .../wm/shell/pip/phone/PipTouchGesture.java | 47 + .../wm/shell/pip/phone/PipTouchHandler.java | 1093 +++++ .../wm/shell/pip/phone/PipTouchState.java | 426 ++ .../wm/shell/pip/tv/CenteredImageSpan.java | 76 + .../src/com/android/wm/shell/pip/tv/OWNERS | 3 + .../android/wm/shell/pip/tv/TvPipAction.java | 106 + .../wm/shell/pip/tv/TvPipActionsProvider.java | 257 ++ .../wm/shell/pip/tv/TvPipBackgroundView.java | 106 + .../wm/shell/pip/tv/TvPipBoundsAlgorithm.java | 366 ++ .../shell/pip/tv/TvPipBoundsController.java | 247 ++ .../wm/shell/pip/tv/TvPipBoundsState.java | 245 ++ .../wm/shell/pip/tv/TvPipController.java | 804 ++++ .../wm/shell/pip/tv/TvPipCustomAction.java | 98 + .../wm/shell/pip/tv/TvPipInterpolators.java | 47 + .../shell/pip/tv/TvPipKeepClearAlgorithm.kt | 771 ++++ .../wm/shell/pip/tv/TvPipMenuController.java | 645 +++ .../shell/pip/tv/TvPipMenuEduTextDrawer.java | 303 ++ .../wm/shell/pip/tv/TvPipMenuView.java | 663 +++ .../pip/tv/TvPipNotificationController.java | 258 ++ .../wm/shell/pip/tv/TvPipSystemAction.java | 89 + .../wm/shell/pip/tv/TvPipTaskOrganizer.java | 121 + .../wm/shell/pip/tv/TvPipTransition.java | 838 ++++ wmshell/src/com/android/wm/shell/pip2/OWNERS | 3 + .../pip2/PipSurfaceTransactionHelper.java | 277 ++ .../src/com/android/wm/shell/pip2/README.md | 3 + .../pip2/animation/PipAlphaAnimator.java | 117 + .../pip2/animation/PipResizeAnimator.java | 154 + .../pip2/phone/PhonePipMenuController.java | 577 +++ .../wm/shell/pip2/phone/PipController.java | 451 ++ .../pip2/phone/PipDismissTargetHandler.java | 311 ++ .../wm/shell/pip2/phone/PipInputConsumer.java | 188 + .../shell/pip2/phone/PipMenuActionView.java | 56 + .../pip2/phone/PipMenuIconsAlgorithm.java | 71 + .../wm/shell/pip2/phone/PipMenuView.java | 600 +++ .../wm/shell/pip2/phone/PipMotionHelper.java | 790 ++++ .../pip2/phone/PipResizeGestureHandler.java | 598 +++ .../wm/shell/pip2/phone/PipScheduler.java | 229 + .../wm/shell/pip2/phone/PipTouchGesture.java | 47 + .../wm/shell/pip2/phone/PipTouchHandler.java | 1128 +++++ .../wm/shell/pip2/phone/PipTouchState.java | 427 ++ .../wm/shell/pip2/phone/PipTransition.java | 564 +++ .../shell/pip2/phone/PipTransitionState.java | 322 ++ .../src/com/android/wm/shell/protolog/OWNERS | 1 + .../wm/shell/protolog/ShellProtoLogGroup.java | 139 + .../wm/shell/recents/IRecentTasks.aidl | 59 + .../shell/recents/IRecentTasksListener.aidl | 48 + .../src/com/android/wm/shell/recents/OWNERS | 6 + .../android/wm/shell/recents/RecentTasks.java | 54 + .../shell/recents/RecentTasksController.java | 660 +++ .../recents/RecentsTransitionHandler.java | 1346 ++++++ .../RecentsTransitionStateListener.java | 30 + .../recents/TaskStackTransitionObserver.kt | 143 + .../wm/shell/splitscreen/ISplitScreen.aidl | 167 + .../splitscreen/ISplitScreenListener.aidl | 33 + .../splitscreen/ISplitSelectListener.aidl | 29 + .../wm/shell/splitscreen/MainStage.java | 85 + .../com/android/wm/shell/splitscreen/OWNERS | 4 + .../wm/shell/splitscreen/SideStage.java | 73 + .../wm/shell/splitscreen/SplitScreen.java | 131 + .../splitscreen/SplitScreenController.java | 1433 +++++++ .../SplitScreenShellCommandHandler.java | 121 + .../splitscreen/SplitScreenTransitions.java | 685 +++ .../splitscreen/SplitscreenEventLogger.java | 435 ++ .../shell/splitscreen/StageCoordinator.java | 3753 ++++++++++++++++ .../shell/splitscreen/StageTaskListener.java | 484 +++ .../android/wm/shell/splitscreen/tv/OWNERS | 3 + .../splitscreen/tv/TvSplitMenuController.java | 218 + .../shell/splitscreen/tv/TvSplitMenuView.java | 117 + .../tv/TvSplitScreenController.java | 123 + .../splitscreen/tv/TvStageCoordinator.java | 96 + .../AbsSplashWindowCreator.java | 67 + .../startingsurface/IStartingWindow.aidl | 29 + .../IStartingWindowListener.aidl | 31 + .../SnapshotWindowCreator.java | 72 + .../SplashScreenExitAnimation.java | 146 + .../SplashScreenExitAnimationUtils.java | 458 ++ .../SplashscreenContentDrawer.java | 1291 ++++++ .../SplashscreenIconDrawableFactory.java | 391 ++ .../SplashscreenWindowCreator.java | 475 ++ .../startingsurface/StartingSurface.java | 39 + .../StartingSurfaceDrawer.java | 369 ++ .../StartingWindowController.java | 309 ++ .../StartingWindowTypeAlgorithm.java | 30 + .../startingsurface/TaskSnapshotWindow.java | 247 ++ .../WindowlessSnapshotWindowCreator.java | 164 + .../WindowlessSplashWindowCreator.java | 151 + .../PhoneStartingWindowTypeAlgorithm.java | 112 + .../tv/TvStartingWindowTypeAlgorithm.java | 35 + .../sysui/ConfigurationChangeListener.java | 51 + .../shell/sysui/DisplayImeChangeListener.java | 34 + .../shell/sysui/KeyguardChangeListener.java | 36 + .../wm/shell/sysui/ShellCommandHandler.java | 128 + .../wm/shell/sysui/ShellController.java | 439 ++ .../com/android/wm/shell/sysui/ShellInit.java | 91 + .../wm/shell/sysui/ShellInterface.java | 96 + .../wm/shell/sysui/ShellSharedConstants.java | 48 + .../wm/shell/sysui/UserChangeListener.java | 39 + .../android/wm/shell/taskview/TaskView.java | 329 ++ .../wm/shell/taskview/TaskViewBase.java | 64 + .../wm/shell/taskview/TaskViewFactory.java | 32 + .../taskview/TaskViewFactoryController.java | 80 + .../taskview/TaskViewTaskController.java | 717 ++++ .../shell/taskview/TaskViewTransitions.java | 449 ++ .../transition/CounterRotatorHelper.java | 110 + .../shell/transition/DefaultMixedHandler.java | 717 ++++ .../transition/DefaultMixedTransition.java | 327 ++ .../transition/DefaultTransitionHandler.java | 1012 +++++ .../transition/HomeTransitionObserver.java | 141 + .../shell/transition/LegacyTransitions.java | 139 + .../transition/MixedTransitionHandler.java | 31 + .../transition/MixedTransitionHelper.java | 225 + .../transition/OneShotRemoteHandler.java | 242 ++ .../transition/RecentsMixedTransition.java | 227 + .../transition/RemoteTransitionHandler.java | 422 ++ .../transition/ScreenRotationAnimation.java | 410 ++ .../wm/shell/transition/SleepHandler.java | 57 + .../transition/TransitionAnimationHelper.java | 420 ++ .../wm/shell/transition/Transitions.java | 1743 ++++++++ .../wm/shell/transition/WindowThumbnail.java | 71 + .../tracing/LegacyTransitionTracer.java | 347 ++ .../wm/shell/transition/tracing/OWNERS | 4 + .../tracing/PerfettoTransitionTracer.java | 223 + .../transition/tracing/TransitionTracer.java | 51 + .../unfold/ShellUnfoldProgressProvider.java | 52 + .../unfold/UnfoldAnimationController.java | 240 ++ .../unfold/UnfoldBackgroundController.java | 111 + .../shell/unfold/UnfoldTransitionHandler.java | 319 ++ .../FullscreenUnfoldTaskAnimator.java | 247 ++ .../animation/SplitTaskUnfoldAnimator.java | 380 ++ .../unfold/animation/UnfoldTaskAnimator.java | 117 + .../qualifier/UnfoldShellTransition.java | 29 + .../unfold/qualifier/UnfoldTransition.java | 30 + .../wm/shell/util/GroupedRecentTaskInfo.aidl | 19 + .../wm/shell/util/GroupedRecentTaskInfo.java | 200 + .../com/android/wm/shell/util/KtProtoLog.kt | 74 + .../android/wm/shell/util/SplitBounds.java | 153 + .../CaptionWindowDecorViewModel.java | 374 ++ .../windowdecor/CaptionWindowDecoration.java | 321 ++ .../DesktopModeWindowDecorViewModel.java | 1244 ++++++ .../DesktopModeWindowDecoration.java | 1068 +++++ .../wm/shell/windowdecor/DragDetector.java | 132 + .../windowdecor/DragPositioningCallback.java | 65 + .../DragPositioningCallbackUtility.java | 231 + .../windowdecor/DragResizeInputListener.java | 544 +++ .../windowdecor/DragResizeWindowGeometry.java | 529 +++ .../FluidResizeTaskPositioner.java | 222 + .../wm/shell/windowdecor/HandleImageButton.kt | 79 + .../wm/shell/windowdecor/HandleMenu.java | 516 +++ .../shell/windowdecor/HandleMenuAnimator.kt | 405 ++ .../windowdecor/HandleMenuImageButton.kt | 48 + .../shell/windowdecor/MaximizeButtonView.kt | 138 + .../wm/shell/windowdecor/MaximizeMenu.kt | 650 +++ .../windowdecor/MoveToDesktopAnimator.kt | 114 + .../com/android/wm/shell/windowdecor/OWNERS | 1 + .../OnTaskResizeAnimationListener.kt | 41 + .../wm/shell/windowdecor/ResizeVeil.kt | 417 ++ .../wm/shell/windowdecor/TaskDragResizer.java | 29 + .../windowdecor/TaskFocusStateConsumer.java | 21 + .../wm/shell/windowdecor/TaskOperations.java | 113 + .../VeiledResizeTaskPositioner.java | 213 + .../windowdecor/WindowDecorLinearLayout.java | 72 + .../windowdecor/WindowDecorViewModel.java | 112 + .../shell/windowdecor/WindowDecoration.java | 786 ++++ .../AdditionalSystemViewContainer.kt | 66 + .../AdditionalViewContainer.kt | 35 + .../AdditionalViewHostViewContainer.kt | 46 + .../wm/shell/windowdecor/common/ThemeUtils.kt | 94 + .../shell/windowdecor/extension/TaskInfo.kt | 37 + .../viewholder/AppHandleViewHolder.kt | 97 + .../viewholder/AppHeaderViewHolder.kt | 500 +++ .../viewholder/WindowDecorationViewHolder.kt | 40 + wmshell/tests/OWNERS | 17 + wmshell/tests/README.md | 15 + wmshell/tests/flicker/Android.bp | 85 + wmshell/tests/flicker/README.md | 10 + wmshell/tests/flicker/appcompat/Android.bp | 43 + .../flicker/appcompat/AndroidManifest.xml | 77 + .../flicker/appcompat/AndroidTestTemplate.xml | 104 + .../res/xml/network_security_config.xml | 22 + .../shell/flicker/appcompat/BaseAppCompat.kt | 99 + .../shell/flicker/appcompat/LetterboxRule.kt | 99 + .../appcompat/OpenAppInSizeCompatModeTest.kt | 108 + .../appcompat/OpenTransparentActivityTest.kt | 117 + .../QuickSwitchLauncherToLetterboxAppTest.kt | 274 ++ .../RepositionFixedPortraitAppTest.kt | 99 + .../RestartAppInSizeCompatModeTest.kt | 103 + .../RotateImmersiveAppInFullscreenTest.kt | 190 + .../appcompat/TransparentBaseAppCompat.kt | 60 + .../trace_config/trace_config.textproto | 71 + wmshell/tests/flicker/bubble/Android.bp | 36 + .../tests/flicker/bubble/AndroidManifest.xml | 77 + .../flicker/bubble/AndroidTestTemplate.xml | 104 + wmshell/tests/flicker/bubble/OWNERS | 2 + .../res/xml/network_security_config.xml | 22 + .../shell/flicker/bubble/BaseBubbleScreen.kt | 112 + .../ChangeActiveActivityFromBubbleTest.kt | 83 + .../bubble/DragToDismissBubbleScreenTest.kt | 87 + .../OpenActivityFromBubbleOnLocksreenTest.kt | 160 + .../bubble/OpenActivityFromBubbleTest.kt | 67 + .../bubble/SendBubbleNotificationTest.kt | 65 + .../trace_config/trace_config.textproto | 71 + wmshell/tests/flicker/pip/Android.bp | 148 + wmshell/tests/flicker/pip/AndroidManifest.xml | 80 + .../tests/flicker/pip/AndroidTestTemplate.xml | 106 + wmshell/tests/flicker/pip/OWNERS | 2 + .../flicker/pip/csuiteDefaultTemplate.xml | 128 + wmshell/tests/flicker/pip/csuitePlan.xml | 3 + .../pip/res/xml/network_security_config.xml | 22 + .../flicker/pip/AutoEnterPipOnGoToHomeTest.kt | 111 + .../pip/AutoEnterPipWithSourceRectHintTest.kt | 84 + .../flicker/pip/ClosePipBySwipingDownTest.kt | 106 + .../pip/ClosePipWithDismissButtonTest.kt | 69 + .../pip/EnterPipOnUserLeaveHintTest.kt | 149 + .../flicker/pip/EnterPipToOtherOrientation.kt | 242 ++ .../flicker/pip/EnterPipViaAppUiButtonTest.kt | 56 + .../pip/ExitPipToAppViaExpandButtonTest.kt | 68 + .../flicker/pip/ExitPipToAppViaIntentTest.kt | 66 + .../flicker/pip/ExpandPipOnDoubleClickTest.kt | 154 + .../flicker/pip/ExpandPipOnPinchOpenTest.kt | 67 + ...omSplitScreenAutoEnterPipOnGoToHomeTest.kt | 159 + ...mSplitScreenEnterPipOnUserLeaveHintTest.kt | 206 + .../pip/MovePipDownOnShelfHeightChange.kt | 71 + .../pip/MovePipOnImeVisibilityChangeTest.kt | 91 + .../pip/MovePipUpOnShelfHeightChangeTest.kt | 72 + .../flicker/pip/PipAspectRatioChangeTest.kt | 62 + .../wm/shell/flicker/pip/PipDragTest.kt | 92 + .../shell/flicker/pip/PipDragThenSnapTest.kt | 198 + .../wm/shell/flicker/pip/PipPinchInTest.kt | 82 + .../pip/SetRequestedOrientationWhilePinned.kt | 159 + .../flicker/pip/ShowPipAndRotateDisplay.kt | 166 + .../pip/apps/AppsEnterPipTransition.kt | 255 ++ .../flicker/pip/apps/MapsEnterPipTest.kt | 157 + .../flicker/pip/apps/NetflixEnterPipTest.kt | 178 + .../flicker/pip/apps/YouTubeEnterPipTest.kt | 94 + .../YouTubeEnterPipToOtherOrientationTest.kt | 163 + .../flicker/pip/common/ClosePipTransition.kt | 87 + .../flicker/pip/common/EnterPipTransition.kt | 137 + .../pip/common/ExitPipToAppTransition.kt | 143 + .../common/MovePipShelfHeightTransition.kt | 124 + .../shell/flicker/pip/common/PipTransition.kt | 108 + .../wm/shell/flicker/pip/tv/PipAppHelperTv.kt | 79 + .../wm/shell/flicker/pip/tv/PipTestBase.kt | 56 + .../wm/shell/flicker/pip/tv/TvPipBasicTest.kt | 83 + .../wm/shell/flicker/pip/tv/TvPipMenuTests.kt | 285 ++ .../flicker/pip/tv/TvPipNotificationTests.kt | 185 + .../wm/shell/flicker/pip/tv/TvPipTestBase.kt | 101 + .../wm/shell/flicker/pip/tv/TvUtils.kt | 161 + .../pip/trace_config/trace_config.textproto | 71 + wmshell/tests/flicker/service/Android.bp | 75 + .../tests/flicker/service/AndroidManifest.xml | 77 + .../flicker/service/AndroidTestTemplate.xml | 104 + .../res/xml/network_security_config.xml | 22 + .../wm/shell/flicker/service/common/Utils.kt | 53 + .../shell/flicker/service/desktopmode/OWNERS | 5 + .../CloseAllAppWithAppHeaderExitLandscape.kt | 47 + .../CloseAllAppWithAppHeaderExitPortrait.kt | 47 + .../flicker/DesktopModeFlickerScenarios.kt | 144 + .../flicker/EnterDesktopWithDragLandscape.kt | 44 + .../flicker/EnterDesktopWithDragPortrait.kt | 43 + .../ResizeAppWithCornerResizeLandscape.kt | 43 + .../ResizeAppWithCornerResizePortrait.kt | 43 + .../CloseAllAppsWithAppHeaderExit.kt | 77 + .../scenarios/EnterDesktopWithDrag.kt | 67 + .../scenarios/ResizeAppWithCornerResize.kt | 68 + .../shell/flicker/service/splitscreen/OWNERS | 2 + .../CopyContentInSplitGesturalNavLandscape.kt | 40 + .../CopyContentInSplitGesturalNavPortrait.kt | 40 + ...plitScreenByDividerGesturalNavLandscape.kt | 45 + ...SplitScreenByDividerGesturalNavPortrait.kt | 45 + ...SplitScreenByGoHomeGesturalNavLandscape.kt | 44 + ...sSplitScreenByGoHomeGesturalNavPortrait.kt | 44 + ...DragDividerToResizeGesturalNavLandscape.kt | 43 + .../DragDividerToResizeGesturalNavPortrait.kt | 43 + ...enByDragFromAllAppsGesturalNavLandscape.kt | 44 + ...eenByDragFromAllAppsGesturalNavPortrait.kt | 44 + ...ragFromNotificationGesturalNavLandscape.kt | 45 + ...DragFromNotificationGesturalNavPortrait.kt | 45 + ...nByDragFromShortcutGesturalNavLandscape.kt | 44 + ...enByDragFromShortcutGesturalNavPortrait.kt | 44 + ...enByDragFromTaskbarGesturalNavLandscape.kt | 44 + ...eenByDragFromTaskbarGesturalNavPortrait.kt | 44 + ...tScreenFromOverviewGesturalNavLandscape.kt | 44 + ...itScreenFromOverviewGesturalNavPortrait.kt | 44 + ...pByDoubleTapDividerGesturalNavLandscape.kt | 44 + ...ppByDoubleTapDividerGesturalNavPortrait.kt | 44 + ...SplitFromAnotherAppGesturalNavLandscape.kt | 44 + ...oSplitFromAnotherAppGesturalNavPortrait.kt | 44 + ...BackToSplitFromHomeGesturalNavLandscape.kt | 44 + ...hBackToSplitFromHomeGesturalNavPortrait.kt | 44 + ...ckToSplitFromRecentGesturalNavLandscape.kt | 44 + ...ackToSplitFromRecentGesturalNavPortrait.kt | 44 + ...chBetweenSplitPairsGesturalNavLandscape.kt | 43 + ...tchBetweenSplitPairsGesturalNavPortrait.kt | 43 + ...yguardToSplitScreenGesturalNavLandscape.kt | 42 + ...eyguardToSplitScreenGesturalNavPortrait.kt | 42 + .../CopyContentInSplitGesturalNavLandscape.kt | 30 + .../CopyContentInSplitGesturalNavPortrait.kt | 30 + ...plitScreenByDividerGesturalNavLandscape.kt | 31 + ...SplitScreenByDividerGesturalNavPortrait.kt | 31 + ...SplitScreenByGoHomeGesturalNavLandscape.kt | 31 + ...sSplitScreenByGoHomeGesturalNavPortrait.kt | 31 + ...DragDividerToResizeGesturalNavLandscape.kt | 30 + .../DragDividerToResizeGesturalNavPortrait.kt | 30 + ...enByDragFromAllAppsGesturalNavLandscape.kt | 31 + ...eenByDragFromAllAppsGesturalNavPortrait.kt | 31 + ...ragFromNotificationGesturalNavLandscape.kt | 32 + ...DragFromNotificationGesturalNavPortrait.kt | 32 + ...nByDragFromShortcutGesturalNavLandscape.kt | 31 + ...enByDragFromShortcutGesturalNavPortrait.kt | 31 + ...enByDragFromTaskbarGesturalNavLandscape.kt | 31 + ...eenByDragFromTaskbarGesturalNavPortrait.kt | 31 + ...tScreenFromOverviewGesturalNavLandscape.kt | 31 + ...itScreenFromOverviewGesturalNavPortrait.kt | 31 + ...pByDoubleTapDividerGesturalNavLandscape.kt | 31 + ...ppByDoubleTapDividerGesturalNavPortrait.kt | 31 + ...SplitFromAnotherAppGesturalNavLandscape.kt | 31 + ...oSplitFromAnotherAppGesturalNavPortrait.kt | 31 + ...BackToSplitFromHomeGesturalNavLandscape.kt | 31 + ...hBackToSplitFromHomeGesturalNavPortrait.kt | 31 + ...ckToSplitFromRecentGesturalNavLandscape.kt | 31 + ...ackToSplitFromRecentGesturalNavPortrait.kt | 31 + ...chBetweenSplitPairsGesturalNavLandscape.kt | 31 + ...tchBetweenSplitPairsGesturalNavPortrait.kt | 31 + ...yguardToSplitScreenGesturalNavLandscape.kt | 32 + ...eyguardToSplitScreenGesturalNavPortrait.kt | 32 + .../scenarios/CopyContentInSplit.kt | 67 + .../scenarios/DismissSplitScreenByDivider.kt | 80 + .../scenarios/DismissSplitScreenByGoHome.kt | 66 + .../scenarios/DragDividerToResize.kt | 65 + .../EnterSplitScreenByDragFromAllApps.kt | 80 + .../EnterSplitScreenByDragFromNotification.kt | 88 + .../EnterSplitScreenByDragFromShortcut.kt | 89 + .../EnterSplitScreenByDragFromTaskbar.kt | 77 + .../scenarios/EnterSplitScreenFromOverview.kt | 73 + .../scenarios/SwitchAppByDoubleTapDivider.kt | 152 + .../SwitchBackToSplitFromAnotherApp.kt | 70 + .../scenarios/SwitchBackToSplitFromHome.kt | 69 + .../scenarios/SwitchBackToSplitFromRecent.kt | 71 + .../scenarios/SwitchBetweenSplitPairs.kt | 72 + .../scenarios/UnlockKeyguardToSplitScreen.kt | 69 + .../trace_config/trace_config.textproto | 71 + wmshell/tests/flicker/splitscreen/Android.bp | 82 + .../flicker/splitscreen/AndroidManifest.xml | 77 + .../splitscreen/AndroidTestTemplate.xml | 106 + wmshell/tests/flicker/splitscreen/OWNERS | 2 + .../res/xml/network_security_config.xml | 22 + .../flicker/splitscreen/CopyContentInSplit.kt | 121 + .../DismissSplitScreenByDivider.kt | 153 + .../splitscreen/DismissSplitScreenByGoHome.kt | 159 + .../splitscreen/DragDividerToResize.kt | 120 + .../EnterSplitScreenByDragFromAllApps.kt | 169 + .../EnterSplitScreenByDragFromNotification.kt | 171 + .../EnterSplitScreenByDragFromShortcut.kt | 114 + .../EnterSplitScreenByDragFromTaskbar.kt | 171 + .../EnterSplitScreenFromOverview.kt | 111 + .../MultipleShowImeRequestsInSplitScreen.kt | 67 + .../SwitchAppByDoubleTapDivider.kt | 112 + .../SwitchBackToSplitFromAnotherApp.kt | 158 + .../splitscreen/SwitchBackToSplitFromHome.kt | 158 + .../SwitchBackToSplitFromRecent.kt | 158 + .../splitscreen/SwitchBetweenSplitPairs.kt | 207 + .../SwitchBetweenSplitPairsNoPip.kt | 159 + .../UnlockKeyguardToSplitScreen.kt | 130 + .../benchmark/CopyContentInSplitBenchmark.kt | 67 + .../DismissSplitScreenByDividerBenchmark.kt | 73 + .../DismissSplitScreenByGoHomeBenchmark.kt | 59 + .../benchmark/DragDividerToResizeBenchmark.kt | 63 + ...erSplitScreenByDragFromAllAppsBenchmark.kt | 77 + ...itScreenByDragFromNotificationBenchmark.kt | 71 + ...rSplitScreenByDragFromShortcutBenchmark.kt | 78 + ...erSplitScreenByDragFromTaskbarBenchmark.kt | 75 + .../EnterSplitScreenFromOverviewBenchmark.kt | 59 + ...leShowImeRequestsInSplitScreenBenchmark.kt | 75 + .../splitscreen/benchmark/SplitScreenBase.kt | 49 + .../SwitchAppByDoubleTapDividerBenchmark.kt | 143 + ...witchBackToSplitFromAnotherAppBenchmark.kt | 69 + .../SwitchBackToSplitFromHomeBenchmark.kt | 67 + .../SwitchBackToSplitFromRecentBenchmark.kt | 67 + .../SwitchBetweenSplitPairsBenchmark.kt | 82 + .../UnlockKeyguardToSplitScreenBenchmark.kt | 57 + .../trace_config/trace_config.textproto | 72 + .../wm/shell/flicker/BaseBenchmarkTest.kt | 47 + .../com/android/wm/shell/flicker/BaseTest.kt | 37 + .../shell/flicker/utils/CommonAssertions.kt | 456 ++ .../wm/shell/flicker/utils/CommonConstants.kt | 35 + .../shell/flicker/utils/ICommonAssertions.kt | 136 + .../shell/flicker/utils/MultiWindowUtils.kt | 61 + .../flicker/utils/NotificationListener.kt | 101 + .../shell/flicker/utils/SplitScreenUtils.kt | 395 ++ .../wm/shell/flicker/utils/WaitUtils.kt | 47 + wmshell/tests/unittest/Android.bp | 91 + wmshell/tests/unittest/AndroidManifest.xml | 43 + wmshell/tests/unittest/AndroidTest.xml | 31 + wmshell/tests/unittest/res/layout/main.xml | 32 + wmshell/tests/unittest/res/values/config.xml | 21 + wmshell/tests/unittest/res/values/dimen.xml | 29 + .../wm/shell/MockSurfaceControlHelper.java | 55 + .../src/com/android/wm/shell/MockToken.java | 40 + .../com/android/wm/shell/ShellInitTest.java | 93 + .../wm/shell/ShellTaskOrganizerTests.java | 697 +++ .../com/android/wm/shell/ShellTestCase.java | 89 + .../src/com/android/wm/shell/TestHandler.java | 37 + .../wm/shell/TestRunningTaskInfoBuilder.java | 140 + .../android/wm/shell/TestShellExecutor.java | 55 + ...ActivityEmbeddingAnimationRunnerTests.java | 164 + .../ActivityEmbeddingAnimationTestBase.java | 110 + .../ActivityEmbeddingControllerTests.java | 306 ++ .../back/BackAnimationControllerTest.java | 697 +++ .../shell/back/BackProgressAnimatorTest.java | 186 + .../CustomCrossActivityBackAnimationTest.kt | 261 ++ .../src/com/android/wm/shell/back/OWNERS | 5 + .../shell/bubbles/BubbleDataRepositoryTest.kt | 201 + .../wm/shell/bubbles/BubbleDataTest.java | 1433 +++++++ .../shell/bubbles/BubbleFlyoutViewTest.java | 113 + .../wm/shell/bubbles/BubbleOverflowTest.java | 94 + .../android/wm/shell/bubbles/BubbleTest.java | 220 + .../wm/shell/bubbles/BubbleViewInfoTest.kt | 262 ++ .../BubblesNavBarMotionEventHandlerTest.java | 140 + .../wm/shell/bubbles/BubblesTestActivity.java | 40 + .../BubblesTransitionObserverTest.java | 234 + .../bubbles/TestableBubblePositioner.java | 48 + .../ExpandedAnimationControllerTest.java | 205 + .../ExpandedViewAnimationControllerTest.java | 178 + .../animation/PhysicsAnimationLayoutTest.java | 515 +++ .../PhysicsAnimationLayoutTestCase.java | 339 ++ .../StackAnimationControllerTest.java | 337 ++ .../bubbles/bar/BubbleBarHandleViewTest.java | 67 + .../storage/BubblePersistentRepositoryTest.kt | 82 + .../storage/BubbleVolatileRepositoryTest.kt | 273 ++ .../bubbles/storage/BubbleXmlHelperTest.kt | 194 + .../common/DevicePostureControllerTest.java | 109 + .../common/DisplayChangeControllerTests.java | 64 + .../shell/common/DisplayControllerTests.java | 65 + .../common/DisplayImeControllerTest.java | 160 + .../common/DisplayInsetsControllerTest.java | 215 + .../wm/shell/common/DisplayLayoutTest.java | 166 + .../common/LaunchAdjacentControllerTest.kt | 172 + .../shell/common/MultiInstanceHelperTest.kt | 194 + .../common/TabletopModeControllerTest.java | 270 ++ .../common/TaskStackListenerImplTest.java | 253 ++ .../common/bubbles/BubbleBarLocationTest.kt | 53 + .../wm/shell/common/bubbles/BubbleInfoTest.kt | 63 + .../magnetictarget/MagnetizedObjectTest.kt | 526 +++ .../shell/common/split/DividerViewTest.java | 123 + .../shell/common/split/SplitLayoutTests.java | 211 + .../common/split/SplitScreenConstantsTest.kt | 59 + .../common/split/SplitWindowManagerTests.java | 67 + .../wm/shell/compatui/AppCompatUtilsTest.kt | 59 + .../compatui/CompatUIControllerTest.java | 713 +++ .../wm/shell/compatui/CompatUILayoutTest.java | 232 + .../compatui/CompatUIWindowManagerTest.java | 561 +++ .../LetterboxEduDialogLayoutTest.java | 109 + .../LetterboxEduWindowManagerTest.java | 484 +++ .../compatui/ReachabilityEduLayoutTest.java | 119 + .../ReachabilityEduWindowManagerTest.java | 114 + .../compatui/RestartDialogLayoutTest.java | 148 + .../RestartDialogWindowManagerTest.java | 97 + .../UserAspectRatioSettingsLayoutTest.java | 156 + ...rAspectRatioSettingsWindowManagerTest.java | 470 ++ .../desktopmode/DesktopModeEventLoggerTest.kt | 169 + ...DesktopModeLoggerTransitionObserverTest.kt | 637 +++ .../DesktopModeTaskRepositoryTest.kt | 549 +++ .../DesktopModeTransitionTypesTest.kt | 73 + .../DesktopModeUiEventLoggerTest.kt | 111 + .../DesktopModeVisualIndicatorTest.kt | 144 + .../desktopmode/DesktopTasksControllerTest.kt | 1870 ++++++++ .../desktopmode/DesktopTasksLimiterTest.kt | 320 ++ .../shell/desktopmode/DesktopTestHelpers.kt | 88 + .../DragToDesktopTransitionHandlerTest.kt | 361 ++ .../ExitDesktopTaskTransitionHandlerTest.java | 154 + .../DragAndDropControllerTest.java | 162 + .../draganddrop/DragAndDropPolicyTest.java | 386 ++ .../draganddrop/GlobalDragListenerTest.kt | 127 + .../freeform/FreeformTaskListenerTests.java | 114 + .../FreeformTaskTransitionObserverTest.java | 238 ++ .../HideDisplayCutoutControllerTest.java | 104 + .../HideDisplayCutoutOrganizerTest.java | 238 ++ .../BackgroundWindowManagerTest.java | 61 + .../OneHandedAnimationControllerTest.java | 68 + .../onehanded/OneHandedControllerTest.java | 572 +++ .../OneHandedDisplayAreaOrganizerTest.java | 443 ++ .../onehanded/OneHandedSettingsUtilTest.java | 57 + .../shell/onehanded/OneHandedStateTest.java | 217 + .../wm/shell/onehanded/OneHandedTestCase.java | 77 + .../OneHandedTimeoutHandlerTest.java | 102 + .../onehanded/OneHandedTouchHandlerTest.java | 80 + .../OneHandedTutorialHandlerTest.java | 140 + .../onehanded/OneHandedUiEventLoggerTest.java | 106 + .../shell/pip/PipAnimationControllerTest.java | 207 + .../wm/shell/pip/PipBoundsAlgorithmTest.java | 512 +++ .../wm/shell/pip/PipBoundsStateTest.java | 237 + .../wm/shell/pip/PipSnapAlgorithmTest.java | 259 ++ .../wm/shell/pip/PipTaskOrganizerTest.java | 324 ++ .../phone/PhonePipKeepClearAlgorithmTest.java | 304 ++ .../pip/phone/PhoneSizeSpecSourceTest.java | 260 ++ .../wm/shell/pip/phone/PipControllerTest.java | 322 ++ .../pip/phone/PipDoubleTapHelperTest.java | 173 + .../phone/PipResizeGestureHandlerTest.java | 299 ++ .../shell/pip/phone/PipTouchHandlerTest.java | 180 + .../wm/shell/pip/phone/PipTouchStateTest.java | 148 + .../src/com/android/wm/shell/pip/tv/OWNERS | 3 + .../shell/pip/tv/TvPipActionProviderTest.java | 432 ++ .../shell/pip/tv/TvPipBoundsControllerTest.kt | 281 ++ .../wm/shell/pip/tv/TvPipGravityTest.java | 339 ++ .../pip/tv/TvPipKeepClearAlgorithmTest.kt | 603 +++ .../shell/pip/tv/TvPipMenuControllerTest.java | 443 ++ .../wm/shell/pip2/PipTransitionStateTest.java | 115 + .../recents/GroupedRecentTaskInfoTest.kt | 171 + .../recents/RecentTasksControllerTest.java | 684 +++ .../wm/shell/recents/SplitBoundsTest.java | 97 + .../TaskStackTransitionObserverTest.kt | 217 + .../shared/animation/PhysicsAnimatorTest.kt | 627 +++ .../wm/shell/splitscreen/MainStageTests.java | 78 + .../wm/shell/splitscreen/SideStageTests.java | 93 + .../SplitScreenControllerTests.java | 321 ++ .../splitscreen/SplitScreenUtilsTests.java | 75 + .../wm/shell/splitscreen/SplitTestUtils.java | 95 + .../splitscreen/SplitTransitionTests.java | 537 +++ .../splitscreen/StageCoordinatorTests.java | 475 ++ .../splitscreen/StageTaskListenerTests.java | 180 + .../StartingSurfaceDrawerTests.java | 375 ++ .../StartingWindowControllerTests.java | 118 + .../wm/shell/sysui/ShellControllerTest.java | 524 +++ .../wm/shell/taskview/TaskViewTest.java | 716 ++++ .../taskview/TaskViewTransitionsTest.java | 282 ++ .../HomeTransitionObserverTest.java | 220 + .../transition/ShellTransitionTests.java | 1781 ++++++++ .../transition/TestRemoteTransition.java | 68 + .../transition/TransitionInfoBuilder.java | 102 + .../unfold/UnfoldAnimationControllerTest.java | 371 ++ .../unfold/UnfoldTransitionHandlerTest.java | 466 ++ .../wm/shell/util/StubTransaction.java | 313 ++ .../DesktopModeWindowDecorViewModelTests.kt | 597 +++ .../DesktopModeWindowDecorationTests.java | 544 +++ .../wm/shell/windowdecor/DragDetectorTest.kt | 210 + .../DragPositioningCallbackUtilityTest.kt | 362 ++ .../DragResizeWindowGeometryTests.java | 390 ++ .../FluidResizeTaskPositionerTest.kt | 838 ++++ .../wm/shell/windowdecor/HandleMenuTest.kt | 212 + .../wm/shell/windowdecor/ResizeVeilTest.kt | 228 + .../VeiledResizeTaskPositionerTest.kt | 533 +++ .../windowdecor/WindowDecorationTests.java | 947 ++++ .../AdditionalSystemViewContainerTest.kt | 90 + .../AdditionalViewHostViewContainerTest.kt | 68 + 1419 files changed, 238037 insertions(+), 44 deletions(-) create mode 100644 flags/src/com/android/systemui/CustomFeatureFlags.java create mode 100644 flags/src/com/android/systemui/FakeFeatureFlagsImpl.java create mode 100644 flags/src/com/android/systemui/FeatureFlags.java create mode 100644 flags/src/com/android/systemui/FeatureFlagsImpl.java create mode 100644 flags/src/com/android/systemui/Flags.java create mode 100644 flags/src/com/android/systemui/shared/CustomFeatureFlags.java create mode 100644 flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java create mode 100644 flags/src/com/android/systemui/shared/FeatureFlags.java create mode 100644 flags/src/com/android/systemui/shared/FeatureFlagsImpl.java create mode 100644 flags/src/com/android/systemui/shared/Flags.java create mode 100644 systemUIAnim/Android.bp create mode 100644 systemUIAnim/AndroidManifest.xml create mode 100644 systemUIAnim/build.gradle create mode 100644 systemUIAnim/res/anim/launch_dialog_enter.xml create mode 100644 systemUIAnim/res/anim/launch_dialog_exit.xml create mode 100644 systemUIAnim/res/values/ids.xml create mode 100644 systemUIAnim/res/values/styles.xml create mode 100644 systemUIAnim/src/com/android/systemui/animation/ActivityTransitionAnimator.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/AnimationFeatureFlags.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/DelegateTransitionAnimatorController.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/DialogTransitionAnimator.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/Expandable.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/FontInterpolator.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/FontVariationUtils.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/LaunchableView.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/RemoteAnimationDelegate.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java create mode 100644 systemUIAnim/src/com/android/systemui/animation/RemoteAnimationTargetCompat.java create mode 100644 systemUIAnim/src/com/android/systemui/animation/ShadeInterpolation.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/TextAnimator.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/TextInterpolator.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/TransitionAnimator.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/ViewDialogTransitionAnimatorController.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/ViewHierarchyAnimator.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/ViewRootSync.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpec.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/back/BackTransformation.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/view/LaunchableImageView.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt create mode 100644 systemUIAnim/src/com/android/systemui/animation/view/LaunchableTextView.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/PaintDrawCallback.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/RenderEffectDrawCallback.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxConfig.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffect.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxShader.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectView.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt create mode 100644 systemUIAnim/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt create mode 100644 systemUIAnim/src/com/android/systemui/util/AnimatorExtensions.kt create mode 100644 systemUIAnim/src/com/android/systemui/util/Dialog.kt create mode 100644 systemUIAnim/src/com/android/systemui/util/Dimension.kt create mode 100644 systemUICommon/.gitignore create mode 100644 systemUICommon/Android.bp create mode 100644 systemUICommon/AndroidManifest.xml create mode 100644 systemUICommon/OWNERS create mode 100644 systemUICommon/README.md create mode 100644 systemUICommon/build.gradle create mode 100644 systemUICommon/src/com/android/systemui/common/buffer/RingBuffer.kt create mode 100644 systemUILog/.gitignore create mode 100644 systemUILog/Android.bp create mode 100644 systemUILog/AndroidManifest.xml create mode 100644 systemUILog/build.gradle create mode 100644 systemUILog/src/com/android/systemui/log/ConstantStringsLogger.kt create mode 100644 systemUILog/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt create mode 100644 systemUILog/src/com/android/systemui/log/LogBuffer.kt create mode 100644 systemUILog/src/com/android/systemui/log/LogMessageImpl.kt create mode 100644 systemUILog/src/com/android/systemui/log/LogcatEchoTracker.kt create mode 100644 systemUILog/src/com/android/systemui/log/core/LogLevel.kt create mode 100644 systemUILog/src/com/android/systemui/log/core/LogMessage.kt create mode 100644 systemUILog/src/com/android/systemui/log/core/LogcatOnlyMessageBuffer.kt create mode 100644 systemUILog/src/com/android/systemui/log/core/Logger.kt create mode 100644 systemUILog/src/com/android/systemui/log/core/MessageBuffer.kt create mode 100644 systemUIPlugin/Android.bp create mode 100644 systemUIPlugin/AndroidManifest.xml create mode 100644 systemUIPlugin/ExamplePlugin/Android.bp create mode 100644 systemUIPlugin/ExamplePlugin/AndroidManifest.xml create mode 100644 systemUIPlugin/ExamplePlugin/res/layout/colored_overlay.xml create mode 100644 systemUIPlugin/ExamplePlugin/res/layout/plugin_settings.xml create mode 100644 systemUIPlugin/ExamplePlugin/res/values/strings.xml create mode 100644 systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java create mode 100644 systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/PluginSettings.java create mode 100644 systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java create mode 100644 systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt create mode 100644 systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java create mode 100644 systemUIPlugin/build.gradle create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/ActivityStarter.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/DarkIconDispatcher.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/DozeServicePlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/FalsingManager.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/FalsingPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/FragmentBase.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/GlobalActions.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/GlobalActionsPanelPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/IntentButtonProvider.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/NotificationListenerController.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/NotificationPersonExtractorPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/OverlayPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/PluginDependency.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/PluginUtils.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/SensorManagerPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/ToastPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/ViewProvider.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/VolumeDialog.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/VolumeDialogController.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/clocks/AlarmData.kt create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/clocks/WeatherData.kt create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/clocks/ZenData.kt create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/qs/QS.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/qs/QSContainerController.kt create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/qs/QSFactory.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/qs/QSIconView.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/qs/QSTile.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/qs/QSTileView.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/statusbar/DozeParameters.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java create mode 100644 systemUIPlugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java create mode 100644 systemUIPlugin/update_plugin_lib.sh create mode 100644 systemUIPluginCore/Android.bp create mode 100644 systemUIPluginCore/AndroidManifest.xml create mode 100644 systemUIPluginCore/build.gradle create mode 100644 systemUIPluginCore/proguard.flags create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/Plugin.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/PluginFragment.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/PluginLifecycleManager.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/PluginListener.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/PluginManager.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/annotations/Dependencies.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/annotations/DependsOn.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/annotations/ProvidesInterface.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requirements.java create mode 100644 systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requires.java create mode 100644 systemUIViewCapture/.gitignore create mode 100644 systemUIViewCapture/Android.bp create mode 100644 systemUIViewCapture/AndroidManifest.xml create mode 100644 systemUIViewCapture/OWNERS create mode 100644 systemUIViewCapture/README.md create mode 100644 systemUIViewCapture/TEST_MAPPING create mode 100644 systemUIViewCapture/build.gradle create mode 100644 systemUIViewCapture/src/com/android/app/viewcapture/LooperExecutor.java create mode 100644 systemUIViewCapture/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt create mode 100644 systemUIViewCapture/src/com/android/app/viewcapture/SimpleViewCapture.kt create mode 100644 systemUIViewCapture/src/com/android/app/viewcapture/ViewCapture.java create mode 100644 systemUIViewCapture/src/com/android/app/viewcapture/proto/view_capture.proto create mode 100644 systemUIViewCapture/tests/AndroidManifest.xml create mode 100644 systemUIViewCapture/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt create mode 100644 systemUIViewCapture/tests/com/android/app/viewcapture/TestActivity.kt create mode 100644 systemUIViewCapture/tests/com/android/app/viewcapture/ViewCaptureTest.kt create mode 100644 wmshell/Android.bp create mode 100644 wmshell/AndroidManifest.xml create mode 100644 wmshell/OWNERS create mode 100644 wmshell/aconfig/Android.bp create mode 100644 wmshell/aconfig/OWNERS create mode 100644 wmshell/aconfig/multitasking.aconfig create mode 100644 wmshell/build.gradle create mode 100644 wmshell/multivalentTests/Android.bp create mode 100644 wmshell/multivalentTests/AndroidManifest.xml create mode 100644 wmshell/multivalentTests/AndroidManifestRobolectric.xml create mode 100644 wmshell/multivalentTests/AndroidTest.xml create mode 100644 wmshell/multivalentTests/OWNERS create mode 100644 wmshell/multivalentTests/robolectric/config/robolectric.properties create mode 100644 wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt create mode 100644 wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt create mode 100644 wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt create mode 100644 wmshell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt create mode 100644 wmshell/multivalentTestsForDevice/Android.bp create mode 100644 wmshell/multivalentTestsForDevice/AndroidManifest.xml create mode 100644 wmshell/multivalentTestsForDevice/AndroidManifestRobolectric.xml create mode 100644 wmshell/multivalentTestsForDevice/AndroidTest.xml create mode 100644 wmshell/multivalentTestsForDevice/OWNERS create mode 100644 wmshell/multivalentTestsForDevice/robolectric/config/robolectric.properties create mode 100644 wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt create mode 100644 wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt create mode 100644 wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt create mode 100644 wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt create mode 100644 wmshell/multivalentTestsForDeviceless/Android.bp create mode 100644 wmshell/multivalentTestsForDeviceless/AndroidManifest.xml create mode 100644 wmshell/multivalentTestsForDeviceless/AndroidManifestRobolectric.xml create mode 100644 wmshell/multivalentTestsForDeviceless/AndroidTest.xml create mode 100644 wmshell/multivalentTestsForDeviceless/OWNERS create mode 100644 wmshell/multivalentTestsForDeviceless/robolectric/config/robolectric.properties create mode 100644 wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt create mode 100644 wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt create mode 100644 wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt create mode 100644 wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt create mode 100644 wmshell/proto/wm_shell_trace.proto create mode 100644 wmshell/proto/wm_shell_transition_trace.proto create mode 100644 wmshell/res/anim/forced_resizable_enter.xml create mode 100644 wmshell/res/anim/forced_resizable_exit.xml create mode 100644 wmshell/res/animator/tv_window_menu_action_button_animator.xml create mode 100644 wmshell/res/color/bubble_drop_target_background_color.xml create mode 100644 wmshell/res/color/compat_background_ripple.xml create mode 100644 wmshell/res/color/decor_button_dark_color.xml create mode 100644 wmshell/res/color/decor_button_light_color.xml create mode 100644 wmshell/res/color/decor_title_color.xml create mode 100644 wmshell/res/color/desktop_mode_caption_button_color_selector_dark.xml create mode 100644 wmshell/res/color/desktop_mode_caption_button_color_selector_light.xml create mode 100644 wmshell/res/color/desktop_mode_maximize_menu_button_color_selector.xml create mode 100644 wmshell/res/color/letterbox_education_dismiss_button_background_ripple.xml create mode 100644 wmshell/res/color/letterbox_restart_button_background_ripple.xml create mode 100644 wmshell/res/color/letterbox_restart_dismiss_button_background_ripple.xml create mode 100644 wmshell/res/color/one_handed_tutorial_background_color.xml create mode 100644 wmshell/res/color/taskbar_background_dark.xml create mode 100644 wmshell/res/color/tv_window_menu_close_icon.xml create mode 100644 wmshell/res/color/tv_window_menu_close_icon_bg.xml create mode 100644 wmshell/res/color/tv_window_menu_icon.xml create mode 100644 wmshell/res/color/tv_window_menu_icon_bg.xml create mode 100644 wmshell/res/color/unfold_background.xml create mode 100644 wmshell/res/drawable-night/reachability_education_ic_left_hand.xml create mode 100644 wmshell/res/drawable-night/reachability_education_ic_right_hand.xml create mode 100644 wmshell/res/drawable/bubble_drop_target_background.xml create mode 100644 wmshell/res/drawable/bubble_ic_create_bubble.xml create mode 100644 wmshell/res/drawable/bubble_ic_empty_overflow_dark.xml create mode 100644 wmshell/res/drawable/bubble_ic_empty_overflow_light.xml create mode 100644 wmshell/res/drawable/bubble_ic_overflow_button.xml create mode 100644 wmshell/res/drawable/bubble_ic_stop_bubble.xml create mode 100644 wmshell/res/drawable/bubble_manage_btn_bg.xml create mode 100644 wmshell/res/drawable/bubble_manage_menu_bg.xml create mode 100644 wmshell/res/drawable/bubble_manage_menu_row.xml create mode 100644 wmshell/res/drawable/bubble_manage_menu_section.xml create mode 100644 wmshell/res/drawable/bubble_stack_user_education_bg.xml create mode 100644 wmshell/res/drawable/bubble_stack_user_education_bg_rtl.xml create mode 100644 wmshell/res/drawable/camera_compat_dismiss_button.xml create mode 100644 wmshell/res/drawable/camera_compat_dismiss_ripple.xml create mode 100644 wmshell/res/drawable/camera_compat_treatment_applied_button.xml create mode 100644 wmshell/res/drawable/camera_compat_treatment_applied_ripple.xml create mode 100644 wmshell/res/drawable/camera_compat_treatment_suggested_button.xml create mode 100644 wmshell/res/drawable/camera_compat_treatment_suggested_ripple.xml create mode 100644 wmshell/res/drawable/caption_decor_title.xml create mode 100644 wmshell/res/drawable/circular_progress.xml create mode 100644 wmshell/res/drawable/compat_hint_bubble.xml create mode 100644 wmshell/res/drawable/compat_hint_point.xml create mode 100644 wmshell/res/drawable/decor_back_button_dark.xml create mode 100644 wmshell/res/drawable/decor_close_button_dark.xml create mode 100644 wmshell/res/drawable/decor_desktop_mode_maximize_button_dark.xml create mode 100644 wmshell/res/drawable/decor_handle_dark.xml create mode 100644 wmshell/res/drawable/decor_maximize_button_dark.xml create mode 100644 wmshell/res/drawable/decor_minimize_button_dark.xml create mode 100644 wmshell/res/drawable/desktop_mode_decor_handle_menu_background.xml create mode 100644 wmshell/res/drawable/desktop_mode_header_ic_close.xml create mode 100644 wmshell/res/drawable/desktop_mode_ic_handle_menu_close.xml create mode 100644 wmshell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml create mode 100644 wmshell/res/drawable/desktop_mode_ic_handle_menu_floating.xml create mode 100644 wmshell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml create mode 100644 wmshell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml create mode 100644 wmshell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml create mode 100644 wmshell/res/drawable/desktop_mode_maximize_menu_background.xml create mode 100644 wmshell/res/drawable/desktop_mode_maximize_menu_button_background.xml create mode 100644 wmshell/res/drawable/desktop_mode_maximize_menu_layout_background.xml create mode 100644 wmshell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml create mode 100644 wmshell/res/drawable/desktop_windowing_transition_background.xml create mode 100644 wmshell/res/drawable/dismiss_circle_background.xml create mode 100644 wmshell/res/drawable/floating_dismiss_gradient.xml create mode 100644 wmshell/res/drawable/floating_dismiss_gradient_transition.xml create mode 100644 wmshell/res/drawable/handle_menu_background.xml create mode 100644 wmshell/res/drawable/home_icon.xml create mode 100644 wmshell/res/drawable/ic_baseline_expand_more_24.xml create mode 100644 wmshell/res/drawable/ic_bubbles_shortcut_widget.xml create mode 100644 wmshell/res/drawable/ic_bubbles_shortcut_widget_background.xml create mode 100644 wmshell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml create mode 100644 wmshell/res/drawable/ic_expand_less.xml create mode 100644 wmshell/res/drawable/ic_floating_landscape.xml create mode 100644 wmshell/res/drawable/ic_remove_no_shadow.xml create mode 100644 wmshell/res/drawable/letterbox_education_dialog_background.xml create mode 100644 wmshell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml create mode 100644 wmshell/res/drawable/letterbox_education_ic_light_bulb.xml create mode 100644 wmshell/res/drawable/letterbox_education_ic_reposition.xml create mode 100644 wmshell/res/drawable/letterbox_education_ic_split_screen.xml create mode 100644 wmshell/res/drawable/letterbox_restart_button_background_ripple.xml create mode 100644 wmshell/res/drawable/letterbox_restart_checkbox_button.xml create mode 100644 wmshell/res/drawable/letterbox_restart_checkbox_checked.xml create mode 100644 wmshell/res/drawable/letterbox_restart_checkbox_unchecked.xml create mode 100644 wmshell/res/drawable/letterbox_restart_dialog_background.xml create mode 100644 wmshell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml create mode 100644 wmshell/res/drawable/letterbox_restart_header_ic_arrows.xml create mode 100644 wmshell/res/drawable/letterbox_restart_ic_arrows.xml create mode 100644 wmshell/res/drawable/one_handed_tutorial_icon.xml create mode 100644 wmshell/res/drawable/pip_custom_close_bg.xml create mode 100644 wmshell/res/drawable/pip_expand.xml create mode 100644 wmshell/res/drawable/pip_ic_close_white.xml create mode 100644 wmshell/res/drawable/pip_ic_collapse.xml create mode 100644 wmshell/res/drawable/pip_ic_expand.xml create mode 100644 wmshell/res/drawable/pip_ic_fullscreen_white.xml create mode 100644 wmshell/res/drawable/pip_ic_move_white.xml create mode 100644 wmshell/res/drawable/pip_ic_pause_white.xml create mode 100644 wmshell/res/drawable/pip_ic_play_arrow_white.xml create mode 100644 wmshell/res/drawable/pip_ic_settings.xml create mode 100644 wmshell/res/drawable/pip_ic_skip_next_white.xml create mode 100644 wmshell/res/drawable/pip_ic_skip_previous_white.xml create mode 100644 wmshell/res/drawable/pip_icon.xml create mode 100644 wmshell/res/drawable/pip_menu_background.xml create mode 100644 wmshell/res/drawable/pip_resize_handle.xml create mode 100644 wmshell/res/drawable/reachability_education_ic_left_hand.xml create mode 100644 wmshell/res/drawable/reachability_education_ic_right_hand.xml create mode 100644 wmshell/res/drawable/rounded_button.xml create mode 100644 wmshell/res/drawable/size_compat_restart_button.xml create mode 100644 wmshell/res/drawable/size_compat_restart_button_ripple.xml create mode 100644 wmshell/res/drawable/tv_pip_menu_background.xml create mode 100644 wmshell/res/drawable/tv_pip_menu_border.xml create mode 100644 wmshell/res/drawable/tv_split_menu_ic_focus.xml create mode 100644 wmshell/res/drawable/tv_split_menu_ic_swap.xml create mode 100644 wmshell/res/drawable/tv_window_button_bg.xml create mode 100644 wmshell/res/drawable/user_aspect_ratio_settings_button.xml create mode 100644 wmshell/res/drawable/user_aspect_ratio_settings_button_ripple.xml create mode 100644 wmshell/res/layout/background_panel.xml create mode 100644 wmshell/res/layout/badged_image_view.xml create mode 100644 wmshell/res/layout/bubble_bar_drop_target.xml create mode 100644 wmshell/res/layout/bubble_bar_expanded_view.xml create mode 100644 wmshell/res/layout/bubble_bar_manage_education.xml create mode 100644 wmshell/res/layout/bubble_bar_menu_item.xml create mode 100644 wmshell/res/layout/bubble_bar_menu_view.xml create mode 100644 wmshell/res/layout/bubble_bar_stack_education.xml create mode 100644 wmshell/res/layout/bubble_expanded_view.xml create mode 100644 wmshell/res/layout/bubble_flyout.xml create mode 100644 wmshell/res/layout/bubble_manage_button.xml create mode 100644 wmshell/res/layout/bubble_manage_menu.xml create mode 100644 wmshell/res/layout/bubble_overflow_button.xml create mode 100644 wmshell/res/layout/bubble_overflow_container.xml create mode 100644 wmshell/res/layout/bubble_overflow_view.xml create mode 100644 wmshell/res/layout/bubble_stack_user_education.xml create mode 100644 wmshell/res/layout/bubble_view.xml create mode 100644 wmshell/res/layout/bubbles_manage_button_education.xml create mode 100644 wmshell/res/layout/caption_window_decor.xml create mode 100644 wmshell/res/layout/compat_mode_hint.xml create mode 100644 wmshell/res/layout/compat_ui_layout.xml create mode 100644 wmshell/res/layout/desktop_mode_app_handle.xml create mode 100644 wmshell/res/layout/desktop_mode_app_header.xml create mode 100644 wmshell/res/layout/desktop_mode_resize_veil.xml create mode 100644 wmshell/res/layout/desktop_mode_window_decor_handle_menu.xml create mode 100644 wmshell/res/layout/desktop_mode_window_decor_maximize_menu.xml create mode 100644 wmshell/res/layout/divider.xml create mode 100644 wmshell/res/layout/forced_resizable_activity.xml create mode 100644 wmshell/res/layout/global_drop_target.xml create mode 100644 wmshell/res/layout/letterbox_education_dialog_action_layout.xml create mode 100644 wmshell/res/layout/letterbox_education_dialog_layout.xml create mode 100644 wmshell/res/layout/letterbox_restart_dialog_layout.xml create mode 100644 wmshell/res/layout/maximize_menu_button.xml create mode 100644 wmshell/res/layout/one_handed_tutorial.xml create mode 100644 wmshell/res/layout/pip_menu.xml create mode 100644 wmshell/res/layout/pip_menu_action.xml create mode 100644 wmshell/res/layout/reachability_ui_layout.xml create mode 100644 wmshell/res/layout/split_decor.xml create mode 100644 wmshell/res/layout/split_divider.xml create mode 100644 wmshell/res/layout/split_outline.xml create mode 100644 wmshell/res/layout/tv_pip_menu.xml create mode 100644 wmshell/res/layout/tv_pip_menu_background.xml create mode 100644 wmshell/res/layout/tv_split_menu_view.xml create mode 100644 wmshell/res/layout/tv_window_menu_action_button.xml create mode 100644 wmshell/res/layout/user_aspect_ratio_settings_layout.xml create mode 100644 wmshell/res/values-af/strings.xml create mode 100644 wmshell/res/values-af/strings_tv.xml create mode 100644 wmshell/res/values-am/strings.xml create mode 100644 wmshell/res/values-am/strings_tv.xml create mode 100644 wmshell/res/values-ar/strings.xml create mode 100644 wmshell/res/values-ar/strings_tv.xml create mode 100644 wmshell/res/values-as/strings.xml create mode 100644 wmshell/res/values-as/strings_tv.xml create mode 100644 wmshell/res/values-az/strings.xml create mode 100644 wmshell/res/values-az/strings_tv.xml create mode 100644 wmshell/res/values-b+sr+Latn/strings.xml create mode 100644 wmshell/res/values-b+sr+Latn/strings_tv.xml create mode 100644 wmshell/res/values-be/strings.xml create mode 100644 wmshell/res/values-be/strings_tv.xml create mode 100644 wmshell/res/values-bg/strings.xml create mode 100644 wmshell/res/values-bg/strings_tv.xml create mode 100644 wmshell/res/values-bn/strings.xml create mode 100644 wmshell/res/values-bn/strings_tv.xml create mode 100644 wmshell/res/values-bs/strings.xml create mode 100644 wmshell/res/values-bs/strings_tv.xml create mode 100644 wmshell/res/values-ca/strings.xml create mode 100644 wmshell/res/values-ca/strings_tv.xml create mode 100644 wmshell/res/values-cs/strings.xml create mode 100644 wmshell/res/values-cs/strings_tv.xml create mode 100644 wmshell/res/values-da/strings.xml create mode 100644 wmshell/res/values-da/strings_tv.xml create mode 100644 wmshell/res/values-de/strings.xml create mode 100644 wmshell/res/values-de/strings_tv.xml create mode 100644 wmshell/res/values-el/strings.xml create mode 100644 wmshell/res/values-el/strings_tv.xml create mode 100644 wmshell/res/values-en-rAU/strings.xml create mode 100644 wmshell/res/values-en-rAU/strings_tv.xml create mode 100644 wmshell/res/values-en-rCA/strings.xml create mode 100644 wmshell/res/values-en-rCA/strings_tv.xml create mode 100644 wmshell/res/values-en-rGB/strings.xml create mode 100644 wmshell/res/values-en-rGB/strings_tv.xml create mode 100644 wmshell/res/values-en-rIN/strings.xml create mode 100644 wmshell/res/values-en-rIN/strings_tv.xml create mode 100644 wmshell/res/values-en-rXC/strings.xml create mode 100644 wmshell/res/values-en-rXC/strings_tv.xml create mode 100644 wmshell/res/values-es-rUS/strings.xml create mode 100644 wmshell/res/values-es-rUS/strings_tv.xml create mode 100644 wmshell/res/values-es/strings.xml create mode 100644 wmshell/res/values-es/strings_tv.xml create mode 100644 wmshell/res/values-et/strings.xml create mode 100644 wmshell/res/values-et/strings_tv.xml create mode 100644 wmshell/res/values-eu/strings.xml create mode 100644 wmshell/res/values-eu/strings_tv.xml create mode 100644 wmshell/res/values-fa/strings.xml create mode 100644 wmshell/res/values-fa/strings_tv.xml create mode 100644 wmshell/res/values-fi/strings.xml create mode 100644 wmshell/res/values-fi/strings_tv.xml create mode 100644 wmshell/res/values-fr-rCA/strings.xml create mode 100644 wmshell/res/values-fr-rCA/strings_tv.xml create mode 100644 wmshell/res/values-fr/strings.xml create mode 100644 wmshell/res/values-fr/strings_tv.xml create mode 100644 wmshell/res/values-gl/strings.xml create mode 100644 wmshell/res/values-gl/strings_tv.xml create mode 100644 wmshell/res/values-gu/strings.xml create mode 100644 wmshell/res/values-gu/strings_tv.xml create mode 100644 wmshell/res/values-hi/strings.xml create mode 100644 wmshell/res/values-hi/strings_tv.xml create mode 100644 wmshell/res/values-hr/strings.xml create mode 100644 wmshell/res/values-hr/strings_tv.xml create mode 100644 wmshell/res/values-hu/strings.xml create mode 100644 wmshell/res/values-hu/strings_tv.xml create mode 100644 wmshell/res/values-hy/strings.xml create mode 100644 wmshell/res/values-hy/strings_tv.xml create mode 100644 wmshell/res/values-in/strings.xml create mode 100644 wmshell/res/values-in/strings_tv.xml create mode 100644 wmshell/res/values-is/strings.xml create mode 100644 wmshell/res/values-is/strings_tv.xml create mode 100644 wmshell/res/values-it/strings.xml create mode 100644 wmshell/res/values-it/strings_tv.xml create mode 100644 wmshell/res/values-iw/strings.xml create mode 100644 wmshell/res/values-iw/strings_tv.xml create mode 100644 wmshell/res/values-ja/strings.xml create mode 100644 wmshell/res/values-ja/strings_tv.xml create mode 100644 wmshell/res/values-ka/strings.xml create mode 100644 wmshell/res/values-ka/strings_tv.xml create mode 100644 wmshell/res/values-kk/strings.xml create mode 100644 wmshell/res/values-kk/strings_tv.xml create mode 100644 wmshell/res/values-km/strings.xml create mode 100644 wmshell/res/values-km/strings_tv.xml create mode 100644 wmshell/res/values-kn/strings.xml create mode 100644 wmshell/res/values-kn/strings_tv.xml create mode 100644 wmshell/res/values-ko/strings.xml create mode 100644 wmshell/res/values-ko/strings_tv.xml create mode 100644 wmshell/res/values-ky/strings.xml create mode 100644 wmshell/res/values-ky/strings_tv.xml create mode 100644 wmshell/res/values-land/dimens.xml create mode 100644 wmshell/res/values-lo/strings.xml create mode 100644 wmshell/res/values-lo/strings_tv.xml create mode 100644 wmshell/res/values-lt/strings.xml create mode 100644 wmshell/res/values-lt/strings_tv.xml create mode 100644 wmshell/res/values-lv/strings.xml create mode 100644 wmshell/res/values-lv/strings_tv.xml create mode 100644 wmshell/res/values-mk/strings.xml create mode 100644 wmshell/res/values-mk/strings_tv.xml create mode 100644 wmshell/res/values-ml/strings.xml create mode 100644 wmshell/res/values-ml/strings_tv.xml create mode 100644 wmshell/res/values-mn/strings.xml create mode 100644 wmshell/res/values-mn/strings_tv.xml create mode 100644 wmshell/res/values-mr/strings.xml create mode 100644 wmshell/res/values-mr/strings_tv.xml create mode 100644 wmshell/res/values-ms/strings.xml create mode 100644 wmshell/res/values-ms/strings_tv.xml create mode 100644 wmshell/res/values-my/strings.xml create mode 100644 wmshell/res/values-my/strings_tv.xml create mode 100644 wmshell/res/values-nb/strings.xml create mode 100644 wmshell/res/values-nb/strings_tv.xml create mode 100644 wmshell/res/values-ne/strings.xml create mode 100644 wmshell/res/values-ne/strings_tv.xml create mode 100644 wmshell/res/values-night/colors.xml create mode 100644 wmshell/res/values-night/styles.xml create mode 100644 wmshell/res/values-nl/strings.xml create mode 100644 wmshell/res/values-nl/strings_tv.xml create mode 100644 wmshell/res/values-or/strings.xml create mode 100644 wmshell/res/values-or/strings_tv.xml create mode 100644 wmshell/res/values-pa/strings.xml create mode 100644 wmshell/res/values-pa/strings_tv.xml create mode 100644 wmshell/res/values-pl/strings.xml create mode 100644 wmshell/res/values-pl/strings_tv.xml create mode 100644 wmshell/res/values-pt-rBR/strings.xml create mode 100644 wmshell/res/values-pt-rBR/strings_tv.xml create mode 100644 wmshell/res/values-pt-rPT/strings.xml create mode 100644 wmshell/res/values-pt-rPT/strings_tv.xml create mode 100644 wmshell/res/values-pt/strings.xml create mode 100644 wmshell/res/values-pt/strings_tv.xml create mode 100644 wmshell/res/values-ro/strings.xml create mode 100644 wmshell/res/values-ro/strings_tv.xml create mode 100644 wmshell/res/values-ru/strings.xml create mode 100644 wmshell/res/values-ru/strings_tv.xml create mode 100644 wmshell/res/values-si/strings.xml create mode 100644 wmshell/res/values-si/strings_tv.xml create mode 100644 wmshell/res/values-sk/strings.xml create mode 100644 wmshell/res/values-sk/strings_tv.xml create mode 100644 wmshell/res/values-sl/strings.xml create mode 100644 wmshell/res/values-sl/strings_tv.xml create mode 100644 wmshell/res/values-sq/strings.xml create mode 100644 wmshell/res/values-sq/strings_tv.xml create mode 100644 wmshell/res/values-sr/strings.xml create mode 100644 wmshell/res/values-sr/strings_tv.xml create mode 100644 wmshell/res/values-sv/strings.xml create mode 100644 wmshell/res/values-sv/strings_tv.xml create mode 100644 wmshell/res/values-sw/strings.xml create mode 100644 wmshell/res/values-sw/strings_tv.xml create mode 100644 wmshell/res/values-sw600dp/config.xml create mode 100644 wmshell/res/values-ta/strings.xml create mode 100644 wmshell/res/values-ta/strings_tv.xml create mode 100644 wmshell/res/values-te/strings.xml create mode 100644 wmshell/res/values-te/strings_tv.xml create mode 100644 wmshell/res/values-television/config.xml create mode 100644 wmshell/res/values-television/dimen.xml create mode 100644 wmshell/res/values-th/strings.xml create mode 100644 wmshell/res/values-th/strings_tv.xml create mode 100644 wmshell/res/values-tl/strings.xml create mode 100644 wmshell/res/values-tl/strings_tv.xml create mode 100644 wmshell/res/values-tr/strings.xml create mode 100644 wmshell/res/values-tr/strings_tv.xml create mode 100644 wmshell/res/values-tvdpi/dimen.xml create mode 100644 wmshell/res/values-uk/strings.xml create mode 100644 wmshell/res/values-uk/strings_tv.xml create mode 100644 wmshell/res/values-ur/strings.xml create mode 100644 wmshell/res/values-ur/strings_tv.xml create mode 100644 wmshell/res/values-uz/strings.xml create mode 100644 wmshell/res/values-uz/strings_tv.xml create mode 100644 wmshell/res/values-vi/strings.xml create mode 100644 wmshell/res/values-vi/strings_tv.xml create mode 100644 wmshell/res/values-watch/colors.xml create mode 100644 wmshell/res/values-watch/config.xml create mode 100644 wmshell/res/values-watch/dimen.xml create mode 100644 wmshell/res/values-zh-rCN/strings.xml create mode 100644 wmshell/res/values-zh-rCN/strings_tv.xml create mode 100644 wmshell/res/values-zh-rHK/strings.xml create mode 100644 wmshell/res/values-zh-rHK/strings_tv.xml create mode 100644 wmshell/res/values-zh-rTW/strings.xml create mode 100644 wmshell/res/values-zh-rTW/strings_tv.xml create mode 100644 wmshell/res/values-zu/strings.xml create mode 100644 wmshell/res/values-zu/strings_tv.xml create mode 100644 wmshell/res/values/attrs.xml create mode 100644 wmshell/res/values/colors.xml create mode 100644 wmshell/res/values/colors_tv.xml create mode 100644 wmshell/res/values/config.xml create mode 100644 wmshell/res/values/config_tv.xml create mode 100644 wmshell/res/values/dimen.xml create mode 100644 wmshell/res/values/ids.xml create mode 100644 wmshell/res/values/integers.xml create mode 100644 wmshell/res/values/strings.xml create mode 100644 wmshell/res/values/strings_tv.xml create mode 100644 wmshell/res/values/styles.xml create mode 100644 wmshell/shared/aidl/com/android/wm/shell/shared/IHomeTransitionListener.aidl create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/CounterRotator.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/ShellTransitions.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/TransitionUtil.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java create mode 100644 wmshell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java create mode 100644 wmshell/src/com/android/wm/shell/ProtoLogController.java create mode 100644 wmshell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java create mode 100644 wmshell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java create mode 100644 wmshell/src/com/android/wm/shell/ShellTaskOrganizer.java create mode 100644 wmshell/src/com/android/wm/shell/WindowManagerShellWrapper.java create mode 100644 wmshell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java create mode 100644 wmshell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java create mode 100644 wmshell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java create mode 100644 wmshell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java create mode 100644 wmshell/src/com/android/wm/shell/animation/FlingAnimationUtils.java create mode 100644 wmshell/src/com/android/wm/shell/animation/FloatProperties.kt create mode 100644 wmshell/src/com/android/wm/shell/animation/Interpolators.java create mode 100644 wmshell/src/com/android/wm/shell/back/BackAnimation.java create mode 100644 wmshell/src/com/android/wm/shell/back/BackAnimationBackground.java create mode 100644 wmshell/src/com/android/wm/shell/back/BackAnimationConstants.java create mode 100644 wmshell/src/com/android/wm/shell/back/BackAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/back/BackAnimationRunner.java create mode 100644 wmshell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt create mode 100644 wmshell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java create mode 100644 wmshell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt create mode 100644 wmshell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt create mode 100644 wmshell/src/com/android/wm/shell/back/IBackAnimation.aidl create mode 100644 wmshell/src/com/android/wm/shell/back/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/back/ShellBackAnimation.java create mode 100644 wmshell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java create mode 100644 wmshell/src/com/android/wm/shell/back/StatusBarCustomizer.java create mode 100644 wmshell/src/com/android/wm/shell/back/TEST_MAPPING create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BadgedImageView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/Bubble.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleController.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleData.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleEntry.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleLogger.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubblePositioner.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleStackView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleTaskViewFactory.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/Bubbles.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubblesNavBarInputEventReceiver.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/DeviceConfig.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/DismissViewExt.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/IBubbles.aidl create mode 100644 wmshell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl create mode 100644 wmshell/src/com/android/wm/shell/bubbles/ManageEducationView.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/bubbles/ObjectWrapper.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/StackEducationView.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/FlingToDismissUtils.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/OneTimeEndListener.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/OverScroll.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepository.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt create mode 100644 wmshell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt create mode 100644 wmshell/src/com/android/wm/shell/common/AlphaOptimizedButton.java create mode 100644 wmshell/src/com/android/wm/shell/common/DevicePostureController.java create mode 100644 wmshell/src/com/android/wm/shell/common/DisplayChangeController.java create mode 100644 wmshell/src/com/android/wm/shell/common/DisplayController.java create mode 100644 wmshell/src/com/android/wm/shell/common/DisplayImeController.java create mode 100644 wmshell/src/com/android/wm/shell/common/DisplayInsetsController.java create mode 100644 wmshell/src/com/android/wm/shell/common/DisplayLayout.java create mode 100644 wmshell/src/com/android/wm/shell/common/DockStateReader.java create mode 100644 wmshell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java create mode 100644 wmshell/src/com/android/wm/shell/common/FloatingContentCoordinator.kt create mode 100644 wmshell/src/com/android/wm/shell/common/HandlerExecutor.java create mode 100644 wmshell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java create mode 100644 wmshell/src/com/android/wm/shell/common/LaunchAdjacentController.kt create mode 100644 wmshell/src/com/android/wm/shell/common/MultiInstanceHelper.kt create mode 100644 wmshell/src/com/android/wm/shell/common/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/common/RemoteCallable.java create mode 100644 wmshell/src/com/android/wm/shell/common/ScreenshotUtils.java create mode 100644 wmshell/src/com/android/wm/shell/common/ShellExecutor.java create mode 100644 wmshell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java create mode 100644 wmshell/src/com/android/wm/shell/common/SurfaceUtils.java create mode 100644 wmshell/src/com/android/wm/shell/common/SyncTransactionQueue.java create mode 100644 wmshell/src/com/android/wm/shell/common/SystemWindows.java create mode 100644 wmshell/src/com/android/wm/shell/common/TabletopModeController.java create mode 100644 wmshell/src/com/android/wm/shell/common/TaskStackListenerCallback.java create mode 100644 wmshell/src/com/android/wm/shell/common/TaskStackListenerImpl.java create mode 100644 wmshell/src/com/android/wm/shell/common/TransactionPool.java create mode 100644 wmshell/src/com/android/wm/shell/common/TriangleShape.java create mode 100644 wmshell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BubbleConstants.java create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/DismissView.kt create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt create mode 100644 wmshell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java create mode 100644 wmshell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl create mode 100644 wmshell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt create mode 100644 wmshell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt create mode 100644 wmshell/src/com/android/wm/shell/common/pip/IPip.aidl create mode 100644 wmshell/src/com/android/wm/shell/common/pip/IPipAnimationListener.aidl create mode 100644 wmshell/src/com/android/wm/shell/common/pip/LegacySizeSpecSource.kt create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipBoundsState.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipDoubleTapHelper.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipKeepClearAlgorithmInterface.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipMediaController.kt create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipMenuController.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipPerfHintController.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipPinchResizingAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipSnapAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipUiEventLogger.kt create mode 100644 wmshell/src/com/android/wm/shell/common/pip/PipUtils.kt create mode 100644 wmshell/src/com/android/wm/shell/common/pip/SizeSpecSource.kt create mode 100644 wmshell/src/com/android/wm/shell/common/split/DividerHandleView.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/DividerView.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/DockedDividerUtils.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/common/split/SplitDecorManager.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/SplitLayout.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/SplitScreenConstants.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/SplitScreenUtils.java create mode 100644 wmshell/src/com/android/wm/shell/common/split/SplitWindowManager.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/AppCompatUtils.kt create mode 100644 wmshell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/CompatUIController.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/CompatUILayout.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/DialogAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/LetterboxEduDialogActionLayout.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/LetterboxEduDialogLayout.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/RestartDialogLayout.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java create mode 100644 wmshell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/DynamicOverride.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/TvWMShellModule.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/WMShellBaseModule.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/WMShellModule.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/WMSingleton.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/pip/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/dagger/pip/Pip1Module.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/pip/Pip2Module.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/pip/PipModule.java create mode 100644 wmshell/src/com/android/wm/shell/dagger/pip/TvPipModule.java create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopMode.java create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt create mode 100644 wmshell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java create mode 100644 wmshell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java create mode 100644 wmshell/src/com/android/wm/shell/docs/README.md create mode 100644 wmshell/src/com/android/wm/shell/docs/changes.md create mode 100644 wmshell/src/com/android/wm/shell/docs/dagger.md create mode 100644 wmshell/src/com/android/wm/shell/docs/debugging.md create mode 100644 wmshell/src/com/android/wm/shell/docs/extending.md create mode 100644 wmshell/src/com/android/wm/shell/docs/overview.md create mode 100644 wmshell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md create mode 100644 wmshell/src/com/android/wm/shell/docs/sysui.md create mode 100644 wmshell/src/com/android/wm/shell/docs/testing.md create mode 100644 wmshell/src/com/android/wm/shell/docs/threading.md create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DragAndDropController.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DragLayout.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DragSession.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DragUtils.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/DropZoneView.java create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt create mode 100644 wmshell/src/com/android/wm/shell/draganddrop/IDragAndDrop.aidl create mode 100644 wmshell/src/com/android/wm/shell/freeform/FreeformComponents.java create mode 100644 wmshell/src/com/android/wm/shell/freeform/FreeformTaskListener.java create mode 100644 wmshell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java create mode 100644 wmshell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java create mode 100644 wmshell/src/com/android/wm/shell/freeform/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java create mode 100644 wmshell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java create mode 100644 wmshell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java create mode 100644 wmshell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/IOneHanded.aidl create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHanded.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedAccessibilityUtil.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedController.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedEventCallback.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedState.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java create mode 100644 wmshell/src/com/android/wm/shell/onehanded/OneHandedUiEventLogger.java create mode 100644 wmshell/src/com/android/wm/shell/performance/PerfHintController.kt create mode 100644 wmshell/src/com/android/wm/shell/pip/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java create mode 100644 wmshell/src/com/android/wm/shell/pip/Pip.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipContentOverlay.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipParamsChangedForwarder.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipTaskOrganizer.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipTransition.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipTransitionController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/PipTransitionState.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipMenuActionView.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipMenuView.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipTouchGesture.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java create mode 100644 wmshell/src/com/android/wm/shell/pip/phone/PipTouchState.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipAction.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipInterpolators.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java create mode 100644 wmshell/src/com/android/wm/shell/pip/tv/TvPipTransition.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/README.md create mode 100644 wmshell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipController.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipMenuView.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipScheduler.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipTouchState.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipTransition.java create mode 100644 wmshell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java create mode 100644 wmshell/src/com/android/wm/shell/protolog/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java create mode 100644 wmshell/src/com/android/wm/shell/recents/IRecentTasks.aidl create mode 100644 wmshell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl create mode 100644 wmshell/src/com/android/wm/shell/recents/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/recents/RecentTasks.java create mode 100644 wmshell/src/com/android/wm/shell/recents/RecentTasksController.java create mode 100644 wmshell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java create mode 100644 wmshell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/ISplitScreenListener.aidl create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/ISplitSelectListener.aidl create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/MainStage.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/SideStage.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/SplitScreen.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/SplitScreenController.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/StageCoordinator.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/StageTaskListener.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/tv/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java create mode 100644 wmshell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/AbsSplashWindowCreator.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/IStartingWindow.aidl create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/IStartingWindowListener.aidl create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/SnapshotWindowCreator.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/StartingSurface.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/StartingWindowController.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/StartingWindowTypeAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/startingsurface/tv/TvStartingWindowTypeAlgorithm.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/ConfigurationChangeListener.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/ShellCommandHandler.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/ShellController.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/ShellInit.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/ShellInterface.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/ShellSharedConstants.java create mode 100644 wmshell/src/com/android/wm/shell/sysui/UserChangeListener.java create mode 100644 wmshell/src/com/android/wm/shell/taskview/TaskView.java create mode 100644 wmshell/src/com/android/wm/shell/taskview/TaskViewBase.java create mode 100644 wmshell/src/com/android/wm/shell/taskview/TaskViewFactory.java create mode 100644 wmshell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java create mode 100644 wmshell/src/com/android/wm/shell/taskview/TaskViewTaskController.java create mode 100644 wmshell/src/com/android/wm/shell/taskview/TaskViewTransitions.java create mode 100644 wmshell/src/com/android/wm/shell/transition/CounterRotatorHelper.java create mode 100644 wmshell/src/com/android/wm/shell/transition/DefaultMixedHandler.java create mode 100644 wmshell/src/com/android/wm/shell/transition/DefaultMixedTransition.java create mode 100644 wmshell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/transition/HomeTransitionObserver.java create mode 100644 wmshell/src/com/android/wm/shell/transition/LegacyTransitions.java create mode 100644 wmshell/src/com/android/wm/shell/transition/MixedTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/transition/MixedTransitionHelper.java create mode 100644 wmshell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java create mode 100644 wmshell/src/com/android/wm/shell/transition/RecentsMixedTransition.java create mode 100644 wmshell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java create mode 100644 wmshell/src/com/android/wm/shell/transition/SleepHandler.java create mode 100644 wmshell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java create mode 100644 wmshell/src/com/android/wm/shell/transition/Transitions.java create mode 100644 wmshell/src/com/android/wm/shell/transition/WindowThumbnail.java create mode 100644 wmshell/src/com/android/wm/shell/transition/tracing/LegacyTransitionTracer.java create mode 100644 wmshell/src/com/android/wm/shell/transition/tracing/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java create mode 100644 wmshell/src/com/android/wm/shell/transition/tracing/TransitionTracer.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java create mode 100644 wmshell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java create mode 100644 wmshell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl create mode 100644 wmshell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java create mode 100644 wmshell/src/com/android/wm/shell/util/KtProtoLog.kt create mode 100644 wmshell/src/com/android/wm/shell/util/SplitBounds.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/DragDetector.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/HandleMenu.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/OWNERS create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/OnTaskResizeAnimationListener.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/TaskFocusStateConsumer.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/TaskOperations.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/WindowDecorLinearLayout.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/WindowDecoration.java create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt create mode 100644 wmshell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt create mode 100644 wmshell/tests/OWNERS create mode 100644 wmshell/tests/README.md create mode 100644 wmshell/tests/flicker/Android.bp create mode 100644 wmshell/tests/flicker/README.md create mode 100644 wmshell/tests/flicker/appcompat/Android.bp create mode 100644 wmshell/tests/flicker/appcompat/AndroidManifest.xml create mode 100644 wmshell/tests/flicker/appcompat/AndroidTestTemplate.xml create mode 100644 wmshell/tests/flicker/appcompat/res/xml/network_security_config.xml create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt create mode 100644 wmshell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt create mode 100644 wmshell/tests/flicker/appcompat/trace_config/trace_config.textproto create mode 100644 wmshell/tests/flicker/bubble/Android.bp create mode 100644 wmshell/tests/flicker/bubble/AndroidManifest.xml create mode 100644 wmshell/tests/flicker/bubble/AndroidTestTemplate.xml create mode 100644 wmshell/tests/flicker/bubble/OWNERS create mode 100644 wmshell/tests/flicker/bubble/res/xml/network_security_config.xml create mode 100644 wmshell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt create mode 100644 wmshell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt create mode 100644 wmshell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt create mode 100644 wmshell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt create mode 100644 wmshell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt create mode 100644 wmshell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt create mode 100644 wmshell/tests/flicker/bubble/trace_config/trace_config.textproto create mode 100644 wmshell/tests/flicker/pip/Android.bp create mode 100644 wmshell/tests/flicker/pip/AndroidManifest.xml create mode 100644 wmshell/tests/flicker/pip/AndroidTestTemplate.xml create mode 100644 wmshell/tests/flicker/pip/OWNERS create mode 100644 wmshell/tests/flicker/pip/csuiteDefaultTemplate.xml create mode 100644 wmshell/tests/flicker/pip/csuitePlan.xml create mode 100644 wmshell/tests/flicker/pip/res/xml/network_security_config.xml create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipDragTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/PipAppHelperTv.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt create mode 100644 wmshell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt create mode 100644 wmshell/tests/flicker/pip/trace_config/trace_config.textproto create mode 100644 wmshell/tests/flicker/service/Android.bp create mode 100644 wmshell/tests/flicker/service/AndroidManifest.xml create mode 100644 wmshell/tests/flicker/service/AndroidTestTemplate.xml create mode 100644 wmshell/tests/flicker/service/res/xml/network_security_config.xml create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/common/Utils.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/OWNERS create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt create mode 100644 wmshell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt create mode 100644 wmshell/tests/flicker/service/trace_config/trace_config.textproto create mode 100644 wmshell/tests/flicker/splitscreen/Android.bp create mode 100644 wmshell/tests/flicker/splitscreen/AndroidManifest.xml create mode 100644 wmshell/tests/flicker/splitscreen/AndroidTestTemplate.xml create mode 100644 wmshell/tests/flicker/splitscreen/OWNERS create mode 100644 wmshell/tests/flicker/splitscreen/res/xml/network_security_config.xml create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt create mode 100644 wmshell/tests/flicker/splitscreen/trace_config/trace_config.textproto create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonConstants.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/utils/MultiWindowUtils.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/utils/NotificationListener.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt create mode 100644 wmshell/tests/flicker/src/com/android/wm/shell/flicker/utils/WaitUtils.kt create mode 100644 wmshell/tests/unittest/Android.bp create mode 100644 wmshell/tests/unittest/AndroidManifest.xml create mode 100644 wmshell/tests/unittest/AndroidTest.xml create mode 100644 wmshell/tests/unittest/res/layout/main.xml create mode 100644 wmshell/tests/unittest/res/values/config.xml create mode 100644 wmshell/tests/unittest/res/values/dimen.xml create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/MockSurfaceControlHelper.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/MockToken.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/TestHandler.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/back/OWNERS create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTestActivity.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/TestableBubblePositioner.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/animation/StackAnimationControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/LaunchAdjacentControllerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenConstantsTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedUiEventLoggerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenUtilsTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/util/StubTransaction.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt create mode 100644 wmshell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09c9f34110..1677ff5a53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: echo ${{ secrets.KEYSTORE }} | base64 --decode > ${{ github.workspace }}/key.jks fi - name: Build debug APK - run: ./gradlew assembleLawnWithQuickstepGithubRelease assembleLawnWithQuickstepGithubRelease + run: ./gradlew assembleLawnWithQuickstepGithubDebug assembleLawnWithQuickstepPlayDebug - name: Upload artifact uses: actions/upload-artifact@v4 with: diff --git a/build.gradle b/build.gradle index bb9ced66e1..54809f5a8f 100644 --- a/build.gradle +++ b/build.gradle @@ -278,7 +278,7 @@ android { lawn { java.srcDirs = ['src_flags', 'src_shortcuts_overrides', 'lawnchair/src', 'tests/shared'] aidl.srcDirs = ['lawnchair/aidl'] - res.srcDirs = ['lawnchair/res', 'platform_frameworks_libs_systemui/animationlib/res'] + res.srcDirs = ['lawnchair/res'] manifest.srcFile "lawnchair/AndroidManifest.xml" assets { srcDirs 'lawnchair/assets' @@ -336,13 +336,13 @@ dependencies { // Recents lib dependency withQuickstepCompileOnly projects.hiddenApi withQuickstepImplementation projects.systemUIShared -// withQuickstepImplementation projects.systemUIAnim + withQuickstepImplementation projects.systemUIAnim withQuickstepImplementation projects.systemUnFold -// withQuickstepImplementation projects.systemUIViewCapture -// withQuickstepImplementation projects.systemUILog -// withQuickstepCompileOnly projects.systemUIPlugin -// withQuickstepImplementation projects.systemUIPluginCore -// withQuickstepCompileOnly projects.systemUICommon + withQuickstepImplementation projects.systemUIViewCapture + withQuickstepImplementation projects.systemUILog + withQuickstepCompileOnly projects.systemUIPlugin + withQuickstepImplementation projects.systemUIPluginCore + withQuickstepCompileOnly projects.systemUICommon // QuickSwitch Compat withQuickstepImplementation projects.compatLib @@ -351,10 +351,13 @@ dependencies { withQuickstepImplementation projects.compatLib.compatLibVS withQuickstepImplementation projects.compatLib.compatLibVT withQuickstepImplementation projects.compatLib.compatLibVU + withQuickstepImplementation projects.compatLib.compatLibVV + withQuickstepImplementation projects.wmshell + withQuickstepImplementation projects.flags + withQuickstepImplementation projects.androidxLib implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd-14.jar') implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell-15.jar') -// withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'QuickstepResLib.jar') withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'framework-15.jar') coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' @@ -362,8 +365,6 @@ dependencies { implementation 'androidx.profileinstaller:profileinstaller:1.4.1' baselineProfile projects.baselineProfile - implementation projects.flags - implementation projects.androidxLib implementation "androidx.recyclerview:recyclerview:1.3.2" implementation "androidx.preference:preference-ktx:1.2.1" diff --git a/flags/build.gradle b/flags/build.gradle index 131ef9d431..ee1b25c573 100644 --- a/flags/build.gradle +++ b/flags/build.gradle @@ -8,6 +8,7 @@ android { sourceSets { main { java.srcDirs = ['src'] + res.srcDirs = ['res'] } } } diff --git a/flags/src/com/android/systemui/CustomFeatureFlags.java b/flags/src/com/android/systemui/CustomFeatureFlags.java new file mode 100644 index 0000000000..aadf0d0267 --- /dev/null +++ b/flags/src/com/android/systemui/CustomFeatureFlags.java @@ -0,0 +1,1102 @@ +package com.android.systemui; + +// TODO(b/303773055): Remove the annotation after access issue is resolved. +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +/** @hide */ +public class CustomFeatureFlags implements FeatureFlags { + + private BiPredicate> mGetValueImpl; + + public CustomFeatureFlags(BiPredicate> getValueImpl) { + mGetValueImpl = getValueImpl; + } + @Override + public boolean activityTransitionUseLargestWindow() { + return getValue(Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, + FeatureFlags::activityTransitionUseLargestWindow); + } + + @Override + public boolean ambientTouchMonitorListenToDisplayChanges() { + return getValue(Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, + FeatureFlags::ambientTouchMonitorListenToDisplayChanges); + } + + @Override + public boolean appClipsBacklinks() { + return getValue(Flags.FLAG_APP_CLIPS_BACKLINKS, + FeatureFlags::appClipsBacklinks); + } + + @Override + public boolean bindKeyguardMediaVisibility() { + return getValue(Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, + FeatureFlags::bindKeyguardMediaVisibility); + } + + @Override + public boolean bpTalkback() { + return getValue(Flags.FLAG_BP_TALKBACK, + FeatureFlags::bpTalkback); + } + + @Override + public boolean brightnessSliderFocusState() { + return getValue(Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, + FeatureFlags::brightnessSliderFocusState); + } + + @Override + public boolean centralizedStatusBarHeightFix() { + return getValue(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX, + FeatureFlags::centralizedStatusBarHeightFix); + } + + @Override + public boolean clipboardNoninteractiveOnLockscreen() { + return getValue(Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, + FeatureFlags::clipboardNoninteractiveOnLockscreen); + } + + @Override + public boolean clockReactiveVariants() { + return getValue(Flags.FLAG_CLOCK_REACTIVE_VARIANTS, + FeatureFlags::clockReactiveVariants); + } + + @Override + public boolean communalBouncerDoNotModifyPluginOpen() { + return getValue(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, + FeatureFlags::communalBouncerDoNotModifyPluginOpen); + } + + @Override + public boolean communalHub() { + return getValue(Flags.FLAG_COMMUNAL_HUB, + FeatureFlags::communalHub); + } + + @Override + public boolean composeBouncer() { + return getValue(Flags.FLAG_COMPOSE_BOUNCER, + FeatureFlags::composeBouncer); + } + + @Override + public boolean composeLockscreen() { + return getValue(Flags.FLAG_COMPOSE_LOCKSCREEN, + FeatureFlags::composeLockscreen); + } + + @Override + public boolean confineNotificationTouchToViewWidth() { + return getValue(Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, + FeatureFlags::confineNotificationTouchToViewWidth); + } + + @Override + public boolean constraintBp() { + return getValue(Flags.FLAG_CONSTRAINT_BP, + FeatureFlags::constraintBp); + } + + @Override + public boolean contextualTipsAssistantDismissFix() { + return getValue(Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, + FeatureFlags::contextualTipsAssistantDismissFix); + } + + @Override + public boolean coroutineTracing() { + return getValue(Flags.FLAG_COROUTINE_TRACING, + FeatureFlags::coroutineTracing); + } + + @Override + public boolean createWindowlessWindowMagnifier() { + return getValue(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, + FeatureFlags::createWindowlessWindowMagnifier); + } + + @Override + public boolean dedicatedNotifInflationThread() { + return getValue(Flags.FLAG_DEDICATED_NOTIF_INFLATION_THREAD, + FeatureFlags::dedicatedNotifInflationThread); + } + + @Override + public boolean delayShowMagnificationButton() { + return getValue(Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, + FeatureFlags::delayShowMagnificationButton); + } + + @Override + public boolean delayedWakelockReleaseOnBackgroundThread() { + return getValue(Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD, + FeatureFlags::delayedWakelockReleaseOnBackgroundThread); + } + + @Override + public boolean deviceEntryUdfpsRefactor() { + return getValue(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, + FeatureFlags::deviceEntryUdfpsRefactor); + } + + @Override + public boolean disableContextualTipsFrequencyCheck() { + return getValue(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, + FeatureFlags::disableContextualTipsFrequencyCheck); + } + + @Override + public boolean disableContextualTipsIosSwitcherCheck() { + return getValue(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, + FeatureFlags::disableContextualTipsIosSwitcherCheck); + } + + @Override + public boolean dozeuiSchedulingAlarmsBackgroundExecution() { + return getValue(Flags.FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION, + FeatureFlags::dozeuiSchedulingAlarmsBackgroundExecution); + } + + @Override + public boolean dreamInputSessionPilferOnce() { + return getValue(Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, + FeatureFlags::dreamInputSessionPilferOnce); + } + + @Override + public boolean dreamOverlayBouncerSwipeDirectionFiltering() { + return getValue(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, + FeatureFlags::dreamOverlayBouncerSwipeDirectionFiltering); + } + + @Override + public boolean dualShade() { + return getValue(Flags.FLAG_DUAL_SHADE, + FeatureFlags::dualShade); + } + + @Override + public boolean edgeBackGestureHandlerThread() { + return getValue(Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, + FeatureFlags::edgeBackGestureHandlerThread); + } + + @Override + public boolean edgebackGestureHandlerGetRunningTasksBackground() { + return getValue(Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, + FeatureFlags::edgebackGestureHandlerGetRunningTasksBackground); + } + + @Override + public boolean enableBackgroundKeyguardOndrawnCallback() { + return getValue(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, + FeatureFlags::enableBackgroundKeyguardOndrawnCallback); + } + + @Override + public boolean enableContextualTipForMuteVolume() { + return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, + FeatureFlags::enableContextualTipForMuteVolume); + } + + @Override + public boolean enableContextualTipForPowerOff() { + return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, + FeatureFlags::enableContextualTipForPowerOff); + } + + @Override + public boolean enableContextualTipForTakeScreenshot() { + return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, + FeatureFlags::enableContextualTipForTakeScreenshot); + } + + @Override + public boolean enableContextualTips() { + return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, + FeatureFlags::enableContextualTips); + } + + @Override + public boolean enableEfficientDisplayRepository() { + return getValue(Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, + FeatureFlags::enableEfficientDisplayRepository); + } + + @Override + public boolean enableLayoutTracing() { + return getValue(Flags.FLAG_ENABLE_LAYOUT_TRACING, + FeatureFlags::enableLayoutTracing); + } + + @Override + public boolean enableViewCaptureTracing() { + return getValue(Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, + FeatureFlags::enableViewCaptureTracing); + } + + @Override + public boolean enableWidgetPickerSizeFilter() { + return getValue(Flags.FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER, + FeatureFlags::enableWidgetPickerSizeFilter); + } + + @Override + public boolean enforceBrightnessBaseUserRestriction() { + return getValue(Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, + FeatureFlags::enforceBrightnessBaseUserRestriction); + } + + @Override + public boolean exampleFlag() { + return getValue(Flags.FLAG_EXAMPLE_FLAG, + FeatureFlags::exampleFlag); + } + + @Override + public boolean fastUnlockTransition() { + return getValue(Flags.FLAG_FAST_UNLOCK_TRANSITION, + FeatureFlags::fastUnlockTransition); + } + + @Override + public boolean fixImageWallpaperCrashSurfaceAlreadyReleased() { + return getValue(Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, + FeatureFlags::fixImageWallpaperCrashSurfaceAlreadyReleased); + } + + @Override + public boolean fixScreenshotActionDismissSystemWindows() { + return getValue(Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + FeatureFlags::fixScreenshotActionDismissSystemWindows); + } + + @Override + public boolean floatingMenuAnimatedTuck() { + return getValue(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, + FeatureFlags::floatingMenuAnimatedTuck); + } + + @Override + public boolean floatingMenuDragToEdit() { + return getValue(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, + FeatureFlags::floatingMenuDragToEdit); + } + + @Override + public boolean floatingMenuDragToHide() { + return getValue(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, + FeatureFlags::floatingMenuDragToHide); + } + + @Override + public boolean floatingMenuImeDisplacementAnimation() { + return getValue(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, + FeatureFlags::floatingMenuImeDisplacementAnimation); + } + + @Override + public boolean floatingMenuNarrowTargetContentObserver() { + return getValue(Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, + FeatureFlags::floatingMenuNarrowTargetContentObserver); + } + + @Override + public boolean floatingMenuOverlapsNavBarsFlag() { + return getValue(Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, + FeatureFlags::floatingMenuOverlapsNavBarsFlag); + } + + @Override + public boolean floatingMenuRadiiAnimation() { + return getValue(Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, + FeatureFlags::floatingMenuRadiiAnimation); + } + + @Override + public boolean getConnectedDeviceNameUnsynchronized() { + return getValue(Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, + FeatureFlags::getConnectedDeviceNameUnsynchronized); + } + + @Override + public boolean glanceableHubAllowKeyguardWhenDreaming() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, + FeatureFlags::glanceableHubAllowKeyguardWhenDreaming); + } + + @Override + public boolean glanceableHubFullscreenSwipe() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE, + FeatureFlags::glanceableHubFullscreenSwipe); + } + + @Override + public boolean glanceableHubGestureHandle() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_GESTURE_HANDLE, + FeatureFlags::glanceableHubGestureHandle); + } + + @Override + public boolean glanceableHubShortcutButton() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON, + FeatureFlags::glanceableHubShortcutButton); + } + + @Override + public boolean hapticBrightnessSlider() { + return getValue(Flags.FLAG_HAPTIC_BRIGHTNESS_SLIDER, + FeatureFlags::hapticBrightnessSlider); + } + + @Override + public boolean hapticVolumeSlider() { + return getValue(Flags.FLAG_HAPTIC_VOLUME_SLIDER, + FeatureFlags::hapticVolumeSlider); + } + + @Override + public boolean hearingAidsQsTileDialog() { + return getValue(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, + FeatureFlags::hearingAidsQsTileDialog); + } + + @Override + public boolean hearingDevicesDialogRelatedTools() { + return getValue(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, + FeatureFlags::hearingDevicesDialogRelatedTools); + } + + @Override + public boolean keyboardDockingIndicator() { + return getValue(Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, + FeatureFlags::keyboardDockingIndicator); + } + + @Override + public boolean keyboardShortcutHelperRewrite() { + return getValue(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, + FeatureFlags::keyboardShortcutHelperRewrite); + } + + @Override + public boolean keyguardBottomAreaRefactor() { + return getValue(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, + FeatureFlags::keyguardBottomAreaRefactor); + } + + @Override + public boolean keyguardWmStateRefactor() { + return getValue(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + FeatureFlags::keyguardWmStateRefactor); + } + + @Override + public boolean lightRevealMigration() { + return getValue(Flags.FLAG_LIGHT_REVEAL_MIGRATION, + FeatureFlags::lightRevealMigration); + } + + @Override + public boolean mediaControlsLockscreenShadeBugFix() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, + FeatureFlags::mediaControlsLockscreenShadeBugFix); + } + + @Override + public boolean mediaControlsRefactor() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_REFACTOR, + FeatureFlags::mediaControlsRefactor); + } + + @Override + public boolean mediaControlsUserInitiatedDeleteintent() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, + FeatureFlags::mediaControlsUserInitiatedDeleteintent); + } + + @Override + public boolean migrateClocksToBlueprint() { + return getValue(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + FeatureFlags::migrateClocksToBlueprint); + } + + @Override + public boolean newAodTransition() { + return getValue(Flags.FLAG_NEW_AOD_TRANSITION, + FeatureFlags::newAodTransition); + } + + @Override + public boolean newTouchpadGesturesTutorial() { + return getValue(Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, + FeatureFlags::newTouchpadGesturesTutorial); + } + + @Override + public boolean newVolumePanel() { + return getValue(Flags.FLAG_NEW_VOLUME_PANEL, + FeatureFlags::newVolumePanel); + } + + @Override + public boolean notificationAsyncGroupHeaderInflation() { + return getValue(Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, + FeatureFlags::notificationAsyncGroupHeaderInflation); + } + + @Override + public boolean notificationAsyncHybridViewInflation() { + return getValue(Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, + FeatureFlags::notificationAsyncHybridViewInflation); + } + + @Override + public boolean notificationAvalancheSuppression() { + return getValue(Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, + FeatureFlags::notificationAvalancheSuppression); + } + + @Override + public boolean notificationAvalancheThrottleHun() { + return getValue(Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, + FeatureFlags::notificationAvalancheThrottleHun); + } + + @Override + public boolean notificationBackgroundTintOptimization() { + return getValue(Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, + FeatureFlags::notificationBackgroundTintOptimization); + } + + @Override + public boolean notificationColorUpdateLogger() { + return getValue(Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, + FeatureFlags::notificationColorUpdateLogger); + } + + @Override + public boolean notificationContentAlphaOptimization() { + return getValue(Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, + FeatureFlags::notificationContentAlphaOptimization); + } + + @Override + public boolean notificationFooterBackgroundTintOptimization() { + return getValue(Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, + FeatureFlags::notificationFooterBackgroundTintOptimization); + } + + @Override + public boolean notificationMediaManagerBackgroundExecution() { + return getValue(Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION, + FeatureFlags::notificationMediaManagerBackgroundExecution); + } + + @Override + public boolean notificationMinimalismPrototype() { + return getValue(Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE, + FeatureFlags::notificationMinimalismPrototype); + } + + @Override + public boolean notificationOverExpansionClippingFix() { + return getValue(Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, + FeatureFlags::notificationOverExpansionClippingFix); + } + + @Override + public boolean notificationPulsingFix() { + return getValue(Flags.FLAG_NOTIFICATION_PULSING_FIX, + FeatureFlags::notificationPulsingFix); + } + + @Override + public boolean notificationRowContentBinderRefactor() { + return getValue(Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, + FeatureFlags::notificationRowContentBinderRefactor); + } + + @Override + public boolean notificationRowUserContext() { + return getValue(Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, + FeatureFlags::notificationRowUserContext); + } + + @Override + public boolean notificationViewFlipperPausingV2() { + return getValue(Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, + FeatureFlags::notificationViewFlipperPausingV2); + } + + @Override + public boolean notificationsBackgroundIcons() { + return getValue(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, + FeatureFlags::notificationsBackgroundIcons); + } + + @Override + public boolean notificationsFooterViewRefactor() { + return getValue(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR, + FeatureFlags::notificationsFooterViewRefactor); + } + + @Override + public boolean notificationsHeadsUpRefactor() { + return getValue(Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR, + FeatureFlags::notificationsHeadsUpRefactor); + } + + @Override + public boolean notificationsHideOnDisplaySwitch() { + return getValue(Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, + FeatureFlags::notificationsHideOnDisplaySwitch); + } + + @Override + public boolean notificationsIconContainerRefactor() { + return getValue(Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, + FeatureFlags::notificationsIconContainerRefactor); + } + + @Override + public boolean notificationsImprovedHunAnimation() { + return getValue(Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION, + FeatureFlags::notificationsImprovedHunAnimation); + } + + @Override + public boolean notificationsLiveDataStoreRefactor() { + return getValue(Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, + FeatureFlags::notificationsLiveDataStoreRefactor); + } + + @Override + public boolean notifyPowerManagerUserActivityBackground() { + return getValue(Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, + FeatureFlags::notifyPowerManagerUserActivityBackground); + } + + @Override + public boolean pinInputFieldStyledFocusState() { + return getValue(Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, + FeatureFlags::pinInputFieldStyledFocusState); + } + + @Override + public boolean predictiveBackAnimateBouncer() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER, + FeatureFlags::predictiveBackAnimateBouncer); + } + + @Override + public boolean predictiveBackAnimateDialogs() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS, + FeatureFlags::predictiveBackAnimateDialogs); + } + + @Override + public boolean predictiveBackAnimateShade() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, + FeatureFlags::predictiveBackAnimateShade); + } + + @Override + public boolean predictiveBackSysui() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_SYSUI, + FeatureFlags::predictiveBackSysui); + } + + @Override + public boolean priorityPeopleSection() { + return getValue(Flags.FLAG_PRIORITY_PEOPLE_SECTION, + FeatureFlags::priorityPeopleSection); + } + + @Override + public boolean privacyDotUnfoldWrongCornerFix() { + return getValue(Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX, + FeatureFlags::privacyDotUnfoldWrongCornerFix); + } + + @Override + public boolean pssAppSelectorAbruptExitFix() { + return getValue(Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX, + FeatureFlags::pssAppSelectorAbruptExitFix); + } + + @Override + public boolean pssAppSelectorRecentsSplitScreen() { + return getValue(Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, + FeatureFlags::pssAppSelectorRecentsSplitScreen); + } + + @Override + public boolean pssTaskSwitcher() { + return getValue(Flags.FLAG_PSS_TASK_SWITCHER, + FeatureFlags::pssTaskSwitcher); + } + + @Override + public boolean qsCustomTileClickGuaranteedBugFix() { + return getValue(Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + FeatureFlags::qsCustomTileClickGuaranteedBugFix); + } + + @Override + public boolean qsNewPipeline() { + return getValue(Flags.FLAG_QS_NEW_PIPELINE, + FeatureFlags::qsNewPipeline); + } + + @Override + public boolean qsNewTiles() { + return getValue(Flags.FLAG_QS_NEW_TILES, + FeatureFlags::qsNewTiles); + } + + @Override + public boolean qsNewTilesFuture() { + return getValue(Flags.FLAG_QS_NEW_TILES_FUTURE, + FeatureFlags::qsNewTilesFuture); + } + + @Override + public boolean qsTileFocusState() { + return getValue(Flags.FLAG_QS_TILE_FOCUS_STATE, + FeatureFlags::qsTileFocusState); + } + + @Override + public boolean qsUiRefactor() { + return getValue(Flags.FLAG_QS_UI_REFACTOR, + FeatureFlags::qsUiRefactor); + } + + @Override + public boolean quickSettingsVisualHapticsLongpress() { + return getValue(Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS, + FeatureFlags::quickSettingsVisualHapticsLongpress); + } + + @Override + public boolean recordIssueQsTile() { + return getValue(Flags.FLAG_RECORD_ISSUE_QS_TILE, + FeatureFlags::recordIssueQsTile); + } + + @Override + public boolean refactorGetCurrentUser() { + return getValue(Flags.FLAG_REFACTOR_GET_CURRENT_USER, + FeatureFlags::refactorGetCurrentUser); + } + + @Override + public boolean registerBatteryControllerReceiversInCorestartable() { + return getValue(Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, + FeatureFlags::registerBatteryControllerReceiversInCorestartable); + } + + @Override + public boolean registerNewWalletCardInBackground() { + return getValue(Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, + FeatureFlags::registerNewWalletCardInBackground); + } + + @Override + public boolean registerWallpaperNotifierBackground() { + return getValue(Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, + FeatureFlags::registerWallpaperNotifierBackground); + } + + @Override + public boolean registerZenModeContentObserverBackground() { + return getValue(Flags.FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND, + FeatureFlags::registerZenModeContentObserverBackground); + } + + @Override + public boolean removeDreamOverlayHideOnTouch() { + return getValue(Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, + FeatureFlags::removeDreamOverlayHideOnTouch); + } + + @Override + public boolean restToUnlock() { + return getValue(Flags.FLAG_REST_TO_UNLOCK, + FeatureFlags::restToUnlock); + } + + @Override + public boolean restartDreamOnUnocclude() { + return getValue(Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, + FeatureFlags::restartDreamOnUnocclude); + } + + @Override + public boolean revampedBouncerMessages() { + return getValue(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, + FeatureFlags::revampedBouncerMessages); + } + + @Override + public boolean runFingerprintDetectOnDismissibleKeyguard() { + return getValue(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, + FeatureFlags::runFingerprintDetectOnDismissibleKeyguard); + } + + @Override + public boolean saveAndRestoreMagnificationSettingsButtons() { + return getValue(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, + FeatureFlags::saveAndRestoreMagnificationSettingsButtons); + } + + @Override + public boolean sceneContainer() { + return getValue(Flags.FLAG_SCENE_CONTAINER, + FeatureFlags::sceneContainer); + } + + @Override + public boolean screenshareNotificationHidingBugFix() { + return getValue(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, + FeatureFlags::screenshareNotificationHidingBugFix); + } + + @Override + public boolean screenshotActionDismissSystemWindows() { + return getValue(Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + FeatureFlags::screenshotActionDismissSystemWindows); + } + + @Override + public boolean screenshotPrivateProfileAccessibilityAnnouncementFix() { + return getValue(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX, + FeatureFlags::screenshotPrivateProfileAccessibilityAnnouncementFix); + } + + @Override + public boolean screenshotPrivateProfileBehaviorFix() { + return getValue(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX, + FeatureFlags::screenshotPrivateProfileBehaviorFix); + } + + @Override + public boolean screenshotScrollCropViewCrashFix() { + return getValue(Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, + FeatureFlags::screenshotScrollCropViewCrashFix); + } + + @Override + public boolean screenshotShelfUi2() { + return getValue(Flags.FLAG_SCREENSHOT_SHELF_UI2, + FeatureFlags::screenshotShelfUi2); + } + + @Override + public boolean shadeCollapseActivityLaunchFix() { + return getValue(Flags.FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX, + FeatureFlags::shadeCollapseActivityLaunchFix); + } + + @Override + public boolean shaderlibLoadingEffectRefactor() { + return getValue(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, + FeatureFlags::shaderlibLoadingEffectRefactor); + } + + @Override + public boolean sliceBroadcastRelayInBackground() { + return getValue(Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, + FeatureFlags::sliceBroadcastRelayInBackground); + } + + @Override + public boolean sliceManagerBinderCallBackground() { + return getValue(Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, + FeatureFlags::sliceManagerBinderCallBackground); + } + + @Override + public boolean smartspaceLockscreenViewmodel() { + return getValue(Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, + FeatureFlags::smartspaceLockscreenViewmodel); + } + + @Override + public boolean smartspaceRelocateToBottom() { + return getValue(Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, + FeatureFlags::smartspaceRelocateToBottom); + } + + @Override + public boolean smartspaceRemoteviewsRendering() { + return getValue(Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING, + FeatureFlags::smartspaceRemoteviewsRendering); + } + + @Override + public boolean statusBarMonochromeIconsFix() { + return getValue(Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, + FeatureFlags::statusBarMonochromeIconsFix); + } + + @Override + public boolean statusBarScreenSharingChips() { + return getValue(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, + FeatureFlags::statusBarScreenSharingChips); + } + + @Override + public boolean statusBarStaticInoutIndicators() { + return getValue(Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, + FeatureFlags::statusBarStaticInoutIndicators); + } + + @Override + public boolean switchUserOnBg() { + return getValue(Flags.FLAG_SWITCH_USER_ON_BG, + FeatureFlags::switchUserOnBg); + } + + @Override + public boolean sysuiTeamfood() { + return getValue(Flags.FLAG_SYSUI_TEAMFOOD, + FeatureFlags::sysuiTeamfood); + } + + @Override + public boolean themeOverlayControllerWakefulnessDeprecation() { + return getValue(Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, + FeatureFlags::themeOverlayControllerWakefulnessDeprecation); + } + + @Override + public boolean translucentOccludingActivityFix() { + return getValue(Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, + FeatureFlags::translucentOccludingActivityFix); + } + + @Override + public boolean truncatedStatusBarIconsFix() { + return getValue(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX, + FeatureFlags::truncatedStatusBarIconsFix); + } + + @Override + public boolean udfpsViewPerformance() { + return getValue(Flags.FLAG_UDFPS_VIEW_PERFORMANCE, + FeatureFlags::udfpsViewPerformance); + } + + @Override + public boolean unfoldAnimationBackgroundProgress() { + return getValue(Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, + FeatureFlags::unfoldAnimationBackgroundProgress); + } + + @Override + public boolean updateUserSwitcherBackground() { + return getValue(Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, + FeatureFlags::updateUserSwitcherBackground); + } + + @Override + public boolean validateKeyboardShortcutHelperIconUri() { + return getValue(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI, + FeatureFlags::validateKeyboardShortcutHelperIconUri); + } + + @Override + public boolean visualInterruptionsRefactor() { + return getValue(Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR, + FeatureFlags::visualInterruptionsRefactor); + } + + public boolean isFlagReadOnlyOptimized(String flagName) { + if (mReadOnlyFlagsSet.contains(flagName) && + isOptimizationEnabled()) { + return true; + } + return false; + } + + private boolean isOptimizationEnabled() { + return false; + } + + protected boolean getValue(String flagName, Predicate getter) { + return mGetValueImpl.test(flagName, getter); + } + + public List getFlagNames() { + return Arrays.asList( + Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, + Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, + Flags.FLAG_APP_CLIPS_BACKLINKS, + Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, + Flags.FLAG_BP_TALKBACK, + Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, + Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX, + Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, + Flags.FLAG_CLOCK_REACTIVE_VARIANTS, + Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, + Flags.FLAG_COMMUNAL_HUB, + Flags.FLAG_COMPOSE_BOUNCER, + Flags.FLAG_COMPOSE_LOCKSCREEN, + Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, + Flags.FLAG_CONSTRAINT_BP, + Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, + Flags.FLAG_COROUTINE_TRACING, + Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, + Flags.FLAG_DEDICATED_NOTIF_INFLATION_THREAD, + Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, + Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD, + Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, + Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, + Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, + Flags.FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION, + Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, + Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, + Flags.FLAG_DUAL_SHADE, + Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, + Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, + Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, + Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, + Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, + Flags.FLAG_ENABLE_LAYOUT_TRACING, + Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, + Flags.FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER, + Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, + Flags.FLAG_EXAMPLE_FLAG, + Flags.FLAG_FAST_UNLOCK_TRANSITION, + Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, + Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, + Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, + Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, + Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, + Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, + Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, + Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, + Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, + Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, + Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE, + Flags.FLAG_GLANCEABLE_HUB_GESTURE_HANDLE, + Flags.FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON, + Flags.FLAG_HAPTIC_BRIGHTNESS_SLIDER, + Flags.FLAG_HAPTIC_VOLUME_SLIDER, + Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, + Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, + Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, + Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, + Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, + Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + Flags.FLAG_LIGHT_REVEAL_MIGRATION, + Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, + Flags.FLAG_MEDIA_CONTROLS_REFACTOR, + Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, + Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + Flags.FLAG_NEW_AOD_TRANSITION, + Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, + Flags.FLAG_NEW_VOLUME_PANEL, + Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, + Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, + Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, + Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, + Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, + Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION, + Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE, + Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, + Flags.FLAG_NOTIFICATION_PULSING_FIX, + Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, + Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, + Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, + Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, + Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR, + Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR, + Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, + Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, + Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION, + Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, + Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, + Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, + Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER, + Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS, + Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, + Flags.FLAG_PREDICTIVE_BACK_SYSUI, + Flags.FLAG_PRIORITY_PEOPLE_SECTION, + Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX, + Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX, + Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, + Flags.FLAG_PSS_TASK_SWITCHER, + Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + Flags.FLAG_QS_NEW_PIPELINE, + Flags.FLAG_QS_NEW_TILES, + Flags.FLAG_QS_NEW_TILES_FUTURE, + Flags.FLAG_QS_TILE_FOCUS_STATE, + Flags.FLAG_QS_UI_REFACTOR, + Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS, + Flags.FLAG_RECORD_ISSUE_QS_TILE, + Flags.FLAG_REFACTOR_GET_CURRENT_USER, + Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, + Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, + Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, + Flags.FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND, + Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, + Flags.FLAG_REST_TO_UNLOCK, + Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, + Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, + Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, + Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, + Flags.FLAG_SCENE_CONTAINER, + Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, + Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX, + Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX, + Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, + Flags.FLAG_SCREENSHOT_SHELF_UI2, + Flags.FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX, + Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, + Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, + Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, + Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, + Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, + Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING, + Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, + Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, + Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, + Flags.FLAG_SWITCH_USER_ON_BG, + Flags.FLAG_SYSUI_TEAMFOOD, + Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, + Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, + Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX, + Flags.FLAG_UDFPS_VIEW_PERFORMANCE, + Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, + Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, + Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI, + Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR + ); + } + + private Set mReadOnlyFlagsSet = new HashSet<>( + Arrays.asList( + "" + ) + ); +} diff --git a/flags/src/com/android/systemui/FakeFeatureFlagsImpl.java b/flags/src/com/android/systemui/FakeFeatureFlagsImpl.java new file mode 100644 index 0000000000..0054d4f3a5 --- /dev/null +++ b/flags/src/com/android/systemui/FakeFeatureFlagsImpl.java @@ -0,0 +1,49 @@ +package com.android.systemui; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +/** @hide */ +public class FakeFeatureFlagsImpl extends CustomFeatureFlags { + private final Map mFlagMap = new HashMap<>(); + private final FeatureFlags mDefaults; + + public FakeFeatureFlagsImpl() { + this(null); + } + + public FakeFeatureFlagsImpl(FeatureFlags defaults) { + super(null); + mDefaults = defaults; + // Initialize the map with null values + for (String flagName : getFlagNames()) { + mFlagMap.put(flagName, null); + } + } + + @Override + protected boolean getValue(String flagName, Predicate getter) { + Boolean value = this.mFlagMap.get(flagName); + if (value != null) { + return value; + } + if (mDefaults != null) { + return getter.test(mDefaults); + } + throw new IllegalArgumentException(flagName + " is not set"); + } + + public void setFlag(String flagName, boolean value) { + if (!this.mFlagMap.containsKey(flagName)) { + throw new IllegalArgumentException("no such flag " + flagName); + } + this.mFlagMap.put(flagName, value); + } + + public void resetAll() { + for (Map.Entry entry : mFlagMap.entrySet()) { + entry.setValue(null); + } + } +} diff --git a/flags/src/com/android/systemui/FeatureFlags.java b/flags/src/com/android/systemui/FeatureFlags.java new file mode 100644 index 0000000000..7ec6e02aed --- /dev/null +++ b/flags/src/com/android/systemui/FeatureFlags.java @@ -0,0 +1,308 @@ +package com.android.systemui; +// TODO(b/303773055): Remove the annotation after access issue is resolved. +/** @hide */ +public interface FeatureFlags { + + + boolean activityTransitionUseLargestWindow(); + + boolean ambientTouchMonitorListenToDisplayChanges(); + + boolean appClipsBacklinks(); + + boolean bindKeyguardMediaVisibility(); + + boolean bpTalkback(); + + boolean brightnessSliderFocusState(); + + boolean centralizedStatusBarHeightFix(); + + boolean clipboardNoninteractiveOnLockscreen(); + + boolean clockReactiveVariants(); + + boolean communalBouncerDoNotModifyPluginOpen(); + + boolean communalHub(); + + boolean composeBouncer(); + + boolean composeLockscreen(); + + boolean confineNotificationTouchToViewWidth(); + + boolean constraintBp(); + + boolean contextualTipsAssistantDismissFix(); + + boolean coroutineTracing(); + + boolean createWindowlessWindowMagnifier(); + + boolean dedicatedNotifInflationThread(); + + boolean delayShowMagnificationButton(); + + boolean delayedWakelockReleaseOnBackgroundThread(); + + boolean deviceEntryUdfpsRefactor(); + + boolean disableContextualTipsFrequencyCheck(); + + boolean disableContextualTipsIosSwitcherCheck(); + + boolean dozeuiSchedulingAlarmsBackgroundExecution(); + + boolean dreamInputSessionPilferOnce(); + + boolean dreamOverlayBouncerSwipeDirectionFiltering(); + + boolean dualShade(); + + boolean edgeBackGestureHandlerThread(); + + boolean edgebackGestureHandlerGetRunningTasksBackground(); + + boolean enableBackgroundKeyguardOndrawnCallback(); + + boolean enableContextualTipForMuteVolume(); + + boolean enableContextualTipForPowerOff(); + + boolean enableContextualTipForTakeScreenshot(); + + boolean enableContextualTips(); + + boolean enableEfficientDisplayRepository(); + + boolean enableLayoutTracing(); + + boolean enableViewCaptureTracing(); + + boolean enableWidgetPickerSizeFilter(); + + boolean enforceBrightnessBaseUserRestriction(); + + boolean exampleFlag(); + + boolean fastUnlockTransition(); + + boolean fixImageWallpaperCrashSurfaceAlreadyReleased(); + + boolean fixScreenshotActionDismissSystemWindows(); + + boolean floatingMenuAnimatedTuck(); + + boolean floatingMenuDragToEdit(); + + boolean floatingMenuDragToHide(); + + boolean floatingMenuImeDisplacementAnimation(); + + boolean floatingMenuNarrowTargetContentObserver(); + + boolean floatingMenuOverlapsNavBarsFlag(); + + boolean floatingMenuRadiiAnimation(); + + boolean getConnectedDeviceNameUnsynchronized(); + + boolean glanceableHubAllowKeyguardWhenDreaming(); + + boolean glanceableHubFullscreenSwipe(); + + boolean glanceableHubGestureHandle(); + + boolean glanceableHubShortcutButton(); + + boolean hapticBrightnessSlider(); + + boolean hapticVolumeSlider(); + + boolean hearingAidsQsTileDialog(); + + boolean hearingDevicesDialogRelatedTools(); + + boolean keyboardDockingIndicator(); + + boolean keyboardShortcutHelperRewrite(); + + boolean keyguardBottomAreaRefactor(); + + boolean keyguardWmStateRefactor(); + + boolean lightRevealMigration(); + + boolean mediaControlsLockscreenShadeBugFix(); + + boolean mediaControlsRefactor(); + + boolean mediaControlsUserInitiatedDeleteintent(); + + boolean migrateClocksToBlueprint(); + + boolean newAodTransition(); + + boolean newTouchpadGesturesTutorial(); + + boolean newVolumePanel(); + + boolean notificationAsyncGroupHeaderInflation(); + + boolean notificationAsyncHybridViewInflation(); + + boolean notificationAvalancheSuppression(); + + boolean notificationAvalancheThrottleHun(); + + boolean notificationBackgroundTintOptimization(); + + boolean notificationColorUpdateLogger(); + + boolean notificationContentAlphaOptimization(); + + boolean notificationFooterBackgroundTintOptimization(); + + boolean notificationMediaManagerBackgroundExecution(); + + boolean notificationMinimalismPrototype(); + + boolean notificationOverExpansionClippingFix(); + + boolean notificationPulsingFix(); + + boolean notificationRowContentBinderRefactor(); + + boolean notificationRowUserContext(); + + boolean notificationViewFlipperPausingV2(); + + boolean notificationsBackgroundIcons(); + + boolean notificationsFooterViewRefactor(); + + boolean notificationsHeadsUpRefactor(); + + boolean notificationsHideOnDisplaySwitch(); + + boolean notificationsIconContainerRefactor(); + + boolean notificationsImprovedHunAnimation(); + + boolean notificationsLiveDataStoreRefactor(); + + boolean notifyPowerManagerUserActivityBackground(); + + boolean pinInputFieldStyledFocusState(); + + boolean predictiveBackAnimateBouncer(); + + boolean predictiveBackAnimateDialogs(); + + boolean predictiveBackAnimateShade(); + + boolean predictiveBackSysui(); + + boolean priorityPeopleSection(); + + boolean privacyDotUnfoldWrongCornerFix(); + + boolean pssAppSelectorAbruptExitFix(); + + boolean pssAppSelectorRecentsSplitScreen(); + + boolean pssTaskSwitcher(); + + boolean qsCustomTileClickGuaranteedBugFix(); + + boolean qsNewPipeline(); + + boolean qsNewTiles(); + + boolean qsNewTilesFuture(); + + boolean qsTileFocusState(); + + boolean qsUiRefactor(); + + boolean quickSettingsVisualHapticsLongpress(); + + boolean recordIssueQsTile(); + + boolean refactorGetCurrentUser(); + + boolean registerBatteryControllerReceiversInCorestartable(); + + boolean registerNewWalletCardInBackground(); + + boolean registerWallpaperNotifierBackground(); + + boolean registerZenModeContentObserverBackground(); + + boolean removeDreamOverlayHideOnTouch(); + + boolean restToUnlock(); + + boolean restartDreamOnUnocclude(); + + boolean revampedBouncerMessages(); + + boolean runFingerprintDetectOnDismissibleKeyguard(); + + boolean saveAndRestoreMagnificationSettingsButtons(); + + boolean sceneContainer(); + + boolean screenshareNotificationHidingBugFix(); + + boolean screenshotActionDismissSystemWindows(); + + boolean screenshotPrivateProfileAccessibilityAnnouncementFix(); + + boolean screenshotPrivateProfileBehaviorFix(); + + boolean screenshotScrollCropViewCrashFix(); + + boolean screenshotShelfUi2(); + + boolean shadeCollapseActivityLaunchFix(); + + boolean shaderlibLoadingEffectRefactor(); + + boolean sliceBroadcastRelayInBackground(); + + boolean sliceManagerBinderCallBackground(); + + boolean smartspaceLockscreenViewmodel(); + + boolean smartspaceRelocateToBottom(); + + boolean smartspaceRemoteviewsRendering(); + + boolean statusBarMonochromeIconsFix(); + + boolean statusBarScreenSharingChips(); + + boolean statusBarStaticInoutIndicators(); + + boolean switchUserOnBg(); + + boolean sysuiTeamfood(); + + boolean themeOverlayControllerWakefulnessDeprecation(); + + boolean translucentOccludingActivityFix(); + + boolean truncatedStatusBarIconsFix(); + + boolean udfpsViewPerformance(); + + boolean unfoldAnimationBackgroundProgress(); + + boolean updateUserSwitcherBackground(); + + boolean validateKeyboardShortcutHelperIconUri(); + + boolean visualInterruptionsRefactor(); +} diff --git a/flags/src/com/android/systemui/FeatureFlagsImpl.java b/flags/src/com/android/systemui/FeatureFlagsImpl.java new file mode 100644 index 0000000000..258a6ba5fe --- /dev/null +++ b/flags/src/com/android/systemui/FeatureFlagsImpl.java @@ -0,0 +1,3157 @@ +package com.android.systemui; +// TODO(b/303773055): Remove the annotation after access issue is resolved. +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import java.nio.file.Files; +import java.nio.file.Paths; +/** @hide */ +public final class FeatureFlagsImpl implements FeatureFlags { + private static final boolean isReadFromNew = Files.exists(Paths.get("/metadata/aconfig/boot/enable_only_new_storage")); + private static volatile boolean isCached = false; + private static volatile boolean accessibility_is_cached = false; + private static volatile boolean biometrics_framework_is_cached = false; + private static volatile boolean communal_is_cached = false; + private static volatile boolean systemui_is_cached = false; + private static boolean activityTransitionUseLargestWindow = true; + private static boolean ambientTouchMonitorListenToDisplayChanges = false; + private static boolean appClipsBacklinks = false; + private static boolean bindKeyguardMediaVisibility = true; + private static boolean bpTalkback = true; + private static boolean brightnessSliderFocusState = false; + private static boolean centralizedStatusBarHeightFix = true; + private static boolean clipboardNoninteractiveOnLockscreen = false; + private static boolean clockReactiveVariants = false; + private static boolean communalBouncerDoNotModifyPluginOpen = false; + private static boolean communalHub = true; + private static boolean composeBouncer = false; + private static boolean composeLockscreen = false; + private static boolean confineNotificationTouchToViewWidth = true; + private static boolean constraintBp = true; + private static boolean contextualTipsAssistantDismissFix = true; + private static boolean coroutineTracing = true; + private static boolean createWindowlessWindowMagnifier = true; + private static boolean dedicatedNotifInflationThread = true; + private static boolean delayShowMagnificationButton = true; + private static boolean delayedWakelockReleaseOnBackgroundThread = true; + private static boolean deviceEntryUdfpsRefactor = true; + private static boolean disableContextualTipsFrequencyCheck = true; + private static boolean disableContextualTipsIosSwitcherCheck = true; + private static boolean dozeuiSchedulingAlarmsBackgroundExecution = false; + private static boolean dreamInputSessionPilferOnce = false; + private static boolean dreamOverlayBouncerSwipeDirectionFiltering = true; + private static boolean dualShade = false; + private static boolean edgeBackGestureHandlerThread = false; + private static boolean edgebackGestureHandlerGetRunningTasksBackground = true; + private static boolean enableBackgroundKeyguardOndrawnCallback = true; + private static boolean enableContextualTipForMuteVolume = false; + private static boolean enableContextualTipForPowerOff = true; + private static boolean enableContextualTipForTakeScreenshot = true; + private static boolean enableContextualTips = true; + private static boolean enableEfficientDisplayRepository = false; + private static boolean enableLayoutTracing = false; + private static boolean enableViewCaptureTracing = false; + private static boolean enableWidgetPickerSizeFilter = false; + private static boolean enforceBrightnessBaseUserRestriction = true; + private static boolean exampleFlag = false; + private static boolean fastUnlockTransition = false; + private static boolean fixImageWallpaperCrashSurfaceAlreadyReleased = true; + private static boolean fixScreenshotActionDismissSystemWindows = true; + private static boolean floatingMenuAnimatedTuck = true; + private static boolean floatingMenuDragToEdit = true; + private static boolean floatingMenuDragToHide = false; + private static boolean floatingMenuImeDisplacementAnimation = true; + private static boolean floatingMenuNarrowTargetContentObserver = true; + private static boolean floatingMenuOverlapsNavBarsFlag = true; + private static boolean floatingMenuRadiiAnimation = true; + private static boolean getConnectedDeviceNameUnsynchronized = true; + private static boolean glanceableHubAllowKeyguardWhenDreaming = false; + private static boolean glanceableHubFullscreenSwipe = false; + private static boolean glanceableHubGestureHandle = false; + private static boolean glanceableHubShortcutButton = false; + private static boolean hapticBrightnessSlider = true; + private static boolean hapticVolumeSlider = true; + private static boolean hearingAidsQsTileDialog = true; + private static boolean hearingDevicesDialogRelatedTools = true; + private static boolean keyboardDockingIndicator = true; + private static boolean keyboardShortcutHelperRewrite = false; + private static boolean keyguardBottomAreaRefactor = true; + private static boolean keyguardWmStateRefactor = false; + private static boolean lightRevealMigration = true; + private static boolean mediaControlsLockscreenShadeBugFix = true; + private static boolean mediaControlsRefactor = true; + private static boolean mediaControlsUserInitiatedDeleteintent = true; + private static boolean migrateClocksToBlueprint = true; + private static boolean newAodTransition = true; + private static boolean newTouchpadGesturesTutorial = false; + private static boolean newVolumePanel = true; + private static boolean notificationAsyncGroupHeaderInflation = true; + private static boolean notificationAsyncHybridViewInflation = true; + private static boolean notificationAvalancheSuppression = true; + private static boolean notificationAvalancheThrottleHun = true; + private static boolean notificationBackgroundTintOptimization = true; + private static boolean notificationColorUpdateLogger = false; + private static boolean notificationContentAlphaOptimization = true; + private static boolean notificationFooterBackgroundTintOptimization = false; + private static boolean notificationMediaManagerBackgroundExecution = true; + private static boolean notificationMinimalismPrototype = false; + private static boolean notificationOverExpansionClippingFix = true; + private static boolean notificationPulsingFix = true; + private static boolean notificationRowContentBinderRefactor = false; + private static boolean notificationRowUserContext = true; + private static boolean notificationViewFlipperPausingV2 = true; + private static boolean notificationsBackgroundIcons = false; + private static boolean notificationsFooterViewRefactor = true; + private static boolean notificationsHeadsUpRefactor = true; + private static boolean notificationsHideOnDisplaySwitch = false; + private static boolean notificationsIconContainerRefactor = true; + private static boolean notificationsImprovedHunAnimation = true; + private static boolean notificationsLiveDataStoreRefactor = true; + private static boolean notifyPowerManagerUserActivityBackground = true; + private static boolean pinInputFieldStyledFocusState = true; + private static boolean predictiveBackAnimateBouncer = true; + private static boolean predictiveBackAnimateDialogs = true; + private static boolean predictiveBackAnimateShade = false; + private static boolean predictiveBackSysui = true; + private static boolean priorityPeopleSection = true; + private static boolean privacyDotUnfoldWrongCornerFix = true; + private static boolean pssAppSelectorAbruptExitFix = true; + private static boolean pssAppSelectorRecentsSplitScreen = true; + private static boolean pssTaskSwitcher = false; + private static boolean qsCustomTileClickGuaranteedBugFix = true; + private static boolean qsNewPipeline = true; + private static boolean qsNewTiles = false; + private static boolean qsNewTilesFuture = false; + private static boolean qsTileFocusState = true; + private static boolean qsUiRefactor = false; + private static boolean quickSettingsVisualHapticsLongpress = true; + private static boolean recordIssueQsTile = true; + private static boolean refactorGetCurrentUser = true; + private static boolean registerBatteryControllerReceiversInCorestartable = false; + private static boolean registerNewWalletCardInBackground = true; + private static boolean registerWallpaperNotifierBackground = true; + private static boolean registerZenModeContentObserverBackground = true; + private static boolean removeDreamOverlayHideOnTouch = true; + private static boolean restToUnlock = false; + private static boolean restartDreamOnUnocclude = false; + private static boolean revampedBouncerMessages = true; + private static boolean runFingerprintDetectOnDismissibleKeyguard = true; + private static boolean saveAndRestoreMagnificationSettingsButtons = false; + private static boolean sceneContainer = false; + private static boolean screenshareNotificationHidingBugFix = true; + private static boolean screenshotActionDismissSystemWindows = true; + private static boolean screenshotPrivateProfileAccessibilityAnnouncementFix = true; + private static boolean screenshotPrivateProfileBehaviorFix = true; + private static boolean screenshotScrollCropViewCrashFix = true; + private static boolean screenshotShelfUi2 = true; + private static boolean shadeCollapseActivityLaunchFix = false; + private static boolean shaderlibLoadingEffectRefactor = true; + private static boolean sliceBroadcastRelayInBackground = true; + private static boolean sliceManagerBinderCallBackground = true; + private static boolean smartspaceLockscreenViewmodel = true; + private static boolean smartspaceRelocateToBottom = false; + private static boolean smartspaceRemoteviewsRendering = false; + private static boolean statusBarMonochromeIconsFix = true; + private static boolean statusBarScreenSharingChips = true; + private static boolean statusBarStaticInoutIndicators = false; + private static boolean switchUserOnBg = true; + private static boolean sysuiTeamfood = true; + private static boolean themeOverlayControllerWakefulnessDeprecation = false; + private static boolean translucentOccludingActivityFix = false; + private static boolean truncatedStatusBarIconsFix = true; + private static boolean udfpsViewPerformance = true; + private static boolean unfoldAnimationBackgroundProgress = true; + private static boolean updateUserSwitcherBackground = true; + private static boolean validateKeyboardShortcutHelperIconUri = true; + private static boolean visualInterruptionsRefactor = true; + + + private void init() { + boolean foundPackage = true; + + createWindowlessWindowMagnifier = foundPackage; + + + delayShowMagnificationButton = foundPackage; + + + floatingMenuAnimatedTuck = foundPackage; + + + floatingMenuDragToEdit = foundPackage; + + + floatingMenuDragToHide = foundPackage; + + + floatingMenuImeDisplacementAnimation = foundPackage; + + + floatingMenuNarrowTargetContentObserver = foundPackage; + + + floatingMenuOverlapsNavBarsFlag = foundPackage; + + + floatingMenuRadiiAnimation = foundPackage; + + + hearingDevicesDialogRelatedTools = foundPackage; + + saveAndRestoreMagnificationSettingsButtons = foundPackage; + bpTalkback = foundPackage; + constraintBp = foundPackage; + communalHub = foundPackage; + enableWidgetPickerSizeFilter = foundPackage; + activityTransitionUseLargestWindow = foundPackage; + ambientTouchMonitorListenToDisplayChanges = foundPackage; + appClipsBacklinks = foundPackage; + bindKeyguardMediaVisibility = foundPackage; + brightnessSliderFocusState = foundPackage; + centralizedStatusBarHeightFix = foundPackage; + clipboardNoninteractiveOnLockscreen = foundPackage; + clockReactiveVariants = foundPackage; + communalBouncerDoNotModifyPluginOpen = foundPackage; + composeBouncer = foundPackage; + composeLockscreen = foundPackage; + confineNotificationTouchToViewWidth = foundPackage; + contextualTipsAssistantDismissFix = foundPackage; + coroutineTracing = foundPackage; + dedicatedNotifInflationThread = foundPackage; + delayedWakelockReleaseOnBackgroundThread = foundPackage; + deviceEntryUdfpsRefactor = foundPackage; + disableContextualTipsFrequencyCheck = foundPackage; + disableContextualTipsIosSwitcherCheck = foundPackage; + dozeuiSchedulingAlarmsBackgroundExecution = foundPackage; + dreamInputSessionPilferOnce = foundPackage; + dreamOverlayBouncerSwipeDirectionFiltering = foundPackage; + dualShade = foundPackage; + edgeBackGestureHandlerThread = foundPackage; + edgebackGestureHandlerGetRunningTasksBackground = foundPackage; + enableBackgroundKeyguardOndrawnCallback = foundPackage; + enableContextualTipForMuteVolume = foundPackage; + enableContextualTipForPowerOff = foundPackage; + enableContextualTipForTakeScreenshot = foundPackage; + enableContextualTips = foundPackage; + enableEfficientDisplayRepository = foundPackage; + enableLayoutTracing = foundPackage; + enableViewCaptureTracing = foundPackage; + enforceBrightnessBaseUserRestriction = foundPackage; + exampleFlag = foundPackage; + fastUnlockTransition = foundPackage; + fixImageWallpaperCrashSurfaceAlreadyReleased = foundPackage; + fixScreenshotActionDismissSystemWindows = foundPackage; + getConnectedDeviceNameUnsynchronized = foundPackage; + glanceableHubAllowKeyguardWhenDreaming = foundPackage; + glanceableHubFullscreenSwipe = foundPackage; + glanceableHubGestureHandle = foundPackage; + glanceableHubShortcutButton = foundPackage; + hapticBrightnessSlider = foundPackage; + hapticVolumeSlider = foundPackage; + hearingAidsQsTileDialog = foundPackage; + keyboardDockingIndicator = foundPackage; + keyboardShortcutHelperRewrite = foundPackage; + keyguardBottomAreaRefactor = foundPackage; + keyguardWmStateRefactor = foundPackage; + lightRevealMigration = foundPackage; + mediaControlsLockscreenShadeBugFix = foundPackage; + mediaControlsRefactor = foundPackage; + mediaControlsUserInitiatedDeleteintent = foundPackage; + migrateClocksToBlueprint = foundPackage; + newAodTransition = foundPackage; + newTouchpadGesturesTutorial = foundPackage; + newVolumePanel = foundPackage; + notificationAsyncGroupHeaderInflation = foundPackage; + notificationAsyncHybridViewInflation = foundPackage; + notificationAvalancheSuppression = foundPackage; + notificationAvalancheThrottleHun = foundPackage; + notificationBackgroundTintOptimization = foundPackage; + notificationColorUpdateLogger = foundPackage; + notificationContentAlphaOptimization = foundPackage; + notificationFooterBackgroundTintOptimization = foundPackage; + notificationMediaManagerBackgroundExecution = foundPackage; + notificationMinimalismPrototype = foundPackage; + notificationOverExpansionClippingFix = foundPackage; + notificationPulsingFix = foundPackage; + notificationRowContentBinderRefactor = foundPackage; + notificationRowUserContext = foundPackage; + notificationViewFlipperPausingV2 = foundPackage; + notificationsBackgroundIcons = foundPackage; + notificationsFooterViewRefactor = foundPackage; + notificationsHeadsUpRefactor = foundPackage; + notificationsHideOnDisplaySwitch = foundPackage; + notificationsIconContainerRefactor = foundPackage; + notificationsImprovedHunAnimation = foundPackage; + notificationsLiveDataStoreRefactor = foundPackage; + notifyPowerManagerUserActivityBackground = foundPackage; + pinInputFieldStyledFocusState = foundPackage; + predictiveBackAnimateBouncer = foundPackage; + predictiveBackAnimateDialogs = foundPackage; + predictiveBackAnimateShade = foundPackage; + predictiveBackSysui = foundPackage; + priorityPeopleSection = foundPackage; + privacyDotUnfoldWrongCornerFix = foundPackage; + pssAppSelectorAbruptExitFix = foundPackage; + pssAppSelectorRecentsSplitScreen = foundPackage; + pssTaskSwitcher = foundPackage; + qsCustomTileClickGuaranteedBugFix = foundPackage; + qsNewPipeline = foundPackage; + qsNewTiles = foundPackage; + qsNewTilesFuture = foundPackage; + qsTileFocusState = foundPackage; + qsUiRefactor = foundPackage; + quickSettingsVisualHapticsLongpress = foundPackage; + recordIssueQsTile = foundPackage; + refactorGetCurrentUser = foundPackage; + registerBatteryControllerReceiversInCorestartable = foundPackage; + registerNewWalletCardInBackground = foundPackage; + registerWallpaperNotifierBackground = foundPackage; + registerZenModeContentObserverBackground = foundPackage; + removeDreamOverlayHideOnTouch = foundPackage; + restToUnlock = foundPackage; + restartDreamOnUnocclude = foundPackage; + revampedBouncerMessages = foundPackage; + runFingerprintDetectOnDismissibleKeyguard = foundPackage; + sceneContainer = foundPackage; + screenshareNotificationHidingBugFix = foundPackage; + screenshotActionDismissSystemWindows = foundPackage; + screenshotPrivateProfileAccessibilityAnnouncementFix = foundPackage; + screenshotPrivateProfileBehaviorFix = foundPackage; + screenshotScrollCropViewCrashFix = foundPackage; + screenshotShelfUi2 = foundPackage; + shadeCollapseActivityLaunchFix = foundPackage; + shaderlibLoadingEffectRefactor = foundPackage; + sliceBroadcastRelayInBackground = foundPackage; + sliceManagerBinderCallBackground = foundPackage; + smartspaceLockscreenViewmodel = foundPackage; + smartspaceRelocateToBottom = foundPackage; + smartspaceRemoteviewsRendering = foundPackage; + + + statusBarMonochromeIconsFix = foundPackage; + + + statusBarScreenSharingChips = foundPackage; + + + statusBarStaticInoutIndicators = foundPackage; + + + switchUserOnBg = foundPackage; + + + sysuiTeamfood = foundPackage; + + + themeOverlayControllerWakefulnessDeprecation = foundPackage; + + + translucentOccludingActivityFix = foundPackage; + + + truncatedStatusBarIconsFix = foundPackage; + + + udfpsViewPerformance = foundPackage; + + + unfoldAnimationBackgroundProgress = foundPackage; + + + updateUserSwitcherBackground = foundPackage; + + + validateKeyboardShortcutHelperIconUri = foundPackage; + + + visualInterruptionsRefactor = foundPackage; + + isCached = true; + } + + + + + private void load_overrides_accessibility() { + try { + Properties properties = DeviceConfig.getProperties("accessibility"); + createWindowlessWindowMagnifier = + properties.getBoolean(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, true); + delayShowMagnificationButton = + properties.getBoolean(Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, true); + floatingMenuAnimatedTuck = + properties.getBoolean(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, true); + floatingMenuDragToEdit = + properties.getBoolean(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, true); + floatingMenuDragToHide = + properties.getBoolean(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, false); + floatingMenuImeDisplacementAnimation = + properties.getBoolean(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, true); + floatingMenuNarrowTargetContentObserver = + properties.getBoolean(Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, true); + floatingMenuOverlapsNavBarsFlag = + properties.getBoolean(Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, true); + floatingMenuRadiiAnimation = + properties.getBoolean(Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, true); + hearingDevicesDialogRelatedTools = + properties.getBoolean(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, true); + saveAndRestoreMagnificationSettingsButtons = + properties.getBoolean(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, false); + } catch (NullPointerException e) { + throw new RuntimeException( + "Cannot read value from namespace accessibility " + + "from DeviceConfig. It could be that the code using flag " + + "executed before SettingsProvider initialization. Please use " + + "fixed read-only flag by adding is_fixed_read_only: true in " + + "flag declaration.", + e + ); + } + accessibility_is_cached = true; + } + + private void load_overrides_biometrics_framework() { + try { + Properties properties = DeviceConfig.getProperties("biometrics_framework"); + bpTalkback = + properties.getBoolean(Flags.FLAG_BP_TALKBACK, true); + constraintBp = + properties.getBoolean(Flags.FLAG_CONSTRAINT_BP, true); + } catch (NullPointerException e) { + throw new RuntimeException( + "Cannot read value from namespace biometrics_framework " + + "from DeviceConfig. It could be that the code using flag " + + "executed before SettingsProvider initialization. Please use " + + "fixed read-only flag by adding is_fixed_read_only: true in " + + "flag declaration.", + e + ); + } + biometrics_framework_is_cached = true; + } + + private void load_overrides_communal() { + try { + Properties properties = DeviceConfig.getProperties("communal"); + communalHub = + properties.getBoolean(Flags.FLAG_COMMUNAL_HUB, true); + enableWidgetPickerSizeFilter = + properties.getBoolean(Flags.FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER, false); + } catch (NullPointerException e) { + throw new RuntimeException( + "Cannot read value from namespace communal " + + "from DeviceConfig. It could be that the code using flag " + + "executed before SettingsProvider initialization. Please use " + + "fixed read-only flag by adding is_fixed_read_only: true in " + + "flag declaration.", + e + ); + } + communal_is_cached = true; + } + + private void load_overrides_systemui() { + try { + Properties properties = DeviceConfig.getProperties("systemui"); + activityTransitionUseLargestWindow = + properties.getBoolean(Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, true); + ambientTouchMonitorListenToDisplayChanges = + properties.getBoolean(Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, false); + appClipsBacklinks = + properties.getBoolean(Flags.FLAG_APP_CLIPS_BACKLINKS, false); + bindKeyguardMediaVisibility = + properties.getBoolean(Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, true); + brightnessSliderFocusState = + properties.getBoolean(Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, false); + centralizedStatusBarHeightFix = + properties.getBoolean(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX, true); + clipboardNoninteractiveOnLockscreen = + properties.getBoolean(Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, false); + clockReactiveVariants = + properties.getBoolean(Flags.FLAG_CLOCK_REACTIVE_VARIANTS, false); + communalBouncerDoNotModifyPluginOpen = + properties.getBoolean(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, false); + composeBouncer = + properties.getBoolean(Flags.FLAG_COMPOSE_BOUNCER, false); + composeLockscreen = + properties.getBoolean(Flags.FLAG_COMPOSE_LOCKSCREEN, false); + confineNotificationTouchToViewWidth = + properties.getBoolean(Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, true); + contextualTipsAssistantDismissFix = + properties.getBoolean(Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, true); + coroutineTracing = + properties.getBoolean(Flags.FLAG_COROUTINE_TRACING, true); + dedicatedNotifInflationThread = + properties.getBoolean(Flags.FLAG_DEDICATED_NOTIF_INFLATION_THREAD, true); + delayedWakelockReleaseOnBackgroundThread = + properties.getBoolean(Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD, true); + deviceEntryUdfpsRefactor = + properties.getBoolean(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, true); + disableContextualTipsFrequencyCheck = + properties.getBoolean(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, true); + disableContextualTipsIosSwitcherCheck = + properties.getBoolean(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, true); + dozeuiSchedulingAlarmsBackgroundExecution = + properties.getBoolean(Flags.FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION, false); + dreamInputSessionPilferOnce = + properties.getBoolean(Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, false); + dreamOverlayBouncerSwipeDirectionFiltering = + properties.getBoolean(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, true); + dualShade = + properties.getBoolean(Flags.FLAG_DUAL_SHADE, false); + edgeBackGestureHandlerThread = + properties.getBoolean(Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, false); + edgebackGestureHandlerGetRunningTasksBackground = + properties.getBoolean(Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, true); + enableBackgroundKeyguardOndrawnCallback = + properties.getBoolean(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, true); + enableContextualTipForMuteVolume = + properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, false); + enableContextualTipForPowerOff = + properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, true); + enableContextualTipForTakeScreenshot = + properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, true); + enableContextualTips = + properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, true); + enableEfficientDisplayRepository = + properties.getBoolean(Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, false); + enableLayoutTracing = + properties.getBoolean(Flags.FLAG_ENABLE_LAYOUT_TRACING, false); + enableViewCaptureTracing = + properties.getBoolean(Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, false); + enforceBrightnessBaseUserRestriction = + properties.getBoolean(Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, true); + exampleFlag = + properties.getBoolean(Flags.FLAG_EXAMPLE_FLAG, false); + fastUnlockTransition = + properties.getBoolean(Flags.FLAG_FAST_UNLOCK_TRANSITION, false); + fixImageWallpaperCrashSurfaceAlreadyReleased = + properties.getBoolean(Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, true); + fixScreenshotActionDismissSystemWindows = + properties.getBoolean(Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, true); + getConnectedDeviceNameUnsynchronized = + properties.getBoolean(Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, true); + glanceableHubAllowKeyguardWhenDreaming = + properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, false); + glanceableHubFullscreenSwipe = + properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE, false); + glanceableHubGestureHandle = + properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_GESTURE_HANDLE, false); + glanceableHubShortcutButton = + properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON, false); + hapticBrightnessSlider = + properties.getBoolean(Flags.FLAG_HAPTIC_BRIGHTNESS_SLIDER, true); + hapticVolumeSlider = + properties.getBoolean(Flags.FLAG_HAPTIC_VOLUME_SLIDER, true); + hearingAidsQsTileDialog = + properties.getBoolean(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, true); + keyboardDockingIndicator = + properties.getBoolean(Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, true); + keyboardShortcutHelperRewrite = + properties.getBoolean(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, false); + keyguardBottomAreaRefactor = + properties.getBoolean(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, true); + keyguardWmStateRefactor = + properties.getBoolean(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, false); + lightRevealMigration = + properties.getBoolean(Flags.FLAG_LIGHT_REVEAL_MIGRATION, true); + mediaControlsLockscreenShadeBugFix = + properties.getBoolean(Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, true); + mediaControlsRefactor = + properties.getBoolean(Flags.FLAG_MEDIA_CONTROLS_REFACTOR, true); + mediaControlsUserInitiatedDeleteintent = + properties.getBoolean(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, true); + migrateClocksToBlueprint = + properties.getBoolean(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, true); + newAodTransition = + properties.getBoolean(Flags.FLAG_NEW_AOD_TRANSITION, true); + newTouchpadGesturesTutorial = + properties.getBoolean(Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, false); + newVolumePanel = + properties.getBoolean(Flags.FLAG_NEW_VOLUME_PANEL, true); + notificationAsyncGroupHeaderInflation = + properties.getBoolean(Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, true); + notificationAsyncHybridViewInflation = + properties.getBoolean(Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, true); + notificationAvalancheSuppression = + properties.getBoolean(Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, true); + notificationAvalancheThrottleHun = + properties.getBoolean(Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, true); + notificationBackgroundTintOptimization = + properties.getBoolean(Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, true); + notificationColorUpdateLogger = + properties.getBoolean(Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, false); + notificationContentAlphaOptimization = + properties.getBoolean(Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, true); + notificationFooterBackgroundTintOptimization = + properties.getBoolean(Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, false); + notificationMediaManagerBackgroundExecution = + properties.getBoolean(Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION, true); + notificationMinimalismPrototype = + properties.getBoolean(Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE, false); + notificationOverExpansionClippingFix = + properties.getBoolean(Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, true); + notificationPulsingFix = + properties.getBoolean(Flags.FLAG_NOTIFICATION_PULSING_FIX, true); + notificationRowContentBinderRefactor = + properties.getBoolean(Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, false); + notificationRowUserContext = + properties.getBoolean(Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, true); + notificationViewFlipperPausingV2 = + properties.getBoolean(Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, true); + notificationsBackgroundIcons = + properties.getBoolean(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, false); + notificationsFooterViewRefactor = + properties.getBoolean(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR, true); + notificationsHeadsUpRefactor = + properties.getBoolean(Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR, true); + notificationsHideOnDisplaySwitch = + properties.getBoolean(Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, false); + notificationsIconContainerRefactor = + properties.getBoolean(Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, true); + notificationsImprovedHunAnimation = + properties.getBoolean(Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION, true); + notificationsLiveDataStoreRefactor = + properties.getBoolean(Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, true); + notifyPowerManagerUserActivityBackground = + properties.getBoolean(Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, true); + pinInputFieldStyledFocusState = + properties.getBoolean(Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, true); + predictiveBackAnimateBouncer = + properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER, true); + predictiveBackAnimateDialogs = + properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS, true); + predictiveBackAnimateShade = + properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, false); + predictiveBackSysui = + properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_SYSUI, true); + priorityPeopleSection = + properties.getBoolean(Flags.FLAG_PRIORITY_PEOPLE_SECTION, true); + privacyDotUnfoldWrongCornerFix = + properties.getBoolean(Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX, true); + pssAppSelectorAbruptExitFix = + properties.getBoolean(Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX, true); + pssAppSelectorRecentsSplitScreen = + properties.getBoolean(Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, true); + pssTaskSwitcher = + properties.getBoolean(Flags.FLAG_PSS_TASK_SWITCHER, false); + qsCustomTileClickGuaranteedBugFix = + properties.getBoolean(Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, true); + qsNewPipeline = + properties.getBoolean(Flags.FLAG_QS_NEW_PIPELINE, true); + qsNewTiles = + properties.getBoolean(Flags.FLAG_QS_NEW_TILES, false); + qsNewTilesFuture = + properties.getBoolean(Flags.FLAG_QS_NEW_TILES_FUTURE, false); + qsTileFocusState = + properties.getBoolean(Flags.FLAG_QS_TILE_FOCUS_STATE, true); + qsUiRefactor = + properties.getBoolean(Flags.FLAG_QS_UI_REFACTOR, false); + quickSettingsVisualHapticsLongpress = + properties.getBoolean(Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS, true); + recordIssueQsTile = + properties.getBoolean(Flags.FLAG_RECORD_ISSUE_QS_TILE, true); + refactorGetCurrentUser = + properties.getBoolean(Flags.FLAG_REFACTOR_GET_CURRENT_USER, true); + registerBatteryControllerReceiversInCorestartable = + properties.getBoolean(Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, false); + registerNewWalletCardInBackground = + properties.getBoolean(Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, true); + registerWallpaperNotifierBackground = + properties.getBoolean(Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, true); + registerZenModeContentObserverBackground = + properties.getBoolean(Flags.FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND, true); + removeDreamOverlayHideOnTouch = + properties.getBoolean(Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, true); + restToUnlock = + properties.getBoolean(Flags.FLAG_REST_TO_UNLOCK, false); + restartDreamOnUnocclude = + properties.getBoolean(Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, false); + revampedBouncerMessages = + properties.getBoolean(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, true); + runFingerprintDetectOnDismissibleKeyguard = + properties.getBoolean(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, true); + sceneContainer = + properties.getBoolean(Flags.FLAG_SCENE_CONTAINER, false); + screenshareNotificationHidingBugFix = + properties.getBoolean(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, true); + screenshotActionDismissSystemWindows = + properties.getBoolean(Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, true); + screenshotPrivateProfileAccessibilityAnnouncementFix = + properties.getBoolean(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX, true); + screenshotPrivateProfileBehaviorFix = + properties.getBoolean(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX, true); + screenshotScrollCropViewCrashFix = + properties.getBoolean(Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, true); + screenshotShelfUi2 = + properties.getBoolean(Flags.FLAG_SCREENSHOT_SHELF_UI2, true); + shadeCollapseActivityLaunchFix = + properties.getBoolean(Flags.FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX, false); + shaderlibLoadingEffectRefactor = + properties.getBoolean(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, true); + sliceBroadcastRelayInBackground = + properties.getBoolean(Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, true); + sliceManagerBinderCallBackground = + properties.getBoolean(Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, true); + smartspaceLockscreenViewmodel = + properties.getBoolean(Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, true); + smartspaceRelocateToBottom = + properties.getBoolean(Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, false); + smartspaceRemoteviewsRendering = + properties.getBoolean(Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING, false); + statusBarMonochromeIconsFix = + properties.getBoolean(Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, true); + statusBarScreenSharingChips = + properties.getBoolean(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, true); + statusBarStaticInoutIndicators = + properties.getBoolean(Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, false); + switchUserOnBg = + properties.getBoolean(Flags.FLAG_SWITCH_USER_ON_BG, true); + sysuiTeamfood = + properties.getBoolean(Flags.FLAG_SYSUI_TEAMFOOD, true); + themeOverlayControllerWakefulnessDeprecation = + properties.getBoolean(Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, false); + translucentOccludingActivityFix = + properties.getBoolean(Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, false); + truncatedStatusBarIconsFix = + properties.getBoolean(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX, true); + udfpsViewPerformance = + properties.getBoolean(Flags.FLAG_UDFPS_VIEW_PERFORMANCE, true); + unfoldAnimationBackgroundProgress = + properties.getBoolean(Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, true); + updateUserSwitcherBackground = + properties.getBoolean(Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, true); + validateKeyboardShortcutHelperIconUri = + properties.getBoolean(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI, true); + visualInterruptionsRefactor = + properties.getBoolean(Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR, true); + } catch (NullPointerException e) { + throw new RuntimeException( + "Cannot read value from namespace systemui " + + "from DeviceConfig. It could be that the code using flag " + + "executed before SettingsProvider initialization. Please use " + + "fixed read-only flag by adding is_fixed_read_only: true in " + + "flag declaration.", + e + ); + } + systemui_is_cached = true; + } + + @Override + + public boolean activityTransitionUseLargestWindow() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return activityTransitionUseLargestWindow; + + } + + @Override + + public boolean ambientTouchMonitorListenToDisplayChanges() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return ambientTouchMonitorListenToDisplayChanges; + + } + + @Override + + public boolean appClipsBacklinks() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return appClipsBacklinks; + + } + + @Override + + public boolean bindKeyguardMediaVisibility() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return bindKeyguardMediaVisibility; + + } + + @Override + + public boolean bpTalkback() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!biometrics_framework_is_cached) { + load_overrides_biometrics_framework(); + } + } + return bpTalkback; + + } + + @Override + + public boolean brightnessSliderFocusState() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return brightnessSliderFocusState; + + } + + @Override + + public boolean centralizedStatusBarHeightFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return centralizedStatusBarHeightFix; + + } + + @Override + + public boolean clipboardNoninteractiveOnLockscreen() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return clipboardNoninteractiveOnLockscreen; + + } + + @Override + + public boolean clockReactiveVariants() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return clockReactiveVariants; + + } + + @Override + + public boolean communalBouncerDoNotModifyPluginOpen() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return communalBouncerDoNotModifyPluginOpen; + + } + + @Override + + public boolean communalHub() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!communal_is_cached) { + load_overrides_communal(); + } + } + return communalHub; + + } + + @Override + + public boolean composeBouncer() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return composeBouncer; + + } + + @Override + + public boolean composeLockscreen() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return composeLockscreen; + + } + + @Override + + public boolean confineNotificationTouchToViewWidth() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return confineNotificationTouchToViewWidth; + + } + + @Override + + public boolean constraintBp() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!biometrics_framework_is_cached) { + load_overrides_biometrics_framework(); + } + } + return constraintBp; + + } + + @Override + + public boolean contextualTipsAssistantDismissFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return contextualTipsAssistantDismissFix; + + } + + @Override + + public boolean coroutineTracing() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return coroutineTracing; + + } + + @Override + + public boolean createWindowlessWindowMagnifier() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return createWindowlessWindowMagnifier; + + } + + @Override + + public boolean dedicatedNotifInflationThread() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return dedicatedNotifInflationThread; + + } + + @Override + + public boolean delayShowMagnificationButton() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return delayShowMagnificationButton; + + } + + @Override + + public boolean delayedWakelockReleaseOnBackgroundThread() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return delayedWakelockReleaseOnBackgroundThread; + + } + + @Override + + public boolean deviceEntryUdfpsRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return deviceEntryUdfpsRefactor; + + } + + @Override + + public boolean disableContextualTipsFrequencyCheck() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return disableContextualTipsFrequencyCheck; + + } + + @Override + + public boolean disableContextualTipsIosSwitcherCheck() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return disableContextualTipsIosSwitcherCheck; + + } + + @Override + + public boolean dozeuiSchedulingAlarmsBackgroundExecution() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return dozeuiSchedulingAlarmsBackgroundExecution; + + } + + @Override + + public boolean dreamInputSessionPilferOnce() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return dreamInputSessionPilferOnce; + + } + + @Override + + public boolean dreamOverlayBouncerSwipeDirectionFiltering() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return dreamOverlayBouncerSwipeDirectionFiltering; + + } + + @Override + + public boolean dualShade() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return dualShade; + + } + + @Override + + public boolean edgeBackGestureHandlerThread() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return edgeBackGestureHandlerThread; + + } + + @Override + + public boolean edgebackGestureHandlerGetRunningTasksBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return edgebackGestureHandlerGetRunningTasksBackground; + + } + + @Override + + public boolean enableBackgroundKeyguardOndrawnCallback() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableBackgroundKeyguardOndrawnCallback; + + } + + @Override + + public boolean enableContextualTipForMuteVolume() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableContextualTipForMuteVolume; + + } + + @Override + + public boolean enableContextualTipForPowerOff() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableContextualTipForPowerOff; + + } + + @Override + + public boolean enableContextualTipForTakeScreenshot() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableContextualTipForTakeScreenshot; + + } + + @Override + + public boolean enableContextualTips() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableContextualTips; + + } + + @Override + + public boolean enableEfficientDisplayRepository() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableEfficientDisplayRepository; + + } + + @Override + + public boolean enableLayoutTracing() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableLayoutTracing; + + } + + @Override + + public boolean enableViewCaptureTracing() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableViewCaptureTracing; + + } + + @Override + + public boolean enableWidgetPickerSizeFilter() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!communal_is_cached) { + load_overrides_communal(); + } + } + return enableWidgetPickerSizeFilter; + + } + + @Override + + public boolean enforceBrightnessBaseUserRestriction() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enforceBrightnessBaseUserRestriction; + + } + + @Override + + public boolean exampleFlag() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return exampleFlag; + + } + + @Override + + public boolean fastUnlockTransition() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return fastUnlockTransition; + + } + + @Override + + public boolean fixImageWallpaperCrashSurfaceAlreadyReleased() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return fixImageWallpaperCrashSurfaceAlreadyReleased; + + } + + @Override + + public boolean fixScreenshotActionDismissSystemWindows() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return fixScreenshotActionDismissSystemWindows; + + } + + @Override + + public boolean floatingMenuAnimatedTuck() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return floatingMenuAnimatedTuck; + + } + + @Override + + public boolean floatingMenuDragToEdit() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return floatingMenuDragToEdit; + + } + + @Override + + public boolean floatingMenuDragToHide() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return floatingMenuDragToHide; + + } + + @Override + + public boolean floatingMenuImeDisplacementAnimation() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return floatingMenuImeDisplacementAnimation; + + } + + @Override + + public boolean floatingMenuNarrowTargetContentObserver() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return floatingMenuNarrowTargetContentObserver; + + } + + @Override + + public boolean floatingMenuOverlapsNavBarsFlag() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return floatingMenuOverlapsNavBarsFlag; + + } + + @Override + + public boolean floatingMenuRadiiAnimation() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return floatingMenuRadiiAnimation; + + } + + @Override + + public boolean getConnectedDeviceNameUnsynchronized() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return getConnectedDeviceNameUnsynchronized; + + } + + @Override + + public boolean glanceableHubAllowKeyguardWhenDreaming() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return glanceableHubAllowKeyguardWhenDreaming; + + } + + @Override + + public boolean glanceableHubFullscreenSwipe() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return glanceableHubFullscreenSwipe; + + } + + @Override + + public boolean glanceableHubGestureHandle() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return glanceableHubGestureHandle; + + } + + @Override + + public boolean glanceableHubShortcutButton() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return glanceableHubShortcutButton; + + } + + @Override + + public boolean hapticBrightnessSlider() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return hapticBrightnessSlider; + + } + + @Override + + public boolean hapticVolumeSlider() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return hapticVolumeSlider; + + } + + @Override + + public boolean hearingAidsQsTileDialog() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return hearingAidsQsTileDialog; + + } + + @Override + + public boolean hearingDevicesDialogRelatedTools() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return hearingDevicesDialogRelatedTools; + + } + + @Override + + public boolean keyboardDockingIndicator() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return keyboardDockingIndicator; + + } + + @Override + + public boolean keyboardShortcutHelperRewrite() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return keyboardShortcutHelperRewrite; + + } + + @Override + + public boolean keyguardBottomAreaRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return keyguardBottomAreaRefactor; + + } + + @Override + + public boolean keyguardWmStateRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return keyguardWmStateRefactor; + + } + + @Override + + public boolean lightRevealMigration() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return lightRevealMigration; + + } + + @Override + + public boolean mediaControlsLockscreenShadeBugFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return mediaControlsLockscreenShadeBugFix; + + } + + @Override + + public boolean mediaControlsRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return mediaControlsRefactor; + + } + + @Override + + public boolean mediaControlsUserInitiatedDeleteintent() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return mediaControlsUserInitiatedDeleteintent; + + } + + @Override + + public boolean migrateClocksToBlueprint() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return migrateClocksToBlueprint; + + } + + @Override + + public boolean newAodTransition() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return newAodTransition; + + } + + @Override + + public boolean newTouchpadGesturesTutorial() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return newTouchpadGesturesTutorial; + + } + + @Override + + public boolean newVolumePanel() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return newVolumePanel; + + } + + @Override + + public boolean notificationAsyncGroupHeaderInflation() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationAsyncGroupHeaderInflation; + + } + + @Override + + public boolean notificationAsyncHybridViewInflation() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationAsyncHybridViewInflation; + + } + + @Override + + public boolean notificationAvalancheSuppression() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationAvalancheSuppression; + + } + + @Override + + public boolean notificationAvalancheThrottleHun() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationAvalancheThrottleHun; + + } + + @Override + + public boolean notificationBackgroundTintOptimization() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationBackgroundTintOptimization; + + } + + @Override + + public boolean notificationColorUpdateLogger() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationColorUpdateLogger; + + } + + @Override + + public boolean notificationContentAlphaOptimization() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationContentAlphaOptimization; + + } + + @Override + + public boolean notificationFooterBackgroundTintOptimization() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationFooterBackgroundTintOptimization; + + } + + @Override + + public boolean notificationMediaManagerBackgroundExecution() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationMediaManagerBackgroundExecution; + + } + + @Override + + public boolean notificationMinimalismPrototype() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationMinimalismPrototype; + + } + + @Override + + public boolean notificationOverExpansionClippingFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationOverExpansionClippingFix; + + } + + @Override + + public boolean notificationPulsingFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationPulsingFix; + + } + + @Override + + public boolean notificationRowContentBinderRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationRowContentBinderRefactor; + + } + + @Override + + public boolean notificationRowUserContext() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationRowUserContext; + + } + + @Override + + public boolean notificationViewFlipperPausingV2() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationViewFlipperPausingV2; + + } + + @Override + + public boolean notificationsBackgroundIcons() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationsBackgroundIcons; + + } + + @Override + + public boolean notificationsFooterViewRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationsFooterViewRefactor; + + } + + @Override + + public boolean notificationsHeadsUpRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationsHeadsUpRefactor; + + } + + @Override + + public boolean notificationsHideOnDisplaySwitch() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationsHideOnDisplaySwitch; + + } + + @Override + + public boolean notificationsIconContainerRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationsIconContainerRefactor; + + } + + @Override + + public boolean notificationsImprovedHunAnimation() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationsImprovedHunAnimation; + + } + + @Override + + public boolean notificationsLiveDataStoreRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notificationsLiveDataStoreRefactor; + + } + + @Override + + public boolean notifyPowerManagerUserActivityBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return notifyPowerManagerUserActivityBackground; + + } + + @Override + + public boolean pinInputFieldStyledFocusState() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return pinInputFieldStyledFocusState; + + } + + @Override + + public boolean predictiveBackAnimateBouncer() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return predictiveBackAnimateBouncer; + + } + + @Override + + public boolean predictiveBackAnimateDialogs() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return predictiveBackAnimateDialogs; + + } + + @Override + + public boolean predictiveBackAnimateShade() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return predictiveBackAnimateShade; + + } + + @Override + + public boolean predictiveBackSysui() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return predictiveBackSysui; + + } + + @Override + + public boolean priorityPeopleSection() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return priorityPeopleSection; + + } + + @Override + + public boolean privacyDotUnfoldWrongCornerFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return privacyDotUnfoldWrongCornerFix; + + } + + @Override + + public boolean pssAppSelectorAbruptExitFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return pssAppSelectorAbruptExitFix; + + } + + @Override + + public boolean pssAppSelectorRecentsSplitScreen() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return pssAppSelectorRecentsSplitScreen; + + } + + @Override + + public boolean pssTaskSwitcher() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return pssTaskSwitcher; + + } + + @Override + + public boolean qsCustomTileClickGuaranteedBugFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return qsCustomTileClickGuaranteedBugFix; + + } + + @Override + + public boolean qsNewPipeline() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return qsNewPipeline; + + } + + @Override + + public boolean qsNewTiles() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return qsNewTiles; + + } + + @Override + + public boolean qsNewTilesFuture() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return qsNewTilesFuture; + + } + + @Override + + public boolean qsTileFocusState() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return qsTileFocusState; + + } + + @Override + + public boolean qsUiRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return qsUiRefactor; + + } + + @Override + + public boolean quickSettingsVisualHapticsLongpress() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return quickSettingsVisualHapticsLongpress; + + } + + @Override + + public boolean recordIssueQsTile() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return recordIssueQsTile; + + } + + @Override + + public boolean refactorGetCurrentUser() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return refactorGetCurrentUser; + + } + + @Override + + public boolean registerBatteryControllerReceiversInCorestartable() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return registerBatteryControllerReceiversInCorestartable; + + } + + @Override + + public boolean registerNewWalletCardInBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return registerNewWalletCardInBackground; + + } + + @Override + + public boolean registerWallpaperNotifierBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return registerWallpaperNotifierBackground; + + } + + @Override + + public boolean registerZenModeContentObserverBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return registerZenModeContentObserverBackground; + + } + + @Override + + public boolean removeDreamOverlayHideOnTouch() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return removeDreamOverlayHideOnTouch; + + } + + @Override + + public boolean restToUnlock() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return restToUnlock; + + } + + @Override + + public boolean restartDreamOnUnocclude() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return restartDreamOnUnocclude; + + } + + @Override + + public boolean revampedBouncerMessages() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return revampedBouncerMessages; + + } + + @Override + + public boolean runFingerprintDetectOnDismissibleKeyguard() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return runFingerprintDetectOnDismissibleKeyguard; + + } + + @Override + + public boolean saveAndRestoreMagnificationSettingsButtons() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!accessibility_is_cached) { + load_overrides_accessibility(); + } + } + return saveAndRestoreMagnificationSettingsButtons; + + } + + @Override + + public boolean sceneContainer() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return sceneContainer; + + } + + @Override + + public boolean screenshareNotificationHidingBugFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return screenshareNotificationHidingBugFix; + + } + + @Override + + public boolean screenshotActionDismissSystemWindows() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return screenshotActionDismissSystemWindows; + + } + + @Override + + public boolean screenshotPrivateProfileAccessibilityAnnouncementFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return screenshotPrivateProfileAccessibilityAnnouncementFix; + + } + + @Override + + public boolean screenshotPrivateProfileBehaviorFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return screenshotPrivateProfileBehaviorFix; + + } + + @Override + + public boolean screenshotScrollCropViewCrashFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return screenshotScrollCropViewCrashFix; + + } + + @Override + + public boolean screenshotShelfUi2() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return screenshotShelfUi2; + + } + + @Override + + public boolean shadeCollapseActivityLaunchFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return shadeCollapseActivityLaunchFix; + + } + + @Override + + public boolean shaderlibLoadingEffectRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return shaderlibLoadingEffectRefactor; + + } + + @Override + + public boolean sliceBroadcastRelayInBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return sliceBroadcastRelayInBackground; + + } + + @Override + + public boolean sliceManagerBinderCallBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return sliceManagerBinderCallBackground; + + } + + @Override + + public boolean smartspaceLockscreenViewmodel() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return smartspaceLockscreenViewmodel; + + } + + @Override + + public boolean smartspaceRelocateToBottom() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return smartspaceRelocateToBottom; + + } + + @Override + + public boolean smartspaceRemoteviewsRendering() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return smartspaceRemoteviewsRendering; + + } + + @Override + + public boolean statusBarMonochromeIconsFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return statusBarMonochromeIconsFix; + + } + + @Override + + public boolean statusBarScreenSharingChips() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return statusBarScreenSharingChips; + + } + + @Override + + public boolean statusBarStaticInoutIndicators() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return statusBarStaticInoutIndicators; + + } + + @Override + + public boolean switchUserOnBg() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return switchUserOnBg; + + } + + @Override + + public boolean sysuiTeamfood() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return sysuiTeamfood; + + } + + @Override + + public boolean themeOverlayControllerWakefulnessDeprecation() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return themeOverlayControllerWakefulnessDeprecation; + + } + + @Override + + public boolean translucentOccludingActivityFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return translucentOccludingActivityFix; + + } + + @Override + + public boolean truncatedStatusBarIconsFix() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return truncatedStatusBarIconsFix; + + } + + @Override + + public boolean udfpsViewPerformance() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return udfpsViewPerformance; + + } + + @Override + + public boolean unfoldAnimationBackgroundProgress() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return unfoldAnimationBackgroundProgress; + + } + + @Override + + public boolean updateUserSwitcherBackground() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return updateUserSwitcherBackground; + + } + + @Override + + public boolean validateKeyboardShortcutHelperIconUri() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return validateKeyboardShortcutHelperIconUri; + + } + + @Override + + public boolean visualInterruptionsRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return visualInterruptionsRefactor; + + } + +} + diff --git a/flags/src/com/android/systemui/Flags.java b/flags/src/com/android/systemui/Flags.java new file mode 100644 index 0000000000..ec840cb508 --- /dev/null +++ b/flags/src/com/android/systemui/Flags.java @@ -0,0 +1,914 @@ +package com.android.systemui; +// TODO(b/303773055): Remove the annotation after access issue is resolved. +/** @hide */ +public final class Flags { + /** @hide */ + public static final String FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW = "com.android.systemui.activity_transition_use_largest_window"; + /** @hide */ + public static final String FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES = "com.android.systemui.ambient_touch_monitor_listen_to_display_changes"; + /** @hide */ + public static final String FLAG_APP_CLIPS_BACKLINKS = "com.android.systemui.app_clips_backlinks"; + /** @hide */ + public static final String FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY = "com.android.systemui.bind_keyguard_media_visibility"; + /** @hide */ + public static final String FLAG_BP_TALKBACK = "com.android.systemui.bp_talkback"; + /** @hide */ + public static final String FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE = "com.android.systemui.brightness_slider_focus_state"; + /** @hide */ + public static final String FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX = "com.android.systemui.centralized_status_bar_height_fix"; + /** @hide */ + public static final String FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN = "com.android.systemui.clipboard_noninteractive_on_lockscreen"; + /** @hide */ + public static final String FLAG_CLOCK_REACTIVE_VARIANTS = "com.android.systemui.clock_reactive_variants"; + /** @hide */ + public static final String FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN = "com.android.systemui.communal_bouncer_do_not_modify_plugin_open"; + /** @hide */ + public static final String FLAG_COMMUNAL_HUB = "com.android.systemui.communal_hub"; + /** @hide */ + public static final String FLAG_COMPOSE_BOUNCER = "com.android.systemui.compose_bouncer"; + /** @hide */ + public static final String FLAG_COMPOSE_LOCKSCREEN = "com.android.systemui.compose_lockscreen"; + /** @hide */ + public static final String FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH = "com.android.systemui.confine_notification_touch_to_view_width"; + /** @hide */ + public static final String FLAG_CONSTRAINT_BP = "com.android.systemui.constraint_bp"; + /** @hide */ + public static final String FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX = "com.android.systemui.contextual_tips_assistant_dismiss_fix"; + /** @hide */ + public static final String FLAG_COROUTINE_TRACING = "com.android.systemui.coroutine_tracing"; + /** @hide */ + public static final String FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER = "com.android.systemui.create_windowless_window_magnifier"; + /** @hide */ + public static final String FLAG_DEDICATED_NOTIF_INFLATION_THREAD = "com.android.systemui.dedicated_notif_inflation_thread"; + /** @hide */ + public static final String FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON = "com.android.systemui.delay_show_magnification_button"; + /** @hide */ + public static final String FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD = "com.android.systemui.delayed_wakelock_release_on_background_thread"; + /** @hide */ + public static final String FLAG_DEVICE_ENTRY_UDFPS_REFACTOR = "com.android.systemui.device_entry_udfps_refactor"; + /** @hide */ + public static final String FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK = "com.android.systemui.disable_contextual_tips_frequency_check"; + /** @hide */ + public static final String FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK = "com.android.systemui.disable_contextual_tips_ios_switcher_check"; + /** @hide */ + public static final String FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION = "com.android.systemui.dozeui_scheduling_alarms_background_execution"; + /** @hide */ + public static final String FLAG_DREAM_INPUT_SESSION_PILFER_ONCE = "com.android.systemui.dream_input_session_pilfer_once"; + /** @hide */ + public static final String FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING = "com.android.systemui.dream_overlay_bouncer_swipe_direction_filtering"; + /** @hide */ + public static final String FLAG_DUAL_SHADE = "com.android.systemui.dual_shade"; + /** @hide */ + public static final String FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD = "com.android.systemui.edge_back_gesture_handler_thread"; + /** @hide */ + public static final String FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND = "com.android.systemui.edgeback_gesture_handler_get_running_tasks_background"; + /** @hide */ + public static final String FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK = "com.android.systemui.enable_background_keyguard_ondrawn_callback"; + /** @hide */ + public static final String FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME = "com.android.systemui.enable_contextual_tip_for_mute_volume"; + /** @hide */ + public static final String FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF = "com.android.systemui.enable_contextual_tip_for_power_off"; + /** @hide */ + public static final String FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT = "com.android.systemui.enable_contextual_tip_for_take_screenshot"; + /** @hide */ + public static final String FLAG_ENABLE_CONTEXTUAL_TIPS = "com.android.systemui.enable_contextual_tips"; + /** @hide */ + public static final String FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY = "com.android.systemui.enable_efficient_display_repository"; + /** @hide */ + public static final String FLAG_ENABLE_LAYOUT_TRACING = "com.android.systemui.enable_layout_tracing"; + /** @hide */ + public static final String FLAG_ENABLE_VIEW_CAPTURE_TRACING = "com.android.systemui.enable_view_capture_tracing"; + /** @hide */ + public static final String FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER = "com.android.systemui.enable_widget_picker_size_filter"; + /** @hide */ + public static final String FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION = "com.android.systemui.enforce_brightness_base_user_restriction"; + /** @hide */ + public static final String FLAG_EXAMPLE_FLAG = "com.android.systemui.example_flag"; + /** @hide */ + public static final String FLAG_FAST_UNLOCK_TRANSITION = "com.android.systemui.fast_unlock_transition"; + /** @hide */ + public static final String FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED = "com.android.systemui.fix_image_wallpaper_crash_surface_already_released"; + /** @hide */ + public static final String FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS = "com.android.systemui.fix_screenshot_action_dismiss_system_windows"; + /** @hide */ + public static final String FLAG_FLOATING_MENU_ANIMATED_TUCK = "com.android.systemui.floating_menu_animated_tuck"; + /** @hide */ + public static final String FLAG_FLOATING_MENU_DRAG_TO_EDIT = "com.android.systemui.floating_menu_drag_to_edit"; + /** @hide */ + public static final String FLAG_FLOATING_MENU_DRAG_TO_HIDE = "com.android.systemui.floating_menu_drag_to_hide"; + /** @hide */ + public static final String FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION = "com.android.systemui.floating_menu_ime_displacement_animation"; + /** @hide */ + public static final String FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER = "com.android.systemui.floating_menu_narrow_target_content_observer"; + /** @hide */ + public static final String FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG = "com.android.systemui.floating_menu_overlaps_nav_bars_flag"; + /** @hide */ + public static final String FLAG_FLOATING_MENU_RADII_ANIMATION = "com.android.systemui.floating_menu_radii_animation"; + /** @hide */ + public static final String FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED = "com.android.systemui.get_connected_device_name_unsynchronized"; + /** @hide */ + public static final String FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING = "com.android.systemui.glanceable_hub_allow_keyguard_when_dreaming"; + /** @hide */ + public static final String FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE = "com.android.systemui.glanceable_hub_fullscreen_swipe"; + /** @hide */ + public static final String FLAG_GLANCEABLE_HUB_GESTURE_HANDLE = "com.android.systemui.glanceable_hub_gesture_handle"; + /** @hide */ + public static final String FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON = "com.android.systemui.glanceable_hub_shortcut_button"; + /** @hide */ + public static final String FLAG_HAPTIC_BRIGHTNESS_SLIDER = "com.android.systemui.haptic_brightness_slider"; + /** @hide */ + public static final String FLAG_HAPTIC_VOLUME_SLIDER = "com.android.systemui.haptic_volume_slider"; + /** @hide */ + public static final String FLAG_HEARING_AIDS_QS_TILE_DIALOG = "com.android.systemui.hearing_aids_qs_tile_dialog"; + /** @hide */ + public static final String FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS = "com.android.systemui.hearing_devices_dialog_related_tools"; + /** @hide */ + public static final String FLAG_KEYBOARD_DOCKING_INDICATOR = "com.android.systemui.keyboard_docking_indicator"; + /** @hide */ + public static final String FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE = "com.android.systemui.keyboard_shortcut_helper_rewrite"; + /** @hide */ + public static final String FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR = "com.android.systemui.keyguard_bottom_area_refactor"; + /** @hide */ + public static final String FLAG_KEYGUARD_WM_STATE_REFACTOR = "com.android.systemui.keyguard_wm_state_refactor"; + /** @hide */ + public static final String FLAG_LIGHT_REVEAL_MIGRATION = "com.android.systemui.light_reveal_migration"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX = "com.android.systemui.media_controls_lockscreen_shade_bug_fix"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_REFACTOR = "com.android.systemui.media_controls_refactor"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT = "com.android.systemui.media_controls_user_initiated_deleteintent"; + /** @hide */ + public static final String FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT = "com.android.systemui.migrate_clocks_to_blueprint"; + /** @hide */ + public static final String FLAG_NEW_AOD_TRANSITION = "com.android.systemui.new_aod_transition"; + /** @hide */ + public static final String FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL = "com.android.systemui.new_touchpad_gestures_tutorial"; + /** @hide */ + public static final String FLAG_NEW_VOLUME_PANEL = "com.android.systemui.new_volume_panel"; + /** @hide */ + public static final String FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION = "com.android.systemui.notification_async_group_header_inflation"; + /** @hide */ + public static final String FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION = "com.android.systemui.notification_async_hybrid_view_inflation"; + /** @hide */ + public static final String FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION = "com.android.systemui.notification_avalanche_suppression"; + /** @hide */ + public static final String FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN = "com.android.systemui.notification_avalanche_throttle_hun"; + /** @hide */ + public static final String FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION = "com.android.systemui.notification_background_tint_optimization"; + /** @hide */ + public static final String FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER = "com.android.systemui.notification_color_update_logger"; + /** @hide */ + public static final String FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION = "com.android.systemui.notification_content_alpha_optimization"; + /** @hide */ + public static final String FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION = "com.android.systemui.notification_footer_background_tint_optimization"; + /** @hide */ + public static final String FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION = "com.android.systemui.notification_media_manager_background_execution"; + /** @hide */ + public static final String FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE = "com.android.systemui.notification_minimalism_prototype"; + /** @hide */ + public static final String FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX = "com.android.systemui.notification_over_expansion_clipping_fix"; + /** @hide */ + public static final String FLAG_NOTIFICATION_PULSING_FIX = "com.android.systemui.notification_pulsing_fix"; + /** @hide */ + public static final String FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR = "com.android.systemui.notification_row_content_binder_refactor"; + /** @hide */ + public static final String FLAG_NOTIFICATION_ROW_USER_CONTEXT = "com.android.systemui.notification_row_user_context"; + /** @hide */ + public static final String FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2 = "com.android.systemui.notification_view_flipper_pausing_v2"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_BACKGROUND_ICONS = "com.android.systemui.notifications_background_icons"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR = "com.android.systemui.notifications_footer_view_refactor"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR = "com.android.systemui.notifications_heads_up_refactor"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH = "com.android.systemui.notifications_hide_on_display_switch"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR = "com.android.systemui.notifications_icon_container_refactor"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION = "com.android.systemui.notifications_improved_hun_animation"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR = "com.android.systemui.notifications_live_data_store_refactor"; + /** @hide */ + public static final String FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND = "com.android.systemui.notify_power_manager_user_activity_background"; + /** @hide */ + public static final String FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE = "com.android.systemui.pin_input_field_styled_focus_state"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER = "com.android.systemui.predictive_back_animate_bouncer"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS = "com.android.systemui.predictive_back_animate_dialogs"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_ANIMATE_SHADE = "com.android.systemui.predictive_back_animate_shade"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_SYSUI = "com.android.systemui.predictive_back_sysui"; + /** @hide */ + public static final String FLAG_PRIORITY_PEOPLE_SECTION = "com.android.systemui.priority_people_section"; + /** @hide */ + public static final String FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX = "com.android.systemui.privacy_dot_unfold_wrong_corner_fix"; + /** @hide */ + public static final String FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX = "com.android.systemui.pss_app_selector_abrupt_exit_fix"; + /** @hide */ + public static final String FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN = "com.android.systemui.pss_app_selector_recents_split_screen"; + /** @hide */ + public static final String FLAG_PSS_TASK_SWITCHER = "com.android.systemui.pss_task_switcher"; + /** @hide */ + public static final String FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX = "com.android.systemui.qs_custom_tile_click_guaranteed_bug_fix"; + /** @hide */ + public static final String FLAG_QS_NEW_PIPELINE = "com.android.systemui.qs_new_pipeline"; + /** @hide */ + public static final String FLAG_QS_NEW_TILES = "com.android.systemui.qs_new_tiles"; + /** @hide */ + public static final String FLAG_QS_NEW_TILES_FUTURE = "com.android.systemui.qs_new_tiles_future"; + /** @hide */ + public static final String FLAG_QS_TILE_FOCUS_STATE = "com.android.systemui.qs_tile_focus_state"; + /** @hide */ + public static final String FLAG_QS_UI_REFACTOR = "com.android.systemui.qs_ui_refactor"; + /** @hide */ + public static final String FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS = "com.android.systemui.quick_settings_visual_haptics_longpress"; + /** @hide */ + public static final String FLAG_RECORD_ISSUE_QS_TILE = "com.android.systemui.record_issue_qs_tile"; + /** @hide */ + public static final String FLAG_REFACTOR_GET_CURRENT_USER = "com.android.systemui.refactor_get_current_user"; + /** @hide */ + public static final String FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE = "com.android.systemui.register_battery_controller_receivers_in_corestartable"; + /** @hide */ + public static final String FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND = "com.android.systemui.register_new_wallet_card_in_background"; + /** @hide */ + public static final String FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND = "com.android.systemui.register_wallpaper_notifier_background"; + /** @hide */ + public static final String FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND = "com.android.systemui.register_zen_mode_content_observer_background"; + /** @hide */ + public static final String FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH = "com.android.systemui.remove_dream_overlay_hide_on_touch"; + /** @hide */ + public static final String FLAG_REST_TO_UNLOCK = "com.android.systemui.rest_to_unlock"; + /** @hide */ + public static final String FLAG_RESTART_DREAM_ON_UNOCCLUDE = "com.android.systemui.restart_dream_on_unocclude"; + /** @hide */ + public static final String FLAG_REVAMPED_BOUNCER_MESSAGES = "com.android.systemui.revamped_bouncer_messages"; + /** @hide */ + public static final String FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD = "com.android.systemui.run_fingerprint_detect_on_dismissible_keyguard"; + /** @hide */ + public static final String FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS = "com.android.systemui.save_and_restore_magnification_settings_buttons"; + /** @hide */ + public static final String FLAG_SCENE_CONTAINER = "com.android.systemui.scene_container"; + /** @hide */ + public static final String FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX = "com.android.systemui.screenshare_notification_hiding_bug_fix"; + /** @hide */ + public static final String FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS = "com.android.systemui.screenshot_action_dismiss_system_windows"; + /** @hide */ + public static final String FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX = "com.android.systemui.screenshot_private_profile_accessibility_announcement_fix"; + /** @hide */ + public static final String FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX = "com.android.systemui.screenshot_private_profile_behavior_fix"; + /** @hide */ + public static final String FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX = "com.android.systemui.screenshot_scroll_crop_view_crash_fix"; + /** @hide */ + public static final String FLAG_SCREENSHOT_SHELF_UI2 = "com.android.systemui.screenshot_shelf_ui2"; + /** @hide */ + public static final String FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX = "com.android.systemui.shade_collapse_activity_launch_fix"; + /** @hide */ + public static final String FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR = "com.android.systemui.shaderlib_loading_effect_refactor"; + /** @hide */ + public static final String FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND = "com.android.systemui.slice_broadcast_relay_in_background"; + /** @hide */ + public static final String FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND = "com.android.systemui.slice_manager_binder_call_background"; + /** @hide */ + public static final String FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL = "com.android.systemui.smartspace_lockscreen_viewmodel"; + /** @hide */ + public static final String FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM = "com.android.systemui.smartspace_relocate_to_bottom"; + /** @hide */ + public static final String FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING = "com.android.systemui.smartspace_remoteviews_rendering"; + /** @hide */ + public static final String FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX = "com.android.systemui.status_bar_monochrome_icons_fix"; + /** @hide */ + public static final String FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS = "com.android.systemui.status_bar_screen_sharing_chips"; + /** @hide */ + public static final String FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS = "com.android.systemui.status_bar_static_inout_indicators"; + /** @hide */ + public static final String FLAG_SWITCH_USER_ON_BG = "com.android.systemui.switch_user_on_bg"; + /** @hide */ + public static final String FLAG_SYSUI_TEAMFOOD = "com.android.systemui.sysui_teamfood"; + /** @hide */ + public static final String FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION = "com.android.systemui.theme_overlay_controller_wakefulness_deprecation"; + /** @hide */ + public static final String FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX = "com.android.systemui.translucent_occluding_activity_fix"; + /** @hide */ + public static final String FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX = "com.android.systemui.truncated_status_bar_icons_fix"; + /** @hide */ + public static final String FLAG_UDFPS_VIEW_PERFORMANCE = "com.android.systemui.udfps_view_performance"; + /** @hide */ + public static final String FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS = "com.android.systemui.unfold_animation_background_progress"; + /** @hide */ + public static final String FLAG_UPDATE_USER_SWITCHER_BACKGROUND = "com.android.systemui.update_user_switcher_background"; + /** @hide */ + public static final String FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI = "com.android.systemui.validate_keyboard_shortcut_helper_icon_uri"; + /** @hide */ + public static final String FLAG_VISUAL_INTERRUPTIONS_REFACTOR = "com.android.systemui.visual_interruptions_refactor"; + + public static boolean activityTransitionUseLargestWindow() { + return FEATURE_FLAGS.activityTransitionUseLargestWindow(); + } + + public static boolean ambientTouchMonitorListenToDisplayChanges() { + return FEATURE_FLAGS.ambientTouchMonitorListenToDisplayChanges(); + } + + public static boolean appClipsBacklinks() { + return FEATURE_FLAGS.appClipsBacklinks(); + } + + public static boolean bindKeyguardMediaVisibility() { + return FEATURE_FLAGS.bindKeyguardMediaVisibility(); + } + + public static boolean bpTalkback() { + return FEATURE_FLAGS.bpTalkback(); + } + + public static boolean brightnessSliderFocusState() { + return FEATURE_FLAGS.brightnessSliderFocusState(); + } + + public static boolean centralizedStatusBarHeightFix() { + return FEATURE_FLAGS.centralizedStatusBarHeightFix(); + } + + public static boolean clipboardNoninteractiveOnLockscreen() { + return FEATURE_FLAGS.clipboardNoninteractiveOnLockscreen(); + } + + public static boolean clockReactiveVariants() { + return FEATURE_FLAGS.clockReactiveVariants(); + } + + public static boolean communalBouncerDoNotModifyPluginOpen() { + return FEATURE_FLAGS.communalBouncerDoNotModifyPluginOpen(); + } + + public static boolean communalHub() { + return FEATURE_FLAGS.communalHub(); + } + + public static boolean composeBouncer() { + return FEATURE_FLAGS.composeBouncer(); + } + + public static boolean composeLockscreen() { + return FEATURE_FLAGS.composeLockscreen(); + } + + public static boolean confineNotificationTouchToViewWidth() { + return FEATURE_FLAGS.confineNotificationTouchToViewWidth(); + } + + public static boolean constraintBp() { + return FEATURE_FLAGS.constraintBp(); + } + + public static boolean contextualTipsAssistantDismissFix() { + return FEATURE_FLAGS.contextualTipsAssistantDismissFix(); + } + + public static boolean coroutineTracing() { + return FEATURE_FLAGS.coroutineTracing(); + } + + public static boolean createWindowlessWindowMagnifier() { + return FEATURE_FLAGS.createWindowlessWindowMagnifier(); + } + + public static boolean dedicatedNotifInflationThread() { + return FEATURE_FLAGS.dedicatedNotifInflationThread(); + } + + public static boolean delayShowMagnificationButton() { + return FEATURE_FLAGS.delayShowMagnificationButton(); + } + + public static boolean delayedWakelockReleaseOnBackgroundThread() { + return FEATURE_FLAGS.delayedWakelockReleaseOnBackgroundThread(); + } + + public static boolean deviceEntryUdfpsRefactor() { + return FEATURE_FLAGS.deviceEntryUdfpsRefactor(); + } + + public static boolean disableContextualTipsFrequencyCheck() { + return FEATURE_FLAGS.disableContextualTipsFrequencyCheck(); + } + + public static boolean disableContextualTipsIosSwitcherCheck() { + return FEATURE_FLAGS.disableContextualTipsIosSwitcherCheck(); + } + + public static boolean dozeuiSchedulingAlarmsBackgroundExecution() { + return FEATURE_FLAGS.dozeuiSchedulingAlarmsBackgroundExecution(); + } + + public static boolean dreamInputSessionPilferOnce() { + return FEATURE_FLAGS.dreamInputSessionPilferOnce(); + } + + public static boolean dreamOverlayBouncerSwipeDirectionFiltering() { + return FEATURE_FLAGS.dreamOverlayBouncerSwipeDirectionFiltering(); + } + + public static boolean dualShade() { + return FEATURE_FLAGS.dualShade(); + } + + public static boolean edgeBackGestureHandlerThread() { + return FEATURE_FLAGS.edgeBackGestureHandlerThread(); + } + + public static boolean edgebackGestureHandlerGetRunningTasksBackground() { + return FEATURE_FLAGS.edgebackGestureHandlerGetRunningTasksBackground(); + } + + public static boolean enableBackgroundKeyguardOndrawnCallback() { + return FEATURE_FLAGS.enableBackgroundKeyguardOndrawnCallback(); + } + + public static boolean enableContextualTipForMuteVolume() { + return FEATURE_FLAGS.enableContextualTipForMuteVolume(); + } + + public static boolean enableContextualTipForPowerOff() { + return FEATURE_FLAGS.enableContextualTipForPowerOff(); + } + + public static boolean enableContextualTipForTakeScreenshot() { + return FEATURE_FLAGS.enableContextualTipForTakeScreenshot(); + } + + public static boolean enableContextualTips() { + return FEATURE_FLAGS.enableContextualTips(); + } + + public static boolean enableEfficientDisplayRepository() { + return FEATURE_FLAGS.enableEfficientDisplayRepository(); + } + + public static boolean enableLayoutTracing() { + return FEATURE_FLAGS.enableLayoutTracing(); + } + + public static boolean enableViewCaptureTracing() { + return FEATURE_FLAGS.enableViewCaptureTracing(); + } + + public static boolean enableWidgetPickerSizeFilter() { + return FEATURE_FLAGS.enableWidgetPickerSizeFilter(); + } + + public static boolean enforceBrightnessBaseUserRestriction() { + return FEATURE_FLAGS.enforceBrightnessBaseUserRestriction(); + } + + public static boolean exampleFlag() { + return FEATURE_FLAGS.exampleFlag(); + } + + public static boolean fastUnlockTransition() { + return FEATURE_FLAGS.fastUnlockTransition(); + } + + public static boolean fixImageWallpaperCrashSurfaceAlreadyReleased() { + return FEATURE_FLAGS.fixImageWallpaperCrashSurfaceAlreadyReleased(); + } + + public static boolean fixScreenshotActionDismissSystemWindows() { + return FEATURE_FLAGS.fixScreenshotActionDismissSystemWindows(); + } + + public static boolean floatingMenuAnimatedTuck() { + return FEATURE_FLAGS.floatingMenuAnimatedTuck(); + } + + public static boolean floatingMenuDragToEdit() { + return FEATURE_FLAGS.floatingMenuDragToEdit(); + } + + public static boolean floatingMenuDragToHide() { + return FEATURE_FLAGS.floatingMenuDragToHide(); + } + + public static boolean floatingMenuImeDisplacementAnimation() { + return FEATURE_FLAGS.floatingMenuImeDisplacementAnimation(); + } + + public static boolean floatingMenuNarrowTargetContentObserver() { + return FEATURE_FLAGS.floatingMenuNarrowTargetContentObserver(); + } + + public static boolean floatingMenuOverlapsNavBarsFlag() { + return FEATURE_FLAGS.floatingMenuOverlapsNavBarsFlag(); + } + + public static boolean floatingMenuRadiiAnimation() { + return FEATURE_FLAGS.floatingMenuRadiiAnimation(); + } + + public static boolean getConnectedDeviceNameUnsynchronized() { + return FEATURE_FLAGS.getConnectedDeviceNameUnsynchronized(); + } + + public static boolean glanceableHubAllowKeyguardWhenDreaming() { + return FEATURE_FLAGS.glanceableHubAllowKeyguardWhenDreaming(); + } + + public static boolean glanceableHubFullscreenSwipe() { + return FEATURE_FLAGS.glanceableHubFullscreenSwipe(); + } + + public static boolean glanceableHubGestureHandle() { + return FEATURE_FLAGS.glanceableHubGestureHandle(); + } + + public static boolean glanceableHubShortcutButton() { + return FEATURE_FLAGS.glanceableHubShortcutButton(); + } + + public static boolean hapticBrightnessSlider() { + return FEATURE_FLAGS.hapticBrightnessSlider(); + } + + public static boolean hapticVolumeSlider() { + return FEATURE_FLAGS.hapticVolumeSlider(); + } + + public static boolean hearingAidsQsTileDialog() { + return FEATURE_FLAGS.hearingAidsQsTileDialog(); + } + + public static boolean hearingDevicesDialogRelatedTools() { + return FEATURE_FLAGS.hearingDevicesDialogRelatedTools(); + } + + public static boolean keyboardDockingIndicator() { + return FEATURE_FLAGS.keyboardDockingIndicator(); + } + + public static boolean keyboardShortcutHelperRewrite() { + return FEATURE_FLAGS.keyboardShortcutHelperRewrite(); + } + + public static boolean keyguardBottomAreaRefactor() { + return FEATURE_FLAGS.keyguardBottomAreaRefactor(); + } + + public static boolean keyguardWmStateRefactor() { + return FEATURE_FLAGS.keyguardWmStateRefactor(); + } + + public static boolean lightRevealMigration() { + return FEATURE_FLAGS.lightRevealMigration(); + } + + public static boolean mediaControlsLockscreenShadeBugFix() { + return FEATURE_FLAGS.mediaControlsLockscreenShadeBugFix(); + } + + public static boolean mediaControlsRefactor() { + return FEATURE_FLAGS.mediaControlsRefactor(); + } + + public static boolean mediaControlsUserInitiatedDeleteintent() { + return FEATURE_FLAGS.mediaControlsUserInitiatedDeleteintent(); + } + + public static boolean migrateClocksToBlueprint() { + return FEATURE_FLAGS.migrateClocksToBlueprint(); + } + + public static boolean newAodTransition() { + return FEATURE_FLAGS.newAodTransition(); + } + + public static boolean newTouchpadGesturesTutorial() { + return FEATURE_FLAGS.newTouchpadGesturesTutorial(); + } + + public static boolean newVolumePanel() { + return FEATURE_FLAGS.newVolumePanel(); + } + + public static boolean notificationAsyncGroupHeaderInflation() { + return FEATURE_FLAGS.notificationAsyncGroupHeaderInflation(); + } + + public static boolean notificationAsyncHybridViewInflation() { + return FEATURE_FLAGS.notificationAsyncHybridViewInflation(); + } + + public static boolean notificationAvalancheSuppression() { + return FEATURE_FLAGS.notificationAvalancheSuppression(); + } + + public static boolean notificationAvalancheThrottleHun() { + return FEATURE_FLAGS.notificationAvalancheThrottleHun(); + } + + public static boolean notificationBackgroundTintOptimization() { + return FEATURE_FLAGS.notificationBackgroundTintOptimization(); + } + + public static boolean notificationColorUpdateLogger() { + return FEATURE_FLAGS.notificationColorUpdateLogger(); + } + + public static boolean notificationContentAlphaOptimization() { + return FEATURE_FLAGS.notificationContentAlphaOptimization(); + } + + public static boolean notificationFooterBackgroundTintOptimization() { + return FEATURE_FLAGS.notificationFooterBackgroundTintOptimization(); + } + + public static boolean notificationMediaManagerBackgroundExecution() { + return FEATURE_FLAGS.notificationMediaManagerBackgroundExecution(); + } + + public static boolean notificationMinimalismPrototype() { + return FEATURE_FLAGS.notificationMinimalismPrototype(); + } + + public static boolean notificationOverExpansionClippingFix() { + return FEATURE_FLAGS.notificationOverExpansionClippingFix(); + } + + public static boolean notificationPulsingFix() { + return FEATURE_FLAGS.notificationPulsingFix(); + } + + public static boolean notificationRowContentBinderRefactor() { + return FEATURE_FLAGS.notificationRowContentBinderRefactor(); + } + + public static boolean notificationRowUserContext() { + return FEATURE_FLAGS.notificationRowUserContext(); + } + + public static boolean notificationViewFlipperPausingV2() { + return FEATURE_FLAGS.notificationViewFlipperPausingV2(); + } + + public static boolean notificationsBackgroundIcons() { + return FEATURE_FLAGS.notificationsBackgroundIcons(); + } + + public static boolean notificationsFooterViewRefactor() { + return FEATURE_FLAGS.notificationsFooterViewRefactor(); + } + + public static boolean notificationsHeadsUpRefactor() { + return FEATURE_FLAGS.notificationsHeadsUpRefactor(); + } + + public static boolean notificationsHideOnDisplaySwitch() { + return FEATURE_FLAGS.notificationsHideOnDisplaySwitch(); + } + + public static boolean notificationsIconContainerRefactor() { + return FEATURE_FLAGS.notificationsIconContainerRefactor(); + } + + public static boolean notificationsImprovedHunAnimation() { + return FEATURE_FLAGS.notificationsImprovedHunAnimation(); + } + + public static boolean notificationsLiveDataStoreRefactor() { + return FEATURE_FLAGS.notificationsLiveDataStoreRefactor(); + } + + public static boolean notifyPowerManagerUserActivityBackground() { + return FEATURE_FLAGS.notifyPowerManagerUserActivityBackground(); + } + + public static boolean pinInputFieldStyledFocusState() { + return FEATURE_FLAGS.pinInputFieldStyledFocusState(); + } + + public static boolean predictiveBackAnimateBouncer() { + return FEATURE_FLAGS.predictiveBackAnimateBouncer(); + } + + public static boolean predictiveBackAnimateDialogs() { + return FEATURE_FLAGS.predictiveBackAnimateDialogs(); + } + + public static boolean predictiveBackAnimateShade() { + return FEATURE_FLAGS.predictiveBackAnimateShade(); + } + + public static boolean predictiveBackSysui() { + return FEATURE_FLAGS.predictiveBackSysui(); + } + + public static boolean priorityPeopleSection() { + return FEATURE_FLAGS.priorityPeopleSection(); + } + + public static boolean privacyDotUnfoldWrongCornerFix() { + return FEATURE_FLAGS.privacyDotUnfoldWrongCornerFix(); + } + + public static boolean pssAppSelectorAbruptExitFix() { + return FEATURE_FLAGS.pssAppSelectorAbruptExitFix(); + } + + public static boolean pssAppSelectorRecentsSplitScreen() { + return FEATURE_FLAGS.pssAppSelectorRecentsSplitScreen(); + } + + public static boolean pssTaskSwitcher() { + return FEATURE_FLAGS.pssTaskSwitcher(); + } + + public static boolean qsCustomTileClickGuaranteedBugFix() { + return FEATURE_FLAGS.qsCustomTileClickGuaranteedBugFix(); + } + + public static boolean qsNewPipeline() { + return FEATURE_FLAGS.qsNewPipeline(); + } + + public static boolean qsNewTiles() { + return FEATURE_FLAGS.qsNewTiles(); + } + + public static boolean qsNewTilesFuture() { + return FEATURE_FLAGS.qsNewTilesFuture(); + } + + public static boolean qsTileFocusState() { + return FEATURE_FLAGS.qsTileFocusState(); + } + + public static boolean qsUiRefactor() { + return FEATURE_FLAGS.qsUiRefactor(); + } + + public static boolean quickSettingsVisualHapticsLongpress() { + return FEATURE_FLAGS.quickSettingsVisualHapticsLongpress(); + } + + public static boolean recordIssueQsTile() { + return FEATURE_FLAGS.recordIssueQsTile(); + } + + public static boolean refactorGetCurrentUser() { + return FEATURE_FLAGS.refactorGetCurrentUser(); + } + + public static boolean registerBatteryControllerReceiversInCorestartable() { + return FEATURE_FLAGS.registerBatteryControllerReceiversInCorestartable(); + } + + public static boolean registerNewWalletCardInBackground() { + return FEATURE_FLAGS.registerNewWalletCardInBackground(); + } + + public static boolean registerWallpaperNotifierBackground() { + return FEATURE_FLAGS.registerWallpaperNotifierBackground(); + } + + public static boolean registerZenModeContentObserverBackground() { + return FEATURE_FLAGS.registerZenModeContentObserverBackground(); + } + + public static boolean removeDreamOverlayHideOnTouch() { + return FEATURE_FLAGS.removeDreamOverlayHideOnTouch(); + } + + public static boolean restToUnlock() { + return FEATURE_FLAGS.restToUnlock(); + } + + public static boolean restartDreamOnUnocclude() { + return FEATURE_FLAGS.restartDreamOnUnocclude(); + } + + public static boolean revampedBouncerMessages() { + return FEATURE_FLAGS.revampedBouncerMessages(); + } + + public static boolean runFingerprintDetectOnDismissibleKeyguard() { + return FEATURE_FLAGS.runFingerprintDetectOnDismissibleKeyguard(); + } + + public static boolean saveAndRestoreMagnificationSettingsButtons() { + return FEATURE_FLAGS.saveAndRestoreMagnificationSettingsButtons(); + } + + public static boolean sceneContainer() { + return FEATURE_FLAGS.sceneContainer(); + } + + public static boolean screenshareNotificationHidingBugFix() { + return FEATURE_FLAGS.screenshareNotificationHidingBugFix(); + } + + public static boolean screenshotActionDismissSystemWindows() { + return FEATURE_FLAGS.screenshotActionDismissSystemWindows(); + } + + public static boolean screenshotPrivateProfileAccessibilityAnnouncementFix() { + return FEATURE_FLAGS.screenshotPrivateProfileAccessibilityAnnouncementFix(); + } + + public static boolean screenshotPrivateProfileBehaviorFix() { + return FEATURE_FLAGS.screenshotPrivateProfileBehaviorFix(); + } + + public static boolean screenshotScrollCropViewCrashFix() { + return FEATURE_FLAGS.screenshotScrollCropViewCrashFix(); + } + + public static boolean screenshotShelfUi2() { + return FEATURE_FLAGS.screenshotShelfUi2(); + } + + public static boolean shadeCollapseActivityLaunchFix() { + return FEATURE_FLAGS.shadeCollapseActivityLaunchFix(); + } + + public static boolean shaderlibLoadingEffectRefactor() { + return FEATURE_FLAGS.shaderlibLoadingEffectRefactor(); + } + + public static boolean sliceBroadcastRelayInBackground() { + return FEATURE_FLAGS.sliceBroadcastRelayInBackground(); + } + + public static boolean sliceManagerBinderCallBackground() { + return FEATURE_FLAGS.sliceManagerBinderCallBackground(); + } + + public static boolean smartspaceLockscreenViewmodel() { + return FEATURE_FLAGS.smartspaceLockscreenViewmodel(); + } + + public static boolean smartspaceRelocateToBottom() { + return FEATURE_FLAGS.smartspaceRelocateToBottom(); + } + + public static boolean smartspaceRemoteviewsRendering() { + return FEATURE_FLAGS.smartspaceRemoteviewsRendering(); + } + + public static boolean statusBarMonochromeIconsFix() { + return FEATURE_FLAGS.statusBarMonochromeIconsFix(); + } + + public static boolean statusBarScreenSharingChips() { + return FEATURE_FLAGS.statusBarScreenSharingChips(); + } + + public static boolean statusBarStaticInoutIndicators() { + return FEATURE_FLAGS.statusBarStaticInoutIndicators(); + } + + public static boolean switchUserOnBg() { + return FEATURE_FLAGS.switchUserOnBg(); + } + + public static boolean sysuiTeamfood() { + return FEATURE_FLAGS.sysuiTeamfood(); + } + + public static boolean themeOverlayControllerWakefulnessDeprecation() { + return FEATURE_FLAGS.themeOverlayControllerWakefulnessDeprecation(); + } + + public static boolean translucentOccludingActivityFix() { + return FEATURE_FLAGS.translucentOccludingActivityFix(); + } + + public static boolean truncatedStatusBarIconsFix() { + return FEATURE_FLAGS.truncatedStatusBarIconsFix(); + } + + public static boolean udfpsViewPerformance() { + return FEATURE_FLAGS.udfpsViewPerformance(); + } + + public static boolean unfoldAnimationBackgroundProgress() { + return FEATURE_FLAGS.unfoldAnimationBackgroundProgress(); + } + + public static boolean updateUserSwitcherBackground() { + return FEATURE_FLAGS.updateUserSwitcherBackground(); + } + + public static boolean validateKeyboardShortcutHelperIconUri() { + return FEATURE_FLAGS.validateKeyboardShortcutHelperIconUri(); + } + + public static boolean visualInterruptionsRefactor() { + return FEATURE_FLAGS.visualInterruptionsRefactor(); + } + + private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); + +} diff --git a/flags/src/com/android/systemui/shared/CustomFeatureFlags.java b/flags/src/com/android/systemui/shared/CustomFeatureFlags.java new file mode 100644 index 0000000000..b301340b4a --- /dev/null +++ b/flags/src/com/android/systemui/shared/CustomFeatureFlags.java @@ -0,0 +1,94 @@ +package com.android.systemui.shared; + +// TODO(b/303773055): Remove the annotation after access issue is resolved. +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +/** @hide */ +public class CustomFeatureFlags implements FeatureFlags { + + private BiPredicate> mGetValueImpl; + + public CustomFeatureFlags(BiPredicate> getValueImpl) { + mGetValueImpl = getValueImpl; + } + @Override + + public boolean bouncerAreaExclusion() { + return getValue(Flags.FLAG_BOUNCER_AREA_EXCLUSION, + FeatureFlags::bouncerAreaExclusion); + } + + @Override + + public boolean enableHomeDelay() { + return getValue(Flags.FLAG_ENABLE_HOME_DELAY, + FeatureFlags::enableHomeDelay); + } + + @Override + + public boolean exampleSharedFlag() { + return getValue(Flags.FLAG_EXAMPLE_SHARED_FLAG, + FeatureFlags::exampleSharedFlag); + } + + @Override + + public boolean returnAnimationFrameworkLibrary() { + return getValue(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + FeatureFlags::returnAnimationFrameworkLibrary); + } + + @Override + + public boolean shadeAllowBackGesture() { + return getValue(Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, + FeatureFlags::shadeAllowBackGesture); + } + + @Override + + public boolean sidefpsControllerRefactor() { + return getValue(Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR, + FeatureFlags::sidefpsControllerRefactor); + } + + public boolean isFlagReadOnlyOptimized(String flagName) { + if (mReadOnlyFlagsSet.contains(flagName) && + isOptimizationEnabled()) { + return true; + } + return false; + } + + + private boolean isOptimizationEnabled() { + return false; + } + + protected boolean getValue(String flagName, Predicate getter) { + return mGetValueImpl.test(flagName, getter); + } + + public List getFlagNames() { + return Arrays.asList( + Flags.FLAG_BOUNCER_AREA_EXCLUSION, + Flags.FLAG_ENABLE_HOME_DELAY, + Flags.FLAG_EXAMPLE_SHARED_FLAG, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, + Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR + ); + } + + private Set mReadOnlyFlagsSet = new HashSet<>( + Arrays.asList( + "" + ) + ); +} diff --git a/flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java b/flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java new file mode 100644 index 0000000000..c223248bbf --- /dev/null +++ b/flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java @@ -0,0 +1,49 @@ +package com.android.systemui.shared; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +/** @hide */ +public class FakeFeatureFlagsImpl extends CustomFeatureFlags { + private final Map mFlagMap = new HashMap<>(); + private final FeatureFlags mDefaults; + + public FakeFeatureFlagsImpl() { + this(null); + } + + public FakeFeatureFlagsImpl(FeatureFlags defaults) { + super(null); + mDefaults = defaults; + // Initialize the map with null values + for (String flagName : getFlagNames()) { + mFlagMap.put(flagName, null); + } + } + + @Override + protected boolean getValue(String flagName, Predicate getter) { + Boolean value = this.mFlagMap.get(flagName); + if (value != null) { + return value; + } + if (mDefaults != null) { + return getter.test(mDefaults); + } + throw new IllegalArgumentException(flagName + " is not set"); + } + + public void setFlag(String flagName, boolean value) { + if (!this.mFlagMap.containsKey(flagName)) { + throw new IllegalArgumentException("no such flag " + flagName); + } + this.mFlagMap.put(flagName, value); + } + + public void resetAll() { + for (Map.Entry entry : mFlagMap.entrySet()) { + entry.setValue(null); + } + } +} diff --git a/flags/src/com/android/systemui/shared/FeatureFlags.java b/flags/src/com/android/systemui/shared/FeatureFlags.java new file mode 100644 index 0000000000..e9fe9c7144 --- /dev/null +++ b/flags/src/com/android/systemui/shared/FeatureFlags.java @@ -0,0 +1,24 @@ +package com.android.systemui.shared; +// TODO(b/303773055): Remove the annotation after access issue is resolved. +/** @hide */ +public interface FeatureFlags { + + + + boolean bouncerAreaExclusion(); + + + boolean enableHomeDelay(); + + + boolean exampleSharedFlag(); + + + boolean returnAnimationFrameworkLibrary(); + + + boolean shadeAllowBackGesture(); + + + boolean sidefpsControllerRefactor(); +} diff --git a/flags/src/com/android/systemui/shared/FeatureFlagsImpl.java b/flags/src/com/android/systemui/shared/FeatureFlagsImpl.java new file mode 100644 index 0000000000..f3df7770be --- /dev/null +++ b/flags/src/com/android/systemui/shared/FeatureFlagsImpl.java @@ -0,0 +1,194 @@ +package com.android.systemui.shared; +// TODO(b/303773055): Remove the annotation after access issue is resolved. +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import java.nio.file.Files; +import java.nio.file.Paths; +/** @hide */ +public final class FeatureFlagsImpl implements FeatureFlags { + private static final boolean isReadFromNew = Files.exists(Paths.get("/metadata/aconfig/boot/enable_only_new_storage")); + private static volatile boolean isCached = false; + private static volatile boolean biometrics_framework_is_cached = false; + private static volatile boolean systemui_is_cached = false; + private static boolean bouncerAreaExclusion = true; + private static boolean enableHomeDelay = false; + private static boolean exampleSharedFlag = false; + private static boolean returnAnimationFrameworkLibrary = false; + private static boolean shadeAllowBackGesture = false; + private static boolean sidefpsControllerRefactor = true; + + + private void init() { + boolean foundPackage = true; + + sidefpsControllerRefactor = foundPackage; + + + bouncerAreaExclusion = foundPackage; + + + enableHomeDelay = foundPackage; + + + exampleSharedFlag = foundPackage; + + + returnAnimationFrameworkLibrary = foundPackage ; + + + shadeAllowBackGesture = foundPackage; + + isCached = true; + } + + + + + private void load_overrides_biometrics_framework() { + try { + Properties properties = DeviceConfig.getProperties("biometrics_framework"); + sidefpsControllerRefactor = + properties.getBoolean(Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR, true); + } catch (NullPointerException e) { + throw new RuntimeException( + "Cannot read value from namespace biometrics_framework " + + "from DeviceConfig. It could be that the code using flag " + + "executed before SettingsProvider initialization. Please use " + + "fixed read-only flag by adding is_fixed_read_only: true in " + + "flag declaration.", + e + ); + } + biometrics_framework_is_cached = true; + } + + private void load_overrides_systemui() { + try { + Properties properties = DeviceConfig.getProperties("systemui"); + bouncerAreaExclusion = + properties.getBoolean(Flags.FLAG_BOUNCER_AREA_EXCLUSION, true); + enableHomeDelay = + properties.getBoolean(Flags.FLAG_ENABLE_HOME_DELAY, false); + exampleSharedFlag = + properties.getBoolean(Flags.FLAG_EXAMPLE_SHARED_FLAG, false); + returnAnimationFrameworkLibrary = + properties.getBoolean(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, false); + shadeAllowBackGesture = + properties.getBoolean(Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, false); + } catch (NullPointerException e) { + throw new RuntimeException( + "Cannot read value from namespace systemui " + + "from DeviceConfig. It could be that the code using flag " + + "executed before SettingsProvider initialization. Please use " + + "fixed read-only flag by adding is_fixed_read_only: true in " + + "flag declaration.", + e + ); + } + systemui_is_cached = true; + } + + @Override + + + public boolean bouncerAreaExclusion() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return bouncerAreaExclusion; + + } + + @Override + + + public boolean enableHomeDelay() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return enableHomeDelay; + + } + + @Override + + + public boolean exampleSharedFlag() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return exampleSharedFlag; + + } + + @Override + + + public boolean returnAnimationFrameworkLibrary() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return returnAnimationFrameworkLibrary; + + } + + @Override + + + public boolean shadeAllowBackGesture() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!systemui_is_cached) { + load_overrides_systemui(); + } + } + return shadeAllowBackGesture; + + } + + @Override + + + public boolean sidefpsControllerRefactor() { + if (isReadFromNew) { + if (!isCached) { + init(); + } + } else { + if (!biometrics_framework_is_cached) { + load_overrides_biometrics_framework(); + } + } + return sidefpsControllerRefactor; + + } + +} + diff --git a/flags/src/com/android/systemui/shared/Flags.java b/flags/src/com/android/systemui/shared/Flags.java new file mode 100644 index 0000000000..4b817be4ef --- /dev/null +++ b/flags/src/com/android/systemui/shared/Flags.java @@ -0,0 +1,50 @@ +package com.android.systemui.shared; +// TODO(b/303773055): Remove the annotation after access issue is resolved. +/** @hide */ +public final class Flags { + /** @hide */ + public static final String FLAG_BOUNCER_AREA_EXCLUSION = "com.android.systemui.shared.bouncer_area_exclusion"; + /** @hide */ + public static final String FLAG_ENABLE_HOME_DELAY = "com.android.systemui.shared.enable_home_delay"; + /** @hide */ + public static final String FLAG_EXAMPLE_SHARED_FLAG = "com.android.systemui.shared.example_shared_flag"; + /** @hide */ + public static final String FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY = "com.android.systemui.shared.return_animation_framework_library"; + /** @hide */ + public static final String FLAG_SHADE_ALLOW_BACK_GESTURE = "com.android.systemui.shared.shade_allow_back_gesture"; + /** @hide */ + public static final String FLAG_SIDEFPS_CONTROLLER_REFACTOR = "com.android.systemui.shared.sidefps_controller_refactor"; + + + public static boolean bouncerAreaExclusion() { + return FEATURE_FLAGS.bouncerAreaExclusion(); + } + + + public static boolean enableHomeDelay() { + return FEATURE_FLAGS.enableHomeDelay(); + } + + + public static boolean exampleSharedFlag() { + return FEATURE_FLAGS.exampleSharedFlag(); + } + + + public static boolean returnAnimationFrameworkLibrary() { + return FEATURE_FLAGS.returnAnimationFrameworkLibrary(); + } + + + public static boolean shadeAllowBackGesture() { + return FEATURE_FLAGS.shadeAllowBackGesture(); + } + + + public static boolean sidefpsControllerRefactor() { + return FEATURE_FLAGS.sidefpsControllerRefactor(); + } + + private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); + +} diff --git a/lawnchair/src/app/lawnchair/allapps/views/SearchResultIcon.kt b/lawnchair/src/app/lawnchair/allapps/views/SearchResultIcon.kt index 74f6ff93e3..901ca58ae4 100644 --- a/lawnchair/src/app/lawnchair/allapps/views/SearchResultIcon.kt +++ b/lawnchair/src/app/lawnchair/allapps/views/SearchResultIcon.kt @@ -19,6 +19,7 @@ import com.android.launcher3.BubbleTextView import com.android.launcher3.LauncherAppState import com.android.launcher3.LauncherSettings import com.android.launcher3.R +import com.android.launcher3.icons.BaseIconFactory import com.android.launcher3.icons.BitmapInfo import com.android.launcher3.icons.IconProvider import com.android.launcher3.icons.LauncherIcons @@ -209,7 +210,7 @@ class SearchResultIcon(context: Context, attrs: AttributeSet?) : info.hasFlags(SearchActionItemInfo.FLAG_PRIMARY_ICON_FROM_TITLE) -> li.createIconBitmap("${info.title}", packageIcon.color) icon == null -> packageIcon - else -> li.createBadgedIconBitmap(icon.loadDrawable(context), info.user, target.packageName != SHORTCUT) + else -> icon.loadDrawable(context)?.let { li.createBadgedIconBitmap(it, BaseIconFactory.IconOptions().setUser(info.user)) } } if (info.hasFlags(SearchActionItemInfo.FLAG_BADGE_WITH_COMPONENT_NAME) && target.extras.containsKey("class")) { try { @@ -218,7 +219,7 @@ class SearchResultIcon(context: Context, attrs: AttributeSet?) : ComponentName(target.packageName, target.extras.getString("class")!!) val activityInfo = context.packageManager.getActivityInfo(componentName, 0) val activityIcon = iconProvider.getIcon(activityInfo) - val bitmap = li.createIconBitmap(activityIcon, 1f, iconSize) + val bitmap = li.createIconBitmap(activityIcon, 1f) val bitmapInfo = BitmapInfo.of(bitmap, packageIcon.color) info.bitmap = li.badgeBitmap(info.bitmap.icon, bitmapInfo) } catch (_: PackageManager.NameNotFoundException) { diff --git a/lawnchair/src/app/lawnchair/nexuslauncher/OverlayCallbackImpl.kt b/lawnchair/src/app/lawnchair/nexuslauncher/OverlayCallbackImpl.kt index 42f91053a0..6aa87d8eeb 100644 --- a/lawnchair/src/app/lawnchair/nexuslauncher/OverlayCallbackImpl.kt +++ b/lawnchair/src/app/lawnchair/nexuslauncher/OverlayCallbackImpl.kt @@ -1,9 +1,7 @@ package app.lawnchair.nexuslauncher -import android.app.Activity import android.content.Context import android.content.pm.ApplicationInfo -import android.os.Bundle import app.lawnchair.FeedBridge import app.lawnchair.LawnchairLauncher import app.lawnchair.preferences2.PreferenceManager2 @@ -78,32 +76,32 @@ class OverlayCallbackImpl(private val mLauncher: LawnchairLauncher) : mClient.hideOverlay(duration) } - override fun startSearch(config: ByteArray?, extras: Bundle?): Boolean = false - - override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit - - override fun onActivityStarted(activity: Activity) { - mClient.onStart() - } - - override fun onActivityResumed(activity: Activity) { - mClient.onResume() - } - - override fun onActivityPaused(activity: Activity) { - mClient.onPause() - } - - override fun onActivityStopped(activity: Activity) { - mClient.onStop() - } - - override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit - - override fun onActivityDestroyed(activity: Activity) { - mClient.onDestroy() - mClient.mDestroyed = true - } +// override fun startSearch(config: ByteArray?, extras: Bundle?): Boolean = false +// +// override fun onActivityCreated(activity: Activity, bundle: Bundle?) = Unit +// +// override fun onActivityStarted(activity: Activity) { +// mClient.onStart() +// } +// +// override fun onActivityResumed(activity: Activity) { +// mClient.onResume() +// } +// +// override fun onActivityPaused(activity: Activity) { +// mClient.onPause() +// } +// +// override fun onActivityStopped(activity: Activity) { +// mClient.onStop() +// } +// +// override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) = Unit +// +// override fun onActivityDestroyed(activity: Activity) { +// mClient.onDestroy() +// mClient.mDestroyed = true +// } override fun onOverlayScrollChanged(progress: Float) { mLauncherOverlayCallbacks?.onOverlayScrollChanged(progress) diff --git a/platform_frameworks_libs_systemui b/platform_frameworks_libs_systemui index 2a60fa6381..a80d6c7db7 160000 --- a/platform_frameworks_libs_systemui +++ b/platform_frameworks_libs_systemui @@ -1 +1 @@ -Subproject commit 2a60fa638116b3aab60f914236f8d3301ed6df67 +Subproject commit a80d6c7db7334c6368d02a2b91b6010c9526c6c8 diff --git a/settings.gradle b/settings.gradle index 62190881b6..2d9db61aed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -57,8 +57,13 @@ project(':animationlib').projectDir = new File(rootDir, 'platform_frameworks_lib include ':hidden-api' include ':systemUIShared' - +include ':systemUIPlugin' +include ':systemUIPluginCore' +include ':systemUICommon' +include ':systemUILog' +include ':systemUIAnim' include ':systemUnFold' +include ':systemUIViewCapture' include ':compatLib' include ':compatLib:compatLibVQ' @@ -72,3 +77,4 @@ include ':baseline-profile' include ':androidx-lib' include ':flags' +include ':wmshell' diff --git a/systemUIAnim/Android.bp b/systemUIAnim/Android.bp new file mode 100644 index 0000000000..6f53b42371 --- /dev/null +++ b/systemUIAnim/Android.bp @@ -0,0 +1,73 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + + name: "SystemUIAnimationLib", + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + exclude_srcs: [ + "src/com/android/systemui/surfaceeffects/**/*.java", + "src/com/android/systemui/surfaceeffects/**/*.kt", + ], + + resource_dirs: [ + "res", + ], + + static_libs: [ + "androidx.core_core-animation-nodeps", + "androidx.core_core-ktx", + "androidx.annotation_annotation", + "SystemUIShaderLib", + "animationlib", + ], + + manifest: "AndroidManifest.xml", + kotlincflags: ["-Xjvm-default=all"], +} + +android_library { + name: "SystemUIShaderLib", + + srcs: [ + "src/com/android/systemui/surfaceeffects/**/*.java", + "src/com/android/systemui/surfaceeffects/**/*.kt", + ], + + static_libs: [ + "androidx.core_core-animation-nodeps", + "androidx.core_core-ktx", + "androidx.annotation_annotation", + ], + + manifest: "AndroidManifest.xml", + kotlincflags: ["-Xjvm-default=all"], + + // sdk_version must be specified, otherwise it compiles against private APIs. + min_sdk_version: "33", + sdk_version: "current", +} diff --git a/systemUIAnim/AndroidManifest.xml b/systemUIAnim/AndroidManifest.xml new file mode 100644 index 0000000000..321cc53142 --- /dev/null +++ b/systemUIAnim/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/systemUIAnim/build.gradle b/systemUIAnim/build.gradle new file mode 100644 index 0000000000..b54d23b517 --- /dev/null +++ b/systemUIAnim/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace "com.android.systemui.animation" + buildFeatures { + aidl true + } + sourceSets { + main { + java.srcDirs = ['src'] + aidl.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + res.srcDirs = ['res'] + } + } +} + +addFrameworkJar('framework-15.jar') +compileOnlyCommonJars() + +dependencies { + implementation 'androidx.core:core-animation:1.0.0-alpha02' + implementation 'androidx.core:core-ktx:1.9.0' + compileOnly projects.animationlib + compileOnly projects.wmshell + compileOnly projects.flags + +} diff --git a/systemUIAnim/res/anim/launch_dialog_enter.xml b/systemUIAnim/res/anim/launch_dialog_enter.xml new file mode 100644 index 0000000000..c6b87d38f7 --- /dev/null +++ b/systemUIAnim/res/anim/launch_dialog_enter.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/systemUIAnim/res/anim/launch_dialog_exit.xml b/systemUIAnim/res/anim/launch_dialog_exit.xml new file mode 100644 index 0000000000..a0f441eaee --- /dev/null +++ b/systemUIAnim/res/anim/launch_dialog_exit.xml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/systemUIAnim/res/values/ids.xml b/systemUIAnim/res/values/ids.xml new file mode 100644 index 0000000000..1a224ac7c1 --- /dev/null +++ b/systemUIAnim/res/values/ids.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/systemUIAnim/res/values/styles.xml b/systemUIAnim/res/values/styles.xml new file mode 100644 index 0000000000..3019eeef23 --- /dev/null +++ b/systemUIAnim/res/values/styles.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/systemUIAnim/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/systemUIAnim/src/com/android/systemui/animation/ActivityTransitionAnimator.kt new file mode 100644 index 0000000000..c14ee62081 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -0,0 +1,1370 @@ +/* + * 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.systemui.animation + +import android.app.ActivityManager +import android.app.ActivityTaskManager +import android.app.PendingIntent +import android.app.TaskInfo +import android.app.WindowConfiguration +import android.content.ComponentName +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Rect +import android.graphics.RectF +import android.os.Binder +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.RemoteException +import android.util.Log +import android.view.IRemoteAnimationFinishedCallback +import android.view.IRemoteAnimationRunner +import android.view.RemoteAnimationAdapter +import android.view.RemoteAnimationTarget +import android.view.SyncRtSurfaceTransactionApplier +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.view.animation.PathInterpolator +import android.window.RemoteTransition +import android.window.TransitionFilter +import androidx.annotation.AnyThread +import androidx.annotation.BinderThread +import androidx.annotation.UiThread +import com.android.app.animation.Interpolators +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.policy.ScreenDecorationsUtils +import com.android.systemui.Flags.activityTransitionUseLargestWindow +import com.android.systemui.Flags.translucentOccludingActivityFix +import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary +import com.android.wm.shell.shared.IShellTransitions +import com.android.wm.shell.shared.ShellTransitions +import java.util.concurrent.Executor +import kotlin.math.roundToInt + +private const val TAG = "ActivityTransitionAnimator" + +/** + * A class that allows activities to be started in a seamless way from a view that is transforming + * nicely into the starting window. + */ +class ActivityTransitionAnimator +@JvmOverloads +constructor( + /** The executor that runs on the main thread. */ + private val mainExecutor: Executor, + + /** The object used to register ephemeral returns and long-lived transitions. */ + private val transitionRegister: TransitionRegister? = null, + + /** The animator used when animating a View into an app. */ + private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor), + + /** The animator used when animating a Dialog into an app. */ + // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to + // TIMINGS.contentBeforeFadeOutDuration. + private val dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor), + + /** + * Whether we should disable the WindowManager timeout. This should be set to true in tests + * only. + */ + // TODO(b/301385865): Remove this flag. + private val disableWmTimeout: Boolean = false, +) { + @JvmOverloads + constructor( + mainExecutor: Executor, + shellTransitions: ShellTransitions, + transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor), + dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor), + disableWmTimeout: Boolean = false, + ) : this( + mainExecutor, + TransitionRegister.fromShellTransitions(shellTransitions), + transitionAnimator, + dialogToAppAnimator, + disableWmTimeout, + ) + + @JvmOverloads + constructor( + mainExecutor: Executor, + iShellTransitions: IShellTransitions, + transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor), + dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor), + disableWmTimeout: Boolean = false, + ) : this( + mainExecutor, + TransitionRegister.fromIShellTransitions(iShellTransitions), + transitionAnimator, + dialogToAppAnimator, + disableWmTimeout, + ) + + companion object { + /** The timings when animating a View into an app. */ + @JvmField + val TIMINGS = + TransitionAnimator.Timings( + totalDuration = 500L, + contentBeforeFadeOutDelay = 0L, + contentBeforeFadeOutDuration = 150L, + contentAfterFadeInDelay = 150L, + contentAfterFadeInDuration = 183L + ) + + /** + * The timings when animating a Dialog into an app. We need to wait at least 200ms before + * showing the app (which is under the dialog window) so that the dialog window dim is fully + * faded out, to avoid flicker. + */ + val DIALOG_TIMINGS = + TIMINGS.copy(contentBeforeFadeOutDuration = 200L, contentAfterFadeInDelay = 200L) + + /** The interpolators when animating a View or a dialog into an app. */ + val INTERPOLATORS = + TransitionAnimator.Interpolators( + positionInterpolator = Interpolators.EMPHASIZED, + positionXInterpolator = Interpolators.EMPHASIZED_COMPLEMENT, + contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, + contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f) + ) + + // TODO(b/288507023): Remove this flag. + @JvmField val DEBUG_TRANSITION_ANIMATION = Build.IS_DEBUGGABLE + + /** Durations & interpolators for the navigation bar fading in & out. */ + private const val ANIMATION_DURATION_NAV_FADE_IN = 266L + private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L + private val ANIMATION_DELAY_NAV_FADE_IN = + TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN + + private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE + private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) + + /** The time we wait before timing out the remote animation after starting the intent. */ + private const val TRANSITION_TIMEOUT = 1_000L + + /** + * The time we wait before we Log.wtf because the remote animation was neither started or + * cancelled by WM. + */ + private const val LONG_TRANSITION_TIMEOUT = 5_000L + + private fun defaultTransitionAnimator(mainExecutor: Executor): TransitionAnimator { + return TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS) + } + + private fun defaultDialogToAppAnimator(mainExecutor: Executor): TransitionAnimator { + return TransitionAnimator(mainExecutor, DIALOG_TIMINGS, INTERPOLATORS) + } + } + + /** + * The callback of this animator. This should be set before any call to + * [start(Pending)IntentWithAnimation]. + */ + var callback: Callback? = null + + /** The set of [Listener] that should be notified of any animation started by this animator. */ + private val listeners = LinkedHashSet() + + /** Top-level listener that can be used to notify all registered [listeners]. */ + private val lifecycleListener = + object : Listener { + override fun onTransitionAnimationStart() { + listeners.forEach { it.onTransitionAnimationStart() } + } + + override fun onTransitionAnimationEnd() { + listeners.forEach { it.onTransitionAnimationEnd() } + } + + override fun onTransitionAnimationProgress(linearProgress: Float) { + listeners.forEach { it.onTransitionAnimationProgress(linearProgress) } + } + + override fun onTransitionAnimationCancelled() { + listeners.forEach { it.onTransitionAnimationCancelled() } + } + } + + /** Book-keeping for long-lived transitions that are currently registered. */ + private val longLivedTransitions = + HashMap>() + + /** + * Start an intent and animate the opening window. The intent will be started by running + * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch + * result. [controller] is responsible from animating the view from which the intent was started + * in [Controller.onTransitionAnimationProgress]. No animation will start if there is no window + * opening. + * + * If [controller] is null or [animate] is false, then the intent will be started and no + * animation will run. + * + * If possible, you should pass the [packageName] of the intent that will be started so that + * trampoline activity launches will also be animated. + * + * If the device is currently locked, the user will have to unlock it before the intent is + * started unless [showOverLockscreen] is true. In that case, the activity will be started + * directly over the lockscreen. + * + * This method will throw any exception thrown by [intentStarter]. + */ + @JvmOverloads + fun startIntentWithAnimation( + controller: Controller?, + animate: Boolean = true, + packageName: String? = null, + showOverLockscreen: Boolean = false, + intentStarter: (RemoteAnimationAdapter?) -> Int + ) { + if (controller == null || !animate) { + Log.i(TAG, "Starting intent with no animation") + intentStarter(null) + controller?.callOnIntentStartedOnMainThread(willAnimate = false) + return + } + + val callback = + this.callback + ?: throw IllegalStateException( + "ActivityTransitionAnimator.callback must be set before using this animator" + ) + val runner = createRunner(controller) + val runnerDelegate = runner.delegate!! + val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen + + // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the + // keyguard with the animation + val animationAdapter = + if (!hideKeyguardWithAnimation) { + RemoteAnimationAdapter( + runner, + TIMINGS.totalDuration, + TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */ + ) + } else { + null + } + + // Register the remote animation for the given package to also animate trampoline + // activity launches. + if (packageName != null && animationAdapter != null) { + try { + ActivityTaskManager.getService() + .registerRemoteAnimationForNextActivityStart( + packageName, + animationAdapter, + null /* launchCookie */ + ) + } catch (e: RemoteException) { + Log.w(TAG, "Unable to register the remote animation", e) + } + } + + if (animationAdapter != null && controller.transitionCookie != null) { + registerEphemeralReturnAnimation(controller, transitionRegister) + } + + val launchResult = intentStarter(animationAdapter) + + // Only animate if the app is not already on top and will be opened, unless we are on the + // keyguard. + val willAnimate = + launchResult == ActivityManager.START_TASK_TO_FRONT || + launchResult == ActivityManager.START_SUCCESS || + (launchResult == ActivityManager.START_DELIVERED_TO_TOP && + hideKeyguardWithAnimation) + + Log.i( + TAG, + "launchResult=$launchResult willAnimate=$willAnimate " + + "hideKeyguardWithAnimation=$hideKeyguardWithAnimation" + ) + controller.callOnIntentStartedOnMainThread(willAnimate) + + // If we expect an animation, post a timeout to cancel it in case the remote animation is + // never started. + if (willAnimate) { + runnerDelegate.postTimeouts() + + // Hide the keyguard using the launch animation instead of the default unlock animation. + if (hideKeyguardWithAnimation) { + callback.hideKeyguardWithAnimation(runner) + } + } else { + // We need to make sure delegate references are dropped to avoid memory leaks. + runner.dispose() + } + } + + private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) { + if (Looper.myLooper() != Looper.getMainLooper()) { + mainExecutor.execute { callOnIntentStartedOnMainThread(willAnimate) } + } else { + if (DEBUG_TRANSITION_ANIMATION) { + Log.d( + TAG, + "Calling controller.onIntentStarted(willAnimate=$willAnimate) " + + "[controller=$this]" + ) + } + this.onIntentStarted(willAnimate) + } + } + + /** + * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a + * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful + * for Java caller starting a [PendingIntent]. + * + * If possible, you should pass the [packageName] of the intent that will be started so that + * trampoline activity launches will also be animated. + */ + @Throws(PendingIntent.CanceledException::class) + @JvmOverloads + fun startPendingIntentWithAnimation( + controller: Controller?, + animate: Boolean = true, + packageName: String? = null, + showOverLockscreen: Boolean = false, + intentStarter: PendingIntentStarter + ) { + startIntentWithAnimation(controller, animate, packageName, showOverLockscreen) { + intentStarter.startPendingIntent(it) + } + } + + /** + * Uses [transitionRegister] to set up the return animation for the given [launchController]. + * + * De-registration is set up automatically once the return animation is run. + * + * TODO(b/339194555): automatically de-register when the launchable is detached. + */ + private fun registerEphemeralReturnAnimation( + launchController: Controller, + transitionRegister: TransitionRegister? + ) { + if (!returnAnimationFrameworkLibrary()) return + + var cleanUpRunnable: Runnable? = null + val returnRunner = + createRunner( + object : DelegateTransitionAnimatorController(launchController) { + override val isLaunching = false + + override fun onTransitionAnimationCancelled( + newKeyguardOccludedState: Boolean? + ) { + super.onTransitionAnimationCancelled(newKeyguardOccludedState) + cleanUp() + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + super.onTransitionAnimationEnd(isExpandingFullyAbove) + cleanUp() + } + + private fun cleanUp() { + cleanUpRunnable?.run() + } + } + ) + + // mTypeSet and mModes match back signals only, and not home. This is on purpose, because + // we only want ephemeral return animations triggered in these scenarios. + val filter = + TransitionFilter().apply { + mTypeSet = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK) + mRequirements = + arrayOf( + TransitionFilter.Requirement().apply { + mLaunchCookie = launchController.transitionCookie + mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK) + } + ) + } + val transition = + RemoteTransition( + RemoteAnimationRunnerCompat.wrap(returnRunner), + "${launchController.transitionCookie}_returnTransition" + ) + + transitionRegister?.register(filter, transition) + cleanUpRunnable = Runnable { transitionRegister?.unregister(transition) } + } + + /** Add a [Listener] that can listen to transition animations. */ + fun addListener(listener: Listener) { + listeners.add(listener) + } + + /** Remove a [Listener]. */ + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + /** Create a new animation [Runner] controlled by [controller]. */ + @VisibleForTesting + fun createRunner(controller: Controller): Runner { + // Make sure we use the modified timings when animating a dialog into an app. + val transitionAnimator = + if (controller.isDialogLaunch) { + dialogToAppAnimator + } else { + transitionAnimator + } + + return Runner(controller, callback!!, transitionAnimator, lifecycleListener) + } + + interface PendingIntentStarter { + /** + * Start a pending intent using the provided [animationAdapter] and return the launch + * result. + */ + @Throws(PendingIntent.CanceledException::class) + fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int + } + + interface Callback { + /** Whether we are currently on the keyguard or not. */ + fun isOnKeyguard(): Boolean = false + + /** Hide the keyguard and animate using [runner]. */ + fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner) { + throw UnsupportedOperationException() + } + + /* Get the background color of [task]. */ + fun getBackgroundColor(task: TaskInfo): Int + } + + interface Listener { + /** Called when an activity transition animation started. */ + fun onTransitionAnimationStart() {} + + /** + * Called when an activity transition animation is finished. This will be called if and only + * if [onTransitionAnimationStart] was called earlier. + */ + fun onTransitionAnimationEnd() {} + + /** + * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called + * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was + * called before the cancellation. + */ + fun onTransitionAnimationCancelled() {} + + /** Called when an activity transition animation made progress. */ + fun onTransitionAnimationProgress(linearProgress: Float) {} + } + + /** + * A controller that takes care of applying the animation to an expanding view. + * + * Note that all callbacks (onXXX methods) are all called on the main thread. + */ + interface Controller : TransitionAnimator.Controller { + companion object { + /** + * Return a [Controller] that will animate and expand [view] into the opening window. + * + * Important: The view must be attached to a [ViewGroup] when calling this function and + * during the animation. For safety, this method will return null when it is not. The + * view must also implement [LaunchableView], otherwise this method will throw. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be + * properly animated. + */ + @JvmOverloads + @JvmStatic + fun fromView( + view: View, + cujType: Int? = null, + cookie: TransitionCookie? = null, + component: ComponentName? = null, + returnCujType: Int? = null + ): Controller? { + // Make sure the View we launch from implements LaunchableView to avoid visibility + // issues. + if (view !is LaunchableView) { + throw IllegalArgumentException( + "An ActivityTransitionAnimator.Controller was created from a View that " + + "does not implement LaunchableView. This can lead to subtle bugs " + + "where the visibility of the View we are launching from is not what " + + "we expected." + ) + } + + if (view.parent !is ViewGroup) { + Log.e( + TAG, + "Skipping animation as view $view is not attached to a ViewGroup", + Exception() + ) + return null + } + + return GhostedViewTransitionAnimatorController( + view, + cujType, + cookie, + component, + returnCujType + ) + } + } + + /** + * Whether this controller is controlling a dialog launch. This will be used to adapt the + * timings, making sure we don't show the app until the dialog dim had the time to fade out. + */ + // TODO(b/218989950): Remove this. + val isDialogLaunch: Boolean + get() = false + + /** + * Whether the expandable controller by this [Controller] is below the window that is going + * to be animated. + * + * This should be `false` when animating an app from or to the shade or status bar, given + * that they are drawn above all apps. This is usually `true` when using this animator in a + * normal app or a launcher, that are drawn below the animating activity/window. + */ + val isBelowAnimatingWindow: Boolean + get() = false + + /** + * The cookie associated with the transition controlled by this [Controller]. + * + * This should be defined for all return [Controller] (when [isLaunching] is false) and for + * their associated launch [Controller]s. + * + * For the recommended format, see [TransitionCookie]. + */ + val transitionCookie: TransitionCookie? + get() = null + + /** + * The [ComponentName] of the activity whose window is tied to this [Controller]. + * + * This is used as a fallback when a cookie is defined but there is no match (e.g. when a + * matching activity was launched by a mean different from the launchable in this + * [Controller]), and should be defined for all long-lived registered [Controller]s. + */ + val component: ComponentName? + get() = null + + /** + * The intent was started. If [willAnimate] is false, nothing else will happen and the + * animation will not be started. + */ + fun onIntentStarted(willAnimate: Boolean) {} + + /** + * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called + * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was + * called before the cancellation. + * + * If this transition animation affected the occlusion state of the keyguard, WM will + * provide us with [newKeyguardOccludedState] so that we can set the occluded state + * appropriately. + */ + fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {} + } + + /** + * Registers [controller] as a long-lived transition handler for launch and return animations. + * + * The [controller] will only be used for transitions matching the [TransitionCookie] defined + * within it, or the [ComponentName] if the cookie matching fails. Both fields are mandatory for + * this registration. + */ + fun register(controller: Controller) { + check(returnAnimationFrameworkLibrary()) { + "Long-lived registrations cannot be used when the returnAnimationFrameworkLibrary " + + "flag is disabled" + } + + if (transitionRegister == null) { + throw IllegalStateException( + "A RemoteTransitionRegister must be provided when creating this animator in " + + "order to use long-lived animations" + ) + } + + val cookie = + controller.transitionCookie + ?: throw IllegalStateException( + "A cookie must be defined in order to use long-lived animations" + ) + val component = + controller.component + ?: throw IllegalStateException( + "A component must be defined in order to use long-lived animations" + ) + + // Make sure that any previous registrations linked to the same cookie are gone. + unregister(cookie) + + val launchFilter = + TransitionFilter().apply { + mRequirements = + arrayOf( + TransitionFilter.Requirement().apply { + mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD + mModes = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT) + mTopActivity = component + } + ) + } + val launchRemoteTransition = + RemoteTransition( + RemoteAnimationRunnerCompat.wrap(createRunner(controller)), + "${cookie}_launchTransition" + ) + transitionRegister.register(launchFilter, launchRemoteTransition) + + val returnController = + object : Controller by controller { + override val isLaunching: Boolean = false + } + val returnFilter = + TransitionFilter().apply { + mRequirements = + arrayOf( + TransitionFilter.Requirement().apply { + mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD + mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK) + mTopActivity = component + } + ) + } + val returnRemoteTransition = + RemoteTransition( + RemoteAnimationRunnerCompat.wrap(createRunner(returnController)), + "${cookie}_returnTransition" + ) + transitionRegister.register(returnFilter, returnRemoteTransition) + + longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition) + } + + /** Unregisters all controllers previously registered that contain [cookie]. */ + fun unregister(cookie: TransitionCookie) { + val transitions = longLivedTransitions[cookie] ?: return + transitionRegister?.unregister(transitions.first) + transitionRegister?.unregister(transitions.second) + longLivedTransitions.remove(cookie) + } + + /** + * Invokes [onAnimationComplete] when animation is either cancelled or completed. Delegates all + * events to the passed [delegate]. + */ + @VisibleForTesting + inner class DelegatingAnimationCompletionListener( + private val delegate: Listener?, + private val onAnimationComplete: () -> Unit + ) : Listener { + var cancelled = false + + override fun onTransitionAnimationStart() { + delegate?.onTransitionAnimationStart() + } + + override fun onTransitionAnimationProgress(linearProgress: Float) { + delegate?.onTransitionAnimationProgress(linearProgress) + } + + override fun onTransitionAnimationEnd() { + delegate?.onTransitionAnimationEnd() + if (!cancelled) { + onAnimationComplete.invoke() + } + } + + override fun onTransitionAnimationCancelled() { + cancelled = true + delegate?.onTransitionAnimationCancelled() + onAnimationComplete.invoke() + } + } + + @VisibleForTesting + inner class Runner( + controller: Controller, + callback: Callback, + /** The animator to use to animate the window transition. */ + transitionAnimator: TransitionAnimator, + /** Listener for animation lifecycle events. */ + listener: Listener? = null + ) : IRemoteAnimationRunner.Stub() { + // This is being passed across IPC boundaries and cycles (through PendingIntentRecords, + // etc.) are possible. So we need to make sure we drop any references that might + // transitively cause leaks when we're done with animation. + @VisibleForTesting var delegate: AnimationDelegate? + + init { + delegate = + AnimationDelegate( + mainExecutor, + controller, + callback, + DelegatingAnimationCompletionListener(listener, this::dispose), + transitionAnimator, + disableWmTimeout, + ) + } + + @BinderThread + override fun onAnimationStart( + transit: Int, + apps: Array?, + wallpapers: Array?, + nonApps: Array?, + finishedCallback: IRemoteAnimationFinishedCallback? + ) { + val delegate = delegate + mainExecutor.execute { + if (delegate == null) { + Log.i(TAG, "onAnimationStart called after completion") + // Animation started too late and timed out already. We need to still + // signal back that we're done with it. + finishedCallback?.onAnimationFinished() + } else { + delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback) + } + } + } + + @BinderThread + override fun onAnimationCancelled() { + val delegate = delegate + mainExecutor.execute { + delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion") + delegate?.onAnimationCancelled() + } + } + + @AnyThread + fun dispose() { + // Drop references to animation controller once we're done with the animation + // to avoid leaking. + mainExecutor.execute { delegate = null } + } + } + + class AnimationDelegate + @JvmOverloads + constructor( + private val mainExecutor: Executor, + private val controller: Controller, + private val callback: Callback, + /** Listener for animation lifecycle events. */ + private val listener: Listener? = null, + /** The animator to use to animate the window transition. */ + private val transitionAnimator: TransitionAnimator = + defaultTransitionAnimator(mainExecutor), + + /** + * Whether we should disable the WindowManager timeout. This should be set to true in tests + * only. + */ + // TODO(b/301385865): Remove this flag. + disableWmTimeout: Boolean = false, + ) : RemoteAnimationDelegate { + private val transitionContainer = controller.transitionContainer + private val context = transitionContainer.context + private val transactionApplierView = + controller.openingWindowSyncView ?: controller.transitionContainer + private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView) + private val timeoutHandler = + if (!disableWmTimeout) { + Handler(Looper.getMainLooper()) + } else { + null + } + + private val matrix = Matrix() + private val invertMatrix = Matrix() + private var windowCrop = Rect() + private var windowCropF = RectF() + private var timedOut = false + private var cancelled = false + private var animation: TransitionAnimator.Animation? = null + + /** + * A timeout to cancel the transition animation if the remote animation is not started or + * cancelled within [TRANSITION_TIMEOUT] milliseconds after the intent was started. + * + * Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise + * it will be automatically converted when posted and we wouldn't be able to remove it after + * posting it. + */ + private var onTimeout = Runnable { onAnimationTimedOut() } + + /** + * A long timeout to Log.wtf (signaling a bug in WM) when the remote animation wasn't + * started or cancelled within [LONG_TRANSITION_TIMEOUT] milliseconds after the intent was + * started. + */ + private var onLongTimeout = Runnable { + Log.wtf( + TAG, + "The remote animation was neither cancelled or started within " + + "$LONG_TRANSITION_TIMEOUT" + ) + } + + init { + // We do this check here to cover all entry points, including Launcher which doesn't + // call startIntentWithAnimation() + if (!controller.isLaunching) TransitionAnimator.checkReturnAnimationFrameworkFlag() + } + + @UiThread + internal fun postTimeouts() { + if (timeoutHandler != null) { + timeoutHandler.postDelayed(onTimeout, TRANSITION_TIMEOUT) + timeoutHandler.postDelayed(onLongTimeout, LONG_TRANSITION_TIMEOUT) + } + } + + private fun removeTimeouts() { + if (timeoutHandler != null) { + timeoutHandler.removeCallbacks(onTimeout) + timeoutHandler.removeCallbacks(onLongTimeout) + } + } + + @UiThread + override fun onAnimationStart( + @WindowManager.TransitionOldType transit: Int, + apps: Array?, + wallpapers: Array?, + nonApps: Array?, + callback: IRemoteAnimationFinishedCallback? + ) { + removeTimeouts() + + // The animation was started too late and we already notified the controller that it + // timed out. + if (timedOut) { + callback?.invoke() + return + } + + // This should not happen, but let's make sure we don't start the animation if it was + // cancelled before and we already notified the controller. + if (cancelled) { + return + } + + val window = findTargetWindowIfPossible(apps) + if (window == null) { + Log.i(TAG, "Aborting the animation as no window is opening") + callback?.invoke() + + if (DEBUG_TRANSITION_ANIMATION) { + Log.d( + TAG, + "Calling controller.onTransitionAnimationCancelled() [no window opening]" + ) + } + controller.onTransitionAnimationCancelled() + listener?.onTransitionAnimationCancelled() + return + } + + val navigationBar = + nonApps?.firstOrNull { + it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR + } + + startAnimation(window, navigationBar, callback) + } + + private fun findTargetWindowIfPossible( + apps: Array? + ): RemoteAnimationTarget? { + if (apps == null) { + return null + } + + val targetMode = + if (controller.isLaunching) { + RemoteAnimationTarget.MODE_OPENING + } else { + RemoteAnimationTarget.MODE_CLOSING + } + var candidate: RemoteAnimationTarget? = null + + for (it in apps) { + if (it.mode == targetMode) { + if (activityTransitionUseLargestWindow()) { + if (returnAnimationFrameworkLibrary()) { + // If the controller contains a cookie, _only_ match if either the + // candidate contains the matching cookie, or a component is also + // defined and is a match. + if ( + controller.transitionCookie != null && + it.taskInfo + ?.launchCookies + ?.contains(controller.transitionCookie) != true && + (controller.component == null || + it.taskInfo?.topActivity != controller.component) + ) { + continue + } + } + + if ( + candidate == null || + !it.hasAnimatingParent && candidate.hasAnimatingParent + ) { + candidate = it + continue + } + if ( + !it.hasAnimatingParent && + it.screenSpaceBounds.hasGreaterAreaThan(candidate.screenSpaceBounds) + ) { + candidate = it + } + } else { + if (!it.hasAnimatingParent) { + return it + } + if (candidate == null) { + candidate = it + } + } + } + } + + return candidate + } + + private fun startAnimation( + window: RemoteAnimationTarget, + navigationBar: RemoteAnimationTarget?, + iCallback: IRemoteAnimationFinishedCallback? + ) { + if (TransitionAnimator.DEBUG) { + Log.d(TAG, "Remote animation started") + } + + val windowBounds = window.screenSpaceBounds + val endState = + if (controller.isLaunching) { + TransitionAnimator.State( + top = windowBounds.top, + bottom = windowBounds.bottom, + left = windowBounds.left, + right = windowBounds.right + ) + } else { + controller.createAnimatorState() + } + val windowBackgroundColor = + if (translucentOccludingActivityFix() && window.isTranslucent) { + Color.TRANSPARENT + } else { + window.taskInfo?.let { callback.getBackgroundColor(it) } + ?: window.backgroundColor + } + + // TODO(b/184121838): We should somehow get the top and bottom radius of the window + // instead of recomputing isExpandingFullyAbove here. + val isExpandingFullyAbove = + transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState) + if (controller.isLaunching) { + val endRadius = getWindowRadius(isExpandingFullyAbove) + endState.topCornerRadius = endRadius + endState.bottomCornerRadius = endRadius + } + + // We animate the opening window and delegate the view expansion to [this.controller]. + val delegate = this.controller + val controller = + object : Controller by delegate { + override fun createAnimatorState(): TransitionAnimator.State { + if (isLaunching) return delegate.createAnimatorState() + val windowRadius = getWindowRadius(isExpandingFullyAbove) + return TransitionAnimator.State( + top = windowBounds.top, + bottom = windowBounds.bottom, + left = windowBounds.left, + right = windowBounds.right, + topCornerRadius = windowRadius, + bottomCornerRadius = windowRadius + ) + } + + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + listener?.onTransitionAnimationStart() + + if (DEBUG_TRANSITION_ANIMATION) { + Log.d( + TAG, + "Calling controller.onTransitionAnimationStart(" + + "isExpandingFullyAbove=$isExpandingFullyAbove) " + + "[controller=$delegate]" + ) + } + delegate.onTransitionAnimationStart(isExpandingFullyAbove) + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + listener?.onTransitionAnimationEnd() + iCallback?.invoke() + + if (DEBUG_TRANSITION_ANIMATION) { + Log.d( + TAG, + "Calling controller.onTransitionAnimationEnd(" + + "isExpandingFullyAbove=$isExpandingFullyAbove) " + + "[controller=$delegate]" + ) + } + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) + } + + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, + progress: Float, + linearProgress: Float + ) { + applyStateToWindow(window, state, linearProgress) + navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } + + listener?.onTransitionAnimationProgress(linearProgress) + delegate.onTransitionAnimationProgress(state, progress, linearProgress) + } + } + + animation = + transitionAnimator.startAnimation( + controller, + endState, + windowBackgroundColor, + fadeWindowBackgroundLayer = !controller.isBelowAnimatingWindow, + drawHole = !controller.isBelowAnimatingWindow, + ) + } + + private fun getWindowRadius(isExpandingFullyAbove: Boolean): Float { + return if (isExpandingFullyAbove) { + // Most of the time, expanding fully above the root view means + // expanding in full screen. + ScreenDecorationsUtils.getWindowCornerRadius(context) + } else { + // This usually means we are in split screen mode, so 2 out of 4 + // corners will have a radius of 0. + 0f + } + } + + private fun applyStateToWindow( + window: RemoteAnimationTarget, + state: TransitionAnimator.State, + linearProgress: Float, + ) { + if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) { + // Don't apply any transaction if the view root we synchronize with was detached or + // if the SurfaceControl associated with [window] is not valid, as + // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw. + return + } + + val screenBounds = window.screenSpaceBounds + val centerX = (screenBounds.left + screenBounds.right) / 2f + val centerY = (screenBounds.top + screenBounds.bottom) / 2f + val width = screenBounds.right - screenBounds.left + val height = screenBounds.bottom - screenBounds.top + + // Scale the window. We use the max of (widthRatio, heightRatio) so that there is no + // blank space on any side. + val widthRatio = state.width.toFloat() / width + val heightRatio = state.height.toFloat() / height + val scale = maxOf(widthRatio, heightRatio) + matrix.reset() + matrix.setScale(scale, scale, centerX, centerY) + + // Align it to the top and center it in the x-axis. + val heightChange = height * scale - height + val translationX = state.centerX - centerX + val translationY = state.top - screenBounds.top + heightChange / 2f + matrix.postTranslate(translationX, translationY) + + // Crop it. The matrix will also be applied to the crop, so we apply the inverse + // operation. Given that we only scale (by factor > 0) then translate, we can assume + // that the matrix is invertible. + val cropX = state.left.toFloat() - screenBounds.left + val cropY = state.top.toFloat() - screenBounds.top + windowCropF.set(cropX, cropY, cropX + state.width, cropY + state.height) + matrix.invert(invertMatrix) + invertMatrix.mapRect(windowCropF) + windowCrop.set( + windowCropF.left.roundToInt(), + windowCropF.top.roundToInt(), + windowCropF.right.roundToInt(), + windowCropF.bottom.roundToInt() + ) + + val windowAnimationDelay = + if (controller.isLaunching) { + TIMINGS.contentAfterFadeInDelay + } else { + TIMINGS.contentBeforeFadeOutDelay + } + val windowAnimationDuration = + if (controller.isLaunching) { + TIMINGS.contentAfterFadeInDuration + } else { + TIMINGS.contentBeforeFadeOutDuration + } + val windowProgress = + TransitionAnimator.getProgress( + TIMINGS, + linearProgress, + windowAnimationDelay, + windowAnimationDuration + ) + + // The alpha of the opening window. If it opens above the expandable, then it should + // fade in progressively. Otherwise, it should be fully opaque and will be progressively + // revealed as the window background color layer above the window fades out. + val alpha = + if (controller.isBelowAnimatingWindow) { + if (controller.isLaunching) { + INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation( + windowProgress + ) + } else { + 1 - + INTERPOLATORS.contentBeforeFadeOutInterpolator.getInterpolation( + windowProgress + ) + } + } else { + 1f + } + + // The scale will also be applied to the corner radius, so we divide by the scale to + // keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to + // make sure that the window does not draw itself behind the expanding view. This is + // especially important for lock screen animations, where the window is not clipped by + // the shade. + val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale + val params = + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash) + .withAlpha(alpha) + .withMatrix(matrix) + .withWindowCrop(windowCrop) + .withCornerRadius(cornerRadius) + .withVisibility(true) + .build() + + transactionApplier.scheduleApply(params) + } + + private fun applyStateToNavigationBar( + navigationBar: RemoteAnimationTarget, + state: TransitionAnimator.State, + linearProgress: Float + ) { + if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) { + // Don't apply any transaction if the view root we synchronize with was detached or + // if the SurfaceControl associated with [navigationBar] is not valid, as + // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw. + return + } + + val fadeInProgress = + TransitionAnimator.getProgress( + TIMINGS, + linearProgress, + ANIMATION_DELAY_NAV_FADE_IN, + ANIMATION_DURATION_NAV_FADE_OUT + ) + + val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) + if (fadeInProgress > 0) { + matrix.reset() + matrix.setTranslate( + 0f, + (state.top - navigationBar.sourceContainerBounds.top).toFloat() + ) + windowCrop.set(state.left, 0, state.right, state.height) + params + .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) + .withMatrix(matrix) + .withWindowCrop(windowCrop) + .withVisibility(true) + } else { + val fadeOutProgress = + TransitionAnimator.getProgress( + TIMINGS, + linearProgress, + 0, + ANIMATION_DURATION_NAV_FADE_OUT + ) + params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) + } + + transactionApplier.scheduleApply(params.build()) + } + + private fun onAnimationTimedOut() { + // The remote animation was cancelled by WM, so we already cancelled the transition + // animation. + if (cancelled) { + return + } + + Log.w(TAG, "Remote animation timed out") + timedOut = true + + if (DEBUG_TRANSITION_ANIMATION) { + Log.d( + TAG, + "Calling controller.onTransitionAnimationCancelled() [animation timed out]" + ) + } + controller.onTransitionAnimationCancelled() + listener?.onTransitionAnimationCancelled() + } + + @UiThread + override fun onAnimationCancelled() { + removeTimeouts() + + // The short timeout happened, so we already cancelled the transition animation. + if (timedOut) { + return + } + + Log.i(TAG, "Remote animation was cancelled") + cancelled = true + + animation?.cancel() + + if (DEBUG_TRANSITION_ANIMATION) { + Log.d( + TAG, + "Calling controller.onTransitionAnimationCancelled() [remote animation " + + "cancelled]", + ) + } + controller.onTransitionAnimationCancelled() + listener?.onTransitionAnimationCancelled() + } + + private fun IRemoteAnimationFinishedCallback.invoke() { + try { + onAnimationFinished() + } catch (e: RemoteException) { + e.printStackTrace() + } + } + + private fun Rect.hasGreaterAreaThan(other: Rect): Boolean { + return (this.width() * this.height()) > (other.width() * other.height()) + } + } + + /** + * Wraps one of the two methods we have to register remote transitions with WM Shell: + * - for in-process registrations (e.g. System UI) we use [ShellTransitions] + * - for cross-process registrations (e.g. Launcher) we use [IShellTransitions] + * + * Important: each instance of this class must wrap exactly one of the two. + */ + class TransitionRegister + private constructor( + private val shellTransitions: ShellTransitions? = null, + private val iShellTransitions: IShellTransitions? = null, + ) { + init { + assert((shellTransitions != null).xor(iShellTransitions != null)) + } + + companion object { + /** Provides a [TransitionRegister] instance wrapping [ShellTransitions]. */ + fun fromShellTransitions(shellTransitions: ShellTransitions): TransitionRegister { + return TransitionRegister(shellTransitions = shellTransitions) + } + + /** Provides a [TransitionRegister] instance wrapping [IShellTransitions]. */ + fun fromIShellTransitions(iShellTransitions: IShellTransitions): TransitionRegister { + return TransitionRegister(iShellTransitions = iShellTransitions) + } + } + + /** Register [remoteTransition] with WM Shell using the given [filter]. */ + internal fun register( + filter: TransitionFilter, + remoteTransition: RemoteTransition, + ) { + shellTransitions?.registerRemote(filter, remoteTransition) + iShellTransitions?.registerRemote(filter, remoteTransition) + } + + /** Unregister [remoteTransition] from WM Shell. */ + internal fun unregister(remoteTransition: RemoteTransition) { + shellTransitions?.unregisterRemote(remoteTransition) + iShellTransitions?.unregisterRemote(remoteTransition) + } + } + + /** + * A cookie used to uniquely identify a task launched using an + * [ActivityTransitionAnimator.Controller]. + * + * The [String] encapsulated by this class should be formatted in such a way to be unique across + * the system, but reliably constant for the same associated launchable. + * + * Recommended naming scheme: + * - DO use the fully qualified name of the class that owns the instance of the launchable, + * along with a concise and precise description of the purpose of the launchable in question. + * - DO NOT introduce uniqueness through the use of timestamps or other runtime variables that + * will change if the instance is destroyed and re-created. + * + * Example: "com.not.the.real.class.name.ShadeController_openSettingsButton" + * + * Note that sometimes (e.g. in recycler views) there could be multiple instances of the same + * launchable, and no static knowledge to adequately differentiate between them using a single + * description. In this case, the recommendation is to append a unique identifier related to the + * contents of the launchable. + * + * Example: “com.not.the.real.class.name.ToastWebResult_launchAga_id143256” + */ + data class TransitionCookie(private val cookie: String) : Binder() +} diff --git a/systemUIAnim/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/systemUIAnim/src/com/android/systemui/animation/AnimationFeatureFlags.kt new file mode 100644 index 0000000000..1c9dabbb0e --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/AnimationFeatureFlags.kt @@ -0,0 +1,6 @@ +package com.android.systemui.animation + +interface AnimationFeatureFlags { + val isPredictiveBackQsDialogAnim: Boolean + get() = false +} diff --git a/systemUIAnim/src/com/android/systemui/animation/DelegateTransitionAnimatorController.kt b/systemUIAnim/src/com/android/systemui/animation/DelegateTransitionAnimatorController.kt new file mode 100644 index 0000000000..e246562761 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/DelegateTransitionAnimatorController.kt @@ -0,0 +1,26 @@ +/* + * 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.systemui.animation + +/** + * A base class to easily create an implementation of [ActivityTransitionAnimator.Controller] which + * delegates most of its call to [delegate]. This is mostly useful for Java code which can't easily + * create such a delegated class. + */ +open class DelegateTransitionAnimatorController( + protected val delegate: ActivityTransitionAnimator.Controller +) : ActivityTransitionAnimator.Controller by delegate diff --git a/systemUIAnim/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/systemUIAnim/src/com/android/systemui/animation/DialogTransitionAnimator.kt new file mode 100644 index 0000000000..907c39d842 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/DialogTransitionAnimator.kt @@ -0,0 +1,1100 @@ +/* + * 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.systemui.animation + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.app.Dialog +import android.graphics.Color +import android.graphics.Rect +import android.os.Looper +import android.util.Log +import android.util.MathUtils +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewRootImpl +import android.view.WindowInsets +import android.view.WindowManager +import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS +import com.android.app.animation.Interpolators +import com.android.internal.jank.Cuj.CujType +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.util.maybeForceFullscreen +import com.android.systemui.util.registerAnimationOnBackInvoked +import java.util.concurrent.Executor +import kotlin.math.roundToInt + +private const val TAG = "DialogTransitionAnimator" + +/** + * A class that allows dialogs to be started in a seamless way from a view that is transforming + * nicely into the starting dialog. + * + * This animator also allows to easily animate a dialog into an activity. + * + * @see show + * @see showFromView + * @see showFromDialog + * @see createActivityTransitionController + */ +class DialogTransitionAnimator +@JvmOverloads +constructor( + private val mainExecutor: Executor, + private val callback: Callback, + private val interactionJankMonitor: InteractionJankMonitor, + private val featureFlags: AnimationFeatureFlags, + private val transitionAnimator: TransitionAnimator = + TransitionAnimator( + mainExecutor, + TIMINGS, + INTERPOLATORS, + ), + private val isForTesting: Boolean = false, +) { + private companion object { + private val TIMINGS = ActivityTransitionAnimator.TIMINGS + + // We use the same interpolator for X and Y axis to make sure the dialog does not move out + // of the screen bounds during the animation. + private val INTERPOLATORS = + ActivityTransitionAnimator.INTERPOLATORS.copy( + positionXInterpolator = + ActivityTransitionAnimator.INTERPOLATORS.positionInterpolator + ) + } + + /** + * A controller that takes care of applying the dialog launch and exit animations to the source + * that triggered the animation. + */ + interface Controller { + /** The [ViewRootImpl] of this controller. */ + val viewRoot: ViewRootImpl? + + /** + * The identity object of the source animated by this controller. This animator will ensure + * that 2 animations with the same source identity are not going to run at the same time, to + * avoid flickers when a dialog is shown from the same source more or less at the same time + * (for instance if the user clicks an expandable button twice). + */ + val sourceIdentity: Any + + /** The CUJ associated to this controller. */ + val cuj: DialogCuj? + + /** + * Move the drawing of the source in the overlay of [viewGroup]. + * + * Once this method is called, and until [stopDrawingInOverlay] is called, the source + * controlled by this Controller should be drawn in the overlay of [viewGroup] so that it is + * drawn above all other elements in the same [viewRoot]. + */ + fun startDrawingInOverlayOf(viewGroup: ViewGroup) + + /** + * Move the drawing of the source back in its original location. + * + * @see startDrawingInOverlayOf + */ + fun stopDrawingInOverlay() + + /** + * Create the [TransitionAnimator.Controller] that will be called to animate the source + * controlled by this [Controller] during the dialog launch animation. + * + * At the end of this animation, the source should *not* be visible anymore (until the + * dialog is closed and is animated back into the source). + */ + fun createTransitionController(): TransitionAnimator.Controller + + /** + * Create the [TransitionAnimator.Controller] that will be called to animate the source + * controlled by this [Controller] during the dialog exit animation. + * + * At the end of this animation, the source should be visible again. + */ + fun createExitController(): TransitionAnimator.Controller + + /** + * Whether we should animate the dialog back into the source when it is dismissed. If this + * methods returns `false`, then the dialog will simply fade out and + * [onExitAnimationCancelled] will be called. + * + * Note that even when this returns `true`, the exit animation might still be cancelled (in + * which case [onExitAnimationCancelled] will also be called). + */ + fun shouldAnimateExit(): Boolean + + /** + * Called if we decided to *not* animate the dialog into the source for some reason. This + * means that [createExitController] will *not* be called and this implementation should + * make sure that the source is back in its original state, before it was animated into the + * dialog. In particular, the source should be visible again. + */ + fun onExitAnimationCancelled() + + /** + * Return the [InteractionJankMonitor.Configuration.Builder] to be used for animations + * controlled by this controller. + */ + // TODO(b/252723237): Make this non-nullable + fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? + + companion object { + /** + * Create a [Controller] that can animate [source] to and from a dialog. + * + * Important: The view must be attached to a [ViewGroup] when calling this function and + * during the animation. For safety, this method will return null when it is not. The + * view must also implement [LaunchableView], otherwise this method will throw. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be + * properly animated. + */ + fun fromView(source: View, cuj: DialogCuj? = null): Controller? { + // Make sure the View we launch from implements LaunchableView to avoid visibility + // issues. + if (source !is LaunchableView) { + throw IllegalArgumentException( + "A DialogTransitionAnimator.Controller was created from a View that does " + + "not implement LaunchableView. This can lead to subtle bugs where " + + "the visibility of the View we are launching from is not what we " + + "expected." + ) + } + + if (source.parent !is ViewGroup) { + Log.e( + TAG, + "Skipping animation as view $source is not attached to a ViewGroup", + Exception(), + ) + return null + } + + return ViewDialogTransitionAnimatorController(source, cuj) + } + } + } + + /** + * The set of dialogs that were animated using this animator and that are still opened (not + * dismissed, but can be hidden). + */ + // TODO(b/201264644): Remove this set. + private val openedDialogs = hashSetOf() + + /** + * Show [dialog] by expanding it from [view]. If [view] is a view inside another dialog that was + * shown using this method, then we will animate from that dialog instead. + * + * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be + * animated when the dialog bounds change. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be properly + * animated. + * + * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be + * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. + */ + @JvmOverloads + fun showFromView( + dialog: Dialog, + view: View, + cuj: DialogCuj? = null, + animateBackgroundBoundsChange: Boolean = false + ) { + val controller = Controller.fromView(view, cuj) + if (controller == null) { + dialog.show() + } else { + show(dialog, controller, animateBackgroundBoundsChange) + } + } + + /** + * Show [dialog] by expanding it from a source controlled by [controller]. + * + * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be + * animated when the dialog bounds change. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be properly + * animated. + * + * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be + * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. + */ + @JvmOverloads + fun show( + dialog: Dialog, + controller: Controller, + animateBackgroundBoundsChange: Boolean = false + ) { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw IllegalStateException( + "showFromView must be called from the main thread and dialog must be created in " + + "the main thread" + ) + } + + // If the view we are launching from belongs to another dialog, then this means the caller + // intent is to launch a dialog from another dialog. + val animatedParent = + openedDialogs.firstOrNull { + it.dialog.window?.decorView?.viewRootImpl == controller.viewRoot + } + val controller = + animatedParent?.dialogContentWithBackground?.let { + Controller.fromView(it, controller.cuj) + } + ?: controller + + // Make sure we don't run the launch animation from the same source twice at the same time. + if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) { + Log.e( + TAG, + "Not running dialog launch animation from source as it is already expanded into a" + + " dialog" + ) + dialog.show() + return + } + + val animatedDialog = + AnimatedDialog( + transitionAnimator = transitionAnimator, + callback = callback, + interactionJankMonitor = interactionJankMonitor, + controller = controller, + onDialogDismissed = { openedDialogs.remove(it) }, + dialog = dialog, + animateBackgroundBoundsChange = animateBackgroundBoundsChange, + parentAnimatedDialog = animatedParent, + forceDisableSynchronization = isForTesting, + featureFlags = featureFlags, + ) + + openedDialogs.add(animatedDialog) + animatedDialog.start() + } + + /** + * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will + * allow for dismissing the whole stack. + * + * @see dismissStack + */ + fun showFromDialog( + dialog: Dialog, + animateFrom: Dialog, + cuj: DialogCuj? = null, + animateBackgroundBoundsChange: Boolean = false + ) { + val view = + openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground + if (view == null) { + Log.w( + TAG, + "Showing dialog $dialog normally as the dialog it is shown from was not shown " + + "using DialogTransitionAnimator" + ) + dialog.show() + return + } + + showFromView( + dialog, + view, + animateBackgroundBoundsChange = animateBackgroundBoundsChange, + cuj = cuj + ) + } + + /** + * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from + * the dialog that contains [View]. Note that the dialog must have been shown using this + * animator, otherwise this method will return null. + * + * The returned controller will take care of dismissing the dialog at the right time after the + * activity started, when the dialog to app animation is done (or when it is cancelled). If this + * method returns null, then the dialog won't be dismissed. + * + * @param view any view inside the dialog to animate. + */ + @JvmOverloads + fun createActivityTransitionController( + view: View, + cujType: Int? = null, + ): ActivityTransitionAnimator.Controller? { + val animatedDialog = + openedDialogs.firstOrNull { + it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl + } + ?: return null + return createActivityTransitionController(animatedDialog, cujType) + } + + /** + * Create an [ActivityTransitionAnimator.Controller] that can be used to launch an activity from + * [dialog]. Note that the dialog must have been shown using this animator, otherwise this + * method will return null. + * + * The returned controller will take care of dismissing the dialog at the right time after the + * activity started, when the dialog to app animation is done (or when it is cancelled). If this + * method returns null, then the dialog won't be dismissed. + * + * @param dialog the dialog to animate. + */ + @JvmOverloads + fun createActivityTransitionController( + dialog: Dialog, + cujType: Int? = null, + ): ActivityTransitionAnimator.Controller? { + val animatedDialog = openedDialogs.firstOrNull { it.dialog == dialog } ?: return null + return createActivityTransitionController(animatedDialog, cujType) + } + + private fun createActivityTransitionController( + animatedDialog: AnimatedDialog, + cujType: Int? = null + ): ActivityTransitionAnimator.Controller? { + // At this point, we know that the intent of the caller is to dismiss the dialog to show + // an app, so we disable the exit animation into the source because we will never want to + // run it anyways. + animatedDialog.exitAnimationDisabled = true + + val dialog = animatedDialog.dialog + + // Don't animate if the dialog is not showing or if we are locked and going to show the + // primary bouncer. + if ( + !dialog.isShowing || + (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock()) + ) { + return null + } + + val dialogContentWithBackground = animatedDialog.dialogContentWithBackground ?: return null + val controller = + ActivityTransitionAnimator.Controller.fromView(dialogContentWithBackground, cujType) + ?: return null + + // Wrap the controller into one that will instantly dismiss the dialog when the animation is + // done or dismiss it normally (fading it out) if the animation is cancelled. + return object : ActivityTransitionAnimator.Controller by controller { + override val isDialogLaunch = true + + override fun onIntentStarted(willAnimate: Boolean) { + controller.onIntentStarted(willAnimate) + + if (!willAnimate) { + dialog.dismiss() + } + } + + override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { + controller.onTransitionAnimationCancelled() + enableDialogDismiss() + dialog.dismiss() + } + + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + controller.onTransitionAnimationStart(isExpandingFullyAbove) + + // Make sure the dialog is not dismissed during the animation. + disableDialogDismiss() + + // If this dialog was shown from a cascade of other dialogs, make sure those ones + // are dismissed too. + animatedDialog.prepareForStackDismiss() + + // Remove the dim. + dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + controller.onTransitionAnimationEnd(isExpandingFullyAbove) + + // Hide the dialog then dismiss it to instantly dismiss it without playing the + // animation. + dialog.hide() + enableDialogDismiss() + dialog.dismiss() + } + + private fun disableDialogDismiss() { + dialog.setDismissOverride { /* Do nothing */} + } + + private fun enableDialogDismiss() { + // We don't set the override to null given that [AnimatedDialog.OnDialogDismissed] + // will still properly dismiss the dialog but will also make sure to clean up + // everything (like making sure that the touched view that triggered the dialog is + // made VISIBLE again). + dialog.setDismissOverride(animatedDialog::onDialogDismissed) + } + } + } + + /** + * Ensure that all dialogs currently shown won't animate into their source when dismissed. + * + * This is a temporary API meant to be called right before we both dismiss a dialog and start an + * activity, which currently does not look good if we animate the dialog into their source at + * the same time as the activity starts. + * + * TODO(b/193634619): Remove this function and animate dialog into opening activity instead. + */ + fun disableAllCurrentDialogsExitAnimations() { + openedDialogs.forEach { it.exitAnimationDisabled = true } + } + + /** + * Dismiss [dialog]. If it was launched from another dialog using this animator, also dismiss + * the stack of dialogs and simply fade out [dialog]. + */ + fun dismissStack(dialog: Dialog) { + openedDialogs.firstOrNull { it.dialog == dialog }?.prepareForStackDismiss() + dialog.dismiss() + } + + interface Callback { + /** Whether the device is currently in dreaming (screensaver) mode. */ + fun isDreaming(): Boolean + + /** + * Whether the device is currently unlocked, i.e. if it is *not* on the keyguard or if the + * keyguard can be dismissed. + */ + fun isUnlocked(): Boolean + + /** + * Whether we are going to show alternate authentication (like UDFPS) instead of the + * traditional bouncer when unlocking the device. + */ + fun isShowingAlternateAuthOnUnlock(): Boolean + } +} + +/** + * The CUJ interaction associated with opening the dialog. + * + * The optional tag indicates the specific dialog being opened. + */ +data class DialogCuj(@CujType val cujType: Int, val tag: String? = null) + +private class AnimatedDialog( + private val transitionAnimator: TransitionAnimator, + private val callback: DialogTransitionAnimator.Callback, + private val interactionJankMonitor: InteractionJankMonitor, + + /** + * The controller of the source that triggered the dialog and that will animate into/from the + * dialog. + */ + val controller: DialogTransitionAnimator.Controller, + + /** + * A callback that will be called with this [AnimatedDialog] after the dialog was dismissed and + * the exit animation is done. + */ + private val onDialogDismissed: (AnimatedDialog) -> Unit, + + /** The dialog to show and animate. */ + val dialog: Dialog, + + /** Whether we should animate the dialog background when its bounds change. */ + animateBackgroundBoundsChange: Boolean, + + /** Launch animation corresponding to the parent [AnimatedDialog]. */ + private val parentAnimatedDialog: AnimatedDialog? = null, + + /** + * Whether synchronization should be disabled, which can be useful if we are running in a test. + */ + private val forceDisableSynchronization: Boolean, + private val featureFlags: AnimationFeatureFlags, +) { + /** + * The DecorView of this dialog window. + * + * Note that we access this DecorView lazily to avoid accessing it before the dialog is created, + * which can sometimes cause crashes (e.g. with the Cast dialog). + */ + private val decorView by lazy { dialog.window!!.decorView as ViewGroup } + + /** + * The dialog content with its background. When animating a fullscreen dialog, this is just the + * first ViewGroup of the dialog that has a background. When animating a normal (not fullscreen) + * dialog, this is an additional view that serves as a fake window that will have the same size + * as the dialog window initially had and to which we will set the dialog window background. + */ + var dialogContentWithBackground: ViewGroup? = null + + /** The background color of [dialog], taking into consideration its window background color. */ + private var originalDialogBackgroundColor = Color.BLACK + + /** + * Whether we are currently launching/showing the dialog by animating it from its source + * controlled by [controller]. + */ + private var isLaunching = true + + /** Whether we are currently dismissing/hiding the dialog by animating into its source. */ + private var isDismissing = false + + private var dismissRequested = false + var exitAnimationDisabled = false + + private var isSourceDrawnInDialog = false + private var isOriginalDialogViewLaidOut = false + + /** A layout listener to animate the dialog height change. */ + private val backgroundLayoutListener = + if (animateBackgroundBoundsChange) { + AnimatedBoundsLayoutListener() + } else { + null + } + + /* + * A layout listener in case the dialog (window) size changes (for instance because of a + * configuration change) to ensure that the dialog stays full width. + */ + private var decorViewLayoutListener: View.OnLayoutChangeListener? = null + + private var hasInstrumentedJank = false + + fun start() { + val cuj = controller.cuj + if (cuj != null) { + val config = controller.jankConfigurationBuilder() + if (config != null) { + if (cuj.tag != null) { + config.setTag(cuj.tag) + } + + interactionJankMonitor.begin(config) + hasInstrumentedJank = true + } + } + + // Create the dialog so that its onCreate() method is called, which usually sets the dialog + // content. + dialog.create() + + val window = dialog.window!! + val isWindowFullScreen = + window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT + val dialogContentWithBackground = + if (isWindowFullScreen) { + // If the dialog window is already fullscreen, then we look for the first ViewGroup + // that has a background (and is not the DecorView, which always has a background) + // and animate towards that ViewGroup given that this is probably what represents + // the actual dialog view. + var viewGroupWithBackground: ViewGroup? = null + for (i in 0 until decorView.childCount) { + viewGroupWithBackground = + findFirstViewGroupWithBackground(decorView.getChildAt(i)) + if (viewGroupWithBackground != null) { + break + } + } + + // Animate that view with the background. Throw if we didn't find one, because + // otherwise it's not clear what we should animate. + if (viewGroupWithBackground == null) { + error("Unable to find ViewGroup with background") + } + + if (viewGroupWithBackground !is LaunchableView) { + error("The animated ViewGroup with background must implement LaunchableView") + } + + viewGroupWithBackground + } else { + val (dialogContentWithBackground, decorViewLayoutListener) = + dialog.maybeForceFullscreen()!! + this.decorViewLayoutListener = decorViewLayoutListener + dialogContentWithBackground + } + + this.dialogContentWithBackground = dialogContentWithBackground + dialogContentWithBackground.setTag(R.id.tag_dialog_background, true) + + val background = dialogContentWithBackground.background + originalDialogBackgroundColor = + GhostedViewTransitionAnimatorController.findGradientDrawable(background) + ?.color + ?.defaultColor + ?: Color.BLACK + + // Make the background view invisible until we start the animation. We use the transition + // visibility like GhostView does so that we don't mess up with the accessibility tree (see + // b/204944038#comment17). Given that this background implements LaunchableView, we call + // setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is + // restored at the end of the animation. + dialogContentWithBackground.setShouldBlockVisibilityChanges(true) + dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE) + + // Make sure the dialog is visible instantly and does not do any window animation. + val attributes = window.attributes + attributes.windowAnimations = R.style.Animation_LaunchAnimation + + // Ensure that the animation is not clipped by the display cut-out when animating this + // dialog into an app. + attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + + // Ensure that the animation is not clipped by the navigation/task bars when animating this + // dialog into an app. + val wasFittingNavigationBars = + attributes.fitInsetsTypes and WindowInsets.Type.navigationBars() != 0 + attributes.fitInsetsTypes = + attributes.fitInsetsTypes and WindowInsets.Type.navigationBars().inv() + + window.attributes = window.attributes + + // We apply the insets ourselves to make sure that the paddings are set on the correct + // View. + window.setDecorFitsSystemWindows(false) + val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup) + viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets -> + val type = + if (wasFittingNavigationBars) { + WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars() + } else { + WindowInsets.Type.displayCutout() + } + + val insets = windowInsets.getInsets(type) + view.setPadding(insets.left, insets.top, insets.right, insets.bottom) + WindowInsets.CONSUMED + } + + // Start the animation once the background view is properly laid out. + dialogContentWithBackground.addOnLayoutChangeListener( + object : View.OnLayoutChangeListener { + override fun onLayoutChange( + v: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + dialogContentWithBackground.removeOnLayoutChangeListener(this) + + isOriginalDialogViewLaidOut = true + maybeStartLaunchAnimation() + } + } + ) + + // Disable the dim. We will enable it once we start the animation. + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + + // Override the dialog dismiss() so that we can animate the exit before actually dismissing + // the dialog. + dialog.setDismissOverride(this::onDialogDismissed) + + if (featureFlags.isPredictiveBackQsDialogAnim) { + dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground) + } + + // Show the dialog. + dialog.show() + moveSourceDrawingToDialog() + } + + private fun moveSourceDrawingToDialog() { + if (decorView.viewRootImpl == null) { + // Make sure that we have access to the dialog view root to move the drawing to the + // dialog overlay. + decorView.post(::moveSourceDrawingToDialog) + return + } + + // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a + // one-off synchronization to make sure that this is done in sync between the two different + // windows. + controller.startDrawingInOverlayOf(decorView) + synchronizeNextDraw( + then = { + isSourceDrawnInDialog = true + maybeStartLaunchAnimation() + } + ) + } + + /** + * Synchronize the next draw of the source and dialog view roots so that they are performed at + * the same time, in the same transaction. This is necessary to make sure that the source is + * drawn in the overlay at the same time as it is removed from its original position (or + * inversely, removed from the overlay when the source is moved back to its original position). + */ + private fun synchronizeNextDraw(then: () -> Unit) { + val controllerRootView = controller.viewRoot?.view + if (forceDisableSynchronization || controllerRootView == null) { + // Don't synchronize when inside an automated test or if the controller root view is + // detached. + then() + return + } + + ViewRootSync.synchronizeNextDraw(controllerRootView, decorView, then) + decorView.invalidate() + controllerRootView.invalidate() + } + + private fun findFirstViewGroupWithBackground(view: View): ViewGroup? { + if (view !is ViewGroup) { + return null + } + + if (view.background != null) { + return view + } + + for (i in 0 until view.childCount) { + val match = findFirstViewGroupWithBackground(view.getChildAt(i)) + if (match != null) { + return match + } + } + + return null + } + + private fun maybeStartLaunchAnimation() { + if (!isSourceDrawnInDialog || !isOriginalDialogViewLaidOut) { + return + } + + // Show the background dim. + dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + + startAnimation( + isLaunching = true, + onLaunchAnimationEnd = { + isLaunching = false + + // dismiss was called during the animation, dismiss again now to actually dismiss. + if (dismissRequested) { + dialog.dismiss() + } + + // If necessary, we animate the dialog background when its bounds change. We do it + // at the end of the launch animation, because the lauch animation already correctly + // handles bounds changes. + if (backgroundLayoutListener != null) { + dialogContentWithBackground!!.addOnLayoutChangeListener( + backgroundLayoutListener + ) + } + + if (hasInstrumentedJank) { + interactionJankMonitor.end(controller.cuj!!.cujType) + } + } + ) + } + + fun onDialogDismissed() { + if (Looper.myLooper() != Looper.getMainLooper()) { + dialog.context.mainExecutor.execute { onDialogDismissed() } + return + } + + // TODO(b/193634619): Support interrupting the launch animation in the middle. + if (isLaunching) { + dismissRequested = true + return + } + + if (isDismissing) { + return + } + + isDismissing = true + hideDialogIntoView { animationRan: Boolean -> + if (animationRan) { + // Instantly dismiss the dialog if we ran the animation into view. If it was + // skipped, dismiss() will run the window animation (which fades out the dialog). + dialog.hide() + } + + dialog.setDismissOverride(null) + dialog.dismiss() + } + } + + /** + * Hide the dialog into the source and call [onAnimationFinished] when the animation is done + * (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually + * dismiss the dialog. + */ + private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) { + // Remove the layout change listener we have added to the DecorView earlier. + if (decorViewLayoutListener != null) { + decorView.removeOnLayoutChangeListener(decorViewLayoutListener) + } + + if (!shouldAnimateDialogIntoSource()) { + Log.i(TAG, "Skipping animation of dialog into the source") + controller.onExitAnimationCancelled() + onAnimationFinished(false /* instantDismiss */) + onDialogDismissed(this@AnimatedDialog) + return + } + + startAnimation( + isLaunching = false, + onLaunchAnimationStart = { + // Remove the dim background as soon as we start the animation. + dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + }, + onLaunchAnimationEnd = { + val dialogContentWithBackground = this.dialogContentWithBackground!! + dialogContentWithBackground.visibility = View.INVISIBLE + + if (backgroundLayoutListener != null) { + dialogContentWithBackground.removeOnLayoutChangeListener( + backgroundLayoutListener + ) + } + + controller.stopDrawingInOverlay() + synchronizeNextDraw { + onAnimationFinished(true /* instantDismiss */) + onDialogDismissed(this@AnimatedDialog) + } + } + ) + } + + private fun startAnimation( + isLaunching: Boolean, + onLaunchAnimationStart: () -> Unit = {}, + onLaunchAnimationEnd: () -> Unit = {} + ) { + // Create 2 controllers to animate both the dialog and the source. + val startController = + if (isLaunching) { + controller.createTransitionController() + } else { + GhostedViewTransitionAnimatorController(dialogContentWithBackground!!) + } + val endController = + if (isLaunching) { + GhostedViewTransitionAnimatorController(dialogContentWithBackground!!) + } else { + controller.createExitController() + } + startController.transitionContainer = decorView + endController.transitionContainer = decorView + + val endState = endController.createAnimatorState() + val controller = + object : TransitionAnimator.Controller { + override var transitionContainer: ViewGroup + get() = startController.transitionContainer + set(value) { + startController.transitionContainer = value + endController.transitionContainer = value + } + + // We tell TransitionController that this is always a launch, and handle the launch + // vs return logic internally. + // TODO(b/323863002): maybe move the launch vs return logic out of this class and + // delegate it to TransitionController? + override val isLaunching: Boolean = true + + override fun createAnimatorState(): TransitionAnimator.State { + return startController.createAnimatorState() + } + + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + // During launch, onLaunchAnimationStart will be used to remove the temporary + // touch surface ghost so it is important to call this before calling + // onLaunchAnimationStart on the controller (which will create its own ghost). + onLaunchAnimationStart() + + startController.onTransitionAnimationStart(isExpandingFullyAbove) + endController.onTransitionAnimationStart(isExpandingFullyAbove) + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + // onLaunchAnimationEnd is called by an Animator at the end of the animation, + // on a Choreographer animation tick. The following calls will move the animated + // content from the dialog overlay back to its original position, and this + // change must be reflected in the next frame given that we then sync the next + // frame of both the content and dialog ViewRoots. However, in case that content + // is rendered by Compose, whose compositions are also scheduled on a + // Choreographer frame, any state change made *right now* won't be reflected in + // the next frame given that a Choreographer frame can't schedule another and + // have it happen in the same frame. So we post the forwarded calls to + // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring + // that the move of the content back to its original window will be reflected in + // the next frame right after [onLaunchAnimationEnd] is called. + // + // TODO(b/330672236): Move this to TransitionAnimator. + dialog.context.mainExecutor.execute { + startController.onTransitionAnimationEnd(isExpandingFullyAbove) + endController.onTransitionAnimationEnd(isExpandingFullyAbove) + + onLaunchAnimationEnd() + } + } + + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, + progress: Float, + linearProgress: Float + ) { + startController.onTransitionAnimationProgress(state, progress, linearProgress) + + // The end view is visible only iff the starting view is not visible. + state.visible = !state.visible + endController.onTransitionAnimationProgress(state, progress, linearProgress) + + // If the dialog content is complex, its dimension might change during the + // launch animation. The animation end position might also change during the + // exit animation, for instance when locking the phone when the dialog is open. + // Therefore we update the end state to the new position/size. Usually the + // dialog dimension or position will change in the early frames, so changing the + // end state shouldn't really be noticeable. + if (endController is GhostedViewTransitionAnimatorController) { + endController.fillGhostedViewState(endState) + } + } + } + + transitionAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) + } + + private fun shouldAnimateDialogIntoSource(): Boolean { + // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit + // animation. + if (exitAnimationDisabled || !dialog.isShowing) { + return false + } + + // If we are dreaming, the dialog was probably closed because of that so we don't animate + // into the source. + if (callback.isDreaming()) { + return false + } + + return controller.shouldAnimateExit() + } + + /** A layout listener to animate the change of bounds of the dialog background. */ + class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener { + companion object { + private const val ANIMATION_DURATION = 500L + } + + private var lastBounds: Rect? = null + private var currentAnimator: ValueAnimator? = null + + override fun onLayoutChange( + view: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + // Don't animate if bounds didn't actually change. + if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) { + // Make sure that we that the last bounds set by the animator were not overridden. + lastBounds?.let { bounds -> + view.left = bounds.left + view.top = bounds.top + view.right = bounds.right + view.bottom = bounds.bottom + } + return + } + + if (lastBounds == null) { + lastBounds = Rect(oldLeft, oldTop, oldRight, oldBottom) + } + + val bounds = lastBounds!! + val startLeft = bounds.left + val startTop = bounds.top + val startRight = bounds.right + val startBottom = bounds.bottom + + currentAnimator?.cancel() + currentAnimator = null + + val animator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = ANIMATION_DURATION + interpolator = Interpolators.STANDARD + + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + } + } + ) + + addUpdateListener { animatedValue -> + val progress = animatedValue.animatedFraction + + // Compute new bounds. + bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt() + bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt() + bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt() + bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt() + + // Set the new bounds. + view.left = bounds.left + view.top = bounds.top + view.right = bounds.right + view.bottom = bounds.bottom + } + } + + currentAnimator = animator + animator.start() + } + } + + fun prepareForStackDismiss() { + if (parentAnimatedDialog == null) { + return + } + parentAnimatedDialog.exitAnimationDisabled = true + parentAnimatedDialog.dialog.hide() + parentAnimatedDialog.prepareForStackDismiss() + parentAnimatedDialog.dialog.dismiss() + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/Expandable.kt b/systemUIAnim/src/com/android/systemui/animation/Expandable.kt new file mode 100644 index 0000000000..3ba9a29748 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/Expandable.kt @@ -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. + */ + +package com.android.systemui.animation + +import android.content.ComponentName +import android.view.View + +/** A piece of UI that can be expanded into a Dialog or an Activity. */ +interface Expandable { + /** + * Create an [ActivityTransitionAnimator.Controller] that can be used to expand this + * [Expandable] into an Activity, or return `null` if this [Expandable] should not be animated + * (e.g. if it is currently not attached or visible). + * + * @param launchCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor] + * associated to the launch that will use this controller. + * @param cookie The unique cookie associated with the launch that will use this controller. + * This is required iff a return animation should be included. + * @param component The name of the activity that will be launched by this controller. This is + * required for long-lived registrations only. + * @param returnCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor] + * associated to the return animation that will use this controller. + */ + fun activityTransitionController( + launchCujType: Int? = null, + cookie: ActivityTransitionAnimator.TransitionCookie? = null, + component: ComponentName? = null, + returnCujType: Int? = null + ): ActivityTransitionAnimator.Controller? + + /** + * See [activityTransitionController] above. + * + * Interfaces don't support [JvmOverloads], so this is a useful overload for Java usages that + * don't use the return-related parameters. + */ + fun activityTransitionController( + launchCujType: Int? = null + ): ActivityTransitionAnimator.Controller? { + return activityTransitionController( + launchCujType, + cookie = null, + component = null, + returnCujType = null + ) + } + + /** + * Create a [DialogTransitionAnimator.Controller] that can be used to expand this [Expandable] + * into a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is + * currently not attached or visible). + */ + fun dialogTransitionController(cuj: DialogCuj? = null): DialogTransitionAnimator.Controller? + + companion object { + /** + * Create an [Expandable] that will animate [view] when expanded. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be properly + * animated. + */ + @JvmStatic + fun fromView(view: View): Expandable { + return object : Expandable { + override fun activityTransitionController( + launchCujType: Int?, + cookie: ActivityTransitionAnimator.TransitionCookie?, + component: ComponentName?, + returnCujType: Int? + ): ActivityTransitionAnimator.Controller? { + return ActivityTransitionAnimator.Controller.fromView( + view, + launchCujType, + cookie, + component, + returnCujType + ) + } + + override fun dialogTransitionController( + cuj: DialogCuj? + ): DialogTransitionAnimator.Controller? { + return DialogTransitionAnimator.Controller.fromView(view, cuj) + } + } + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/FontInterpolator.kt b/systemUIAnim/src/com/android/systemui/animation/FontInterpolator.kt new file mode 100644 index 0000000000..addabcc028 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/FontInterpolator.kt @@ -0,0 +1,238 @@ +/* + * 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.systemui.animation + +import android.graphics.fonts.Font +import android.graphics.fonts.FontVariationAxis +import android.util.Log +import android.util.LruCache +import android.util.MathUtils +import androidx.annotation.VisibleForTesting +import java.lang.Float.max +import java.lang.Float.min + +private const val TAG_WGHT = "wght" +private const val TAG_ITAL = "ital" + +private const val FONT_WEIGHT_DEFAULT_VALUE = 400f +private const val FONT_ITALIC_MAX = 1f +private const val FONT_ITALIC_MIN = 0f +private const val FONT_ITALIC_ANIMATION_STEP = 0.1f +private const val FONT_ITALIC_DEFAULT_VALUE = 0f + +// Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in +// frame draw time on a Pixel 6. +@VisibleForTesting const val DEFAULT_FONT_CACHE_MAX_ENTRIES = 10 + +/** Provide interpolation of two fonts by adjusting font variation settings. */ +class FontInterpolator( + numberOfAnimationSteps: Int? = null, +) { + /** + * Cache key for the interpolated font. + * + * This class is mutable for recycling. + */ + private data class InterpKey(var l: Font?, var r: Font?, var progress: Float) { + fun set(l: Font, r: Font, progress: Float) { + this.l = l + this.r = r + this.progress = progress + } + } + + /** + * Cache key for the font that has variable font. + * + * This class is mutable for recycling. + */ + private data class VarFontKey( + var sourceId: Int, + var index: Int, + val sortedAxes: MutableList + ) { + constructor( + font: Font, + axes: List + ) : this( + font.sourceIdentifier, + font.ttcIndex, + axes.toMutableList().apply { sortBy { it.tag } } + ) + + fun set(font: Font, axes: List) { + sourceId = font.sourceIdentifier + index = font.ttcIndex + sortedAxes.clear() + sortedAxes.addAll(axes) + sortedAxes.sortBy { it.tag } + } + } + + // Font interpolator has two level caches: one for input and one for font with different + // variation settings. No synchronization is needed since FontInterpolator is not designed to be + // thread-safe and can be used only on UI thread. + val cacheMaxEntries = numberOfAnimationSteps?.let { it * 2 } ?: DEFAULT_FONT_CACHE_MAX_ENTRIES + private val interpCache = LruCache(cacheMaxEntries) + private val verFontCache = LruCache(cacheMaxEntries) + + // Mutable keys for recycling. + private val tmpInterpKey = InterpKey(null, null, 0f) + private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf()) + + /** Linear interpolate the font variation settings. */ + fun lerp(start: Font, end: Font, progress: Float): Font { + if (progress == 0f) { + return start + } else if (progress == 1f) { + return end + } + + val startAxes = start.axes ?: EMPTY_AXES + val endAxes = end.axes ?: EMPTY_AXES + + if (startAxes.isEmpty() && endAxes.isEmpty()) { + return start + } + + // Check we already know the result. This is commonly happens since we draws the different + // text chunks with the same font. + tmpInterpKey.set(start, end, progress) + val cachedFont = interpCache[tmpInterpKey] + if (cachedFont != null) { + if (DEBUG) { + Log.d(LOG_TAG, "[$progress] Interp. cache hit for $tmpInterpKey") + } + return cachedFont + } + + // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually + // this doesn't take much time since the variation axes is usually up to 5. If we need to + // support more number of axes, we may want to preprocess the font and store the sorted axes + // and also pre-fill the missing axes value with default value from 'fvar' table. + val newAxes = + lerp(startAxes, endAxes) { tag, startValue, endValue -> + when (tag) { + TAG_WGHT -> + MathUtils.lerp( + startValue ?: FONT_WEIGHT_DEFAULT_VALUE, + endValue ?: FONT_WEIGHT_DEFAULT_VALUE, + progress + ) + TAG_ITAL -> + adjustItalic( + MathUtils.lerp( + startValue ?: FONT_ITALIC_DEFAULT_VALUE, + endValue ?: FONT_ITALIC_DEFAULT_VALUE, + progress + ) + ) + else -> { + require(startValue != null && endValue != null) { + "Unable to interpolate due to unknown default axes value : $tag" + } + MathUtils.lerp(startValue, endValue, progress) + } + } + } + + // Check if we already make font for this axes. This is typically happens if the animation + // happens backward. + tmpVarFontKey.set(start, newAxes) + val axesCachedFont = verFontCache[tmpVarFontKey] + if (axesCachedFont != null) { + interpCache.put(InterpKey(start, end, progress), axesCachedFont) + if (DEBUG) { + Log.d(LOG_TAG, "[$progress] Axis cache hit for $tmpVarFontKey") + } + return axesCachedFont + } + + // This is the first time to make the font for the axes. Build and store it to the cache. + // Font.Builder#build won't throw IOException since creating fonts from existing fonts will + // not do any IO work. + val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build() + interpCache.put(InterpKey(start, end, progress), newFont) + verFontCache.put(VarFontKey(start, newAxes), newFont) + + // Cache misses are likely to create memory leaks, so this is logged at error level. + Log.e(LOG_TAG, "[$progress] Cache MISS for $tmpInterpKey / $tmpVarFontKey") + return newFont + } + + private fun lerp( + start: Array, + end: Array, + filter: (tag: String, left: Float?, right: Float?) -> Float + ): List { + // Safe to modify result of Font#getAxes since it returns cloned object. + start.sortBy { axis -> axis.tag } + end.sortBy { axis -> axis.tag } + + val result = mutableListOf() + var i = 0 + var j = 0 + while (i < start.size || j < end.size) { + val tagA = if (i < start.size) start[i].tag else null + val tagB = if (j < end.size) end[j].tag else null + + val comp = + when { + tagA == null -> 1 + tagB == null -> -1 + else -> tagA.compareTo(tagB) + } + + val axis = + when { + comp == 0 -> { + val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue) + FontVariationAxis(tagA, v) + } + comp < 0 -> { + val v = filter(tagA!!, start[i++].styleValue, null) + FontVariationAxis(tagA, v) + } + else -> { // comp > 0 + val v = filter(tagB!!, null, end[j++].styleValue) + FontVariationAxis(tagB, v) + } + } + + result.add(axis) + } + return result + } + + // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps + // Cache hit ratio in the Skia glyph cache. + private fun adjustItalic(value: Float) = + coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP) + + private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) = + (v.coerceIn(min, max) / step).toInt() * step + + companion object { + private const val LOG_TAG = "FontInterpolator" + private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) + private val EMPTY_AXES = arrayOf() + + // Returns true if given two font instance can be interpolated. + fun canInterpolate(start: Font, end: Font) = + start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/FontVariationUtils.kt b/systemUIAnim/src/com/android/systemui/animation/FontVariationUtils.kt new file mode 100644 index 0000000000..78ae4af258 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/FontVariationUtils.kt @@ -0,0 +1,59 @@ +package com.android.systemui.animation + +private const val TAG_WGHT = "wght" +private const val TAG_WDTH = "wdth" +private const val TAG_OPSZ = "opsz" +private const val TAG_ROND = "ROND" + +class FontVariationUtils { + private var mWeight = -1 + private var mWidth = -1 + private var mOpticalSize = -1 + private var mRoundness = -1 + private var isUpdated = false + + /* + * generate fontVariationSettings string, used for key in typefaceCache in TextAnimator + * the order of axes should align to the order of parameters + * if every axis remains unchanged, return "" + */ + fun updateFontVariation( + weight: Int = -1, + width: Int = -1, + opticalSize: Int = -1, + roundness: Int = -1 + ): String { + isUpdated = false + if (weight >= 0 && mWeight != weight) { + isUpdated = true + mWeight = weight + } + if (width >= 0 && mWidth != width) { + isUpdated = true + mWidth = width + } + if (opticalSize >= 0 && mOpticalSize != opticalSize) { + isUpdated = true + mOpticalSize = opticalSize + } + + if (roundness >= 0 && mRoundness != roundness) { + isUpdated = true + mRoundness = roundness + } + var resultString = "" + if (mWeight >= 0) { + resultString += "'$TAG_WGHT' $mWeight" + } + if (mWidth >= 0) { + resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_WDTH' $mWidth" + } + if (mOpticalSize >= 0) { + resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_OPSZ' $mOpticalSize" + } + if (mRoundness >= 0) { + resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_ROND' $mRoundness" + } + return if (isUpdated) resultString else "" + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/systemUIAnim/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt new file mode 100644 index 0000000000..e626c04675 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -0,0 +1,532 @@ +/* + * 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.systemui.animation + +import android.content.ComponentName +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Insets +import android.graphics.Matrix +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.StateListDrawable +import android.util.Log +import android.view.GhostView +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroupOverlay +import android.widget.FrameLayout +import com.android.internal.jank.Cuj.CujType +import com.android.internal.jank.InteractionJankMonitor +import java.util.LinkedList +import kotlin.math.min +import kotlin.math.roundToInt + +private const val TAG = "GhostedViewTransitionAnimatorController" + +/** + * A base implementation of [ActivityTransitionAnimator.Controller] which creates a + * [ghost][GhostView] of [ghostedView] as well as an expandable background view, which are drawn and + * animated instead of the ghosted view. + * + * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during + * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown + * during this controller instantiation. + * + * Note: Avoid instantiating this directly and call [ActivityTransitionAnimator.Controller.fromView] + * whenever possible instead. + */ +open class GhostedViewTransitionAnimatorController +@JvmOverloads +constructor( + /** The view that will be ghosted and from which the background will be extracted. */ + private val ghostedView: View, + + /** The [CujType] associated to this launch animation. */ + private val launchCujType: Int? = null, + override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null, + override val component: ComponentName? = null, + + /** The [CujType] associated to this return animation. */ + private val returnCujType: Int? = null, + private var interactionJankMonitor: InteractionJankMonitor = + InteractionJankMonitor.getInstance(), +) : ActivityTransitionAnimator.Controller { + override val isLaunching: Boolean = true + + /** The container to which we will add the ghost view and expanding background. */ + override var transitionContainer = ghostedView.rootView as ViewGroup + private val transitionContainerOverlay: ViewGroupOverlay + get() = transitionContainer.overlay + + private val transitionContainerLocation = IntArray(2) + + /** The ghost view that is drawn and animated instead of the ghosted view. */ + private var ghostView: GhostView? = null + private val initialGhostViewMatrixValues = FloatArray(9) { 0f } + private val ghostViewMatrix = Matrix() + + /** + * The expanding background view that will be added to [transitionContainer] (below [ghostView]) + * and animate. + */ + private var backgroundView: FrameLayout? = null + + /** + * The drawable wrapping the [ghostedView] background and used as background for + * [backgroundView]. + */ + private var backgroundDrawable: WrappedDrawable? = null + private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE } + private var startBackgroundAlpha: Int = 0xFF + + private val ghostedViewLocation = IntArray(2) + private val ghostedViewState = TransitionAnimator.State() + + /** + * The background of the [ghostedView]. This background will be used to draw the background of + * the background view that is expanding up to the final animation position. + * + * Note that during the animation, the alpha value value of this background will be set to 0, + * then set back to its initial value at the end of the animation. + */ + private val background: Drawable? + + /** CUJ identifier accounting for whether this controller is for a launch or a return. */ + private val cujType: Int? + get() = + if (isLaunching) { + launchCujType + } else { + returnCujType + } + + init { + // Make sure the View we launch from implements LaunchableView to avoid visibility issues. + if (ghostedView !is LaunchableView) { + throw IllegalArgumentException( + "A GhostedViewLaunchAnimatorController was created from a View that does not " + + "implement LaunchableView. This can lead to subtle bugs where the visibility " + + "of the View we are launching from is not what we expected." + ) + } + + /** Find the first view with a background in [view] and its children. */ + fun findBackground(view: View): Drawable? { + if (view.background != null) { + return view.background + } + + // Perform a BFS to find the largest View with background. + val views = LinkedList().apply { add(view) } + + while (views.isNotEmpty()) { + val v = views.removeAt(0) + if (v.background != null) { + return v.background + } + + if (v is ViewGroup) { + for (i in 0 until v.childCount) { + views.add(v.getChildAt(i)) + } + } + } + + return null + } + + background = findBackground(ghostedView) + } + + /** + * Set the corner radius of [background]. The background is the one that was returned by + * [getBackground]. + */ + protected open fun setBackgroundCornerRadius( + background: Drawable, + topCornerRadius: Float, + bottomCornerRadius: Float + ) { + // By default, we rely on WrappedDrawable to set/restore the background radii before/after + // each draw. + backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius) + } + + /** Return the current top corner radius of the background. */ + protected open fun getCurrentTopCornerRadius(): Float { + val drawable = background ?: return 0f + val gradient = findGradientDrawable(drawable) ?: return 0f + + // TODO(b/184121838): Support more than symmetric top & bottom radius. + val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius + return radius * ghostedView.scaleX + } + + /** Return the current bottom corner radius of the background. */ + protected open fun getCurrentBottomCornerRadius(): Float { + val drawable = background ?: return 0f + val gradient = findGradientDrawable(drawable) ?: return 0f + + // TODO(b/184121838): Support more than symmetric top & bottom radius. + val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius + return radius * ghostedView.scaleX + } + + override fun createAnimatorState(): TransitionAnimator.State { + val state = + TransitionAnimator.State( + topCornerRadius = getCurrentTopCornerRadius(), + bottomCornerRadius = getCurrentBottomCornerRadius() + ) + fillGhostedViewState(state) + return state + } + + fun fillGhostedViewState(state: TransitionAnimator.State) { + // For the animation we are interested in the area that has a non transparent background, + // so we have to take the optical insets into account. + ghostedView.getLocationOnScreen(ghostedViewLocation) + val insets = backgroundInsets + val boundCorrections: Rect = + if (ghostedView is LaunchableView) { + ghostedView.getPaddingForLaunchAnimation() + } else { + Rect() + } + state.top = ghostedViewLocation[1] + insets.top + boundCorrections.top + state.bottom = + ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() - + insets.bottom + boundCorrections.bottom + state.left = ghostedViewLocation[0] + insets.left + boundCorrections.left + state.right = + ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() - + insets.right + boundCorrections.right + } + + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + if (ghostedView.parent !is ViewGroup) { + // This should usually not happen, but let's make sure we don't crash if the view was + // detached right before we started the animation. + Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup") + return + } + + backgroundView = + FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) } + + // We wrap the ghosted view background and use it to draw the expandable background. Its + // alpha will be set to 0 as soon as we start drawing the expanding background. + startBackgroundAlpha = background?.alpha ?: 0xFF + backgroundDrawable = WrappedDrawable(background) + backgroundView?.background = backgroundDrawable + + // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be + // called before `GhostView.addGhost()` is called because the latter will change the + // *transition* visibility, which won't be blocked and will affect the normal View + // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration. + (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + + // Create a ghost of the view that will be moving and fading out. This allows to fade out + // the content before fading out the background. + ghostView = GhostView.addGhost(ghostedView, transitionContainer) + + // [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and + // adds it first to a [FrameLayout] container. It then adds _that_ container to an + // [OverlayViewGroup]. We need to turn off clipping for that container view. Currently, + // however, the only way to get a reference to that overlay is by going through our + // [ghostView]. The [OverlayViewGroup] will always be its grandparent view. + // TODO(b/306652954) reference the overlay view group directly if we can + (ghostView?.parent?.parent as? ViewGroup)?.let { + it.clipChildren = false + it.clipToPadding = false + } + + val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX + matrix.getValues(initialGhostViewMatrixValues) + + cujType?.let { interactionJankMonitor.begin(ghostedView, it) } + } + + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, + progress: Float, + linearProgress: Float + ) { + val ghostView = this.ghostView ?: return + val backgroundView = this.backgroundView!! + + if (!state.visible || !ghostedView.isAttachedToWindow) { + if (ghostView.visibility == View.VISIBLE) { + // Making the ghost view invisible will make the ghosted view visible, so order is + // important here. + ghostView.visibility = View.INVISIBLE + + // Make the ghosted view invisible again. We use the transition visibility like + // GhostView does so that we don't mess up with the accessibility tree (see + // b/204944038#comment17). + ghostedView.setTransitionVisibility(View.INVISIBLE) + backgroundView.visibility = View.INVISIBLE + } + return + } + + // The ghost and backgrounds views were made invisible earlier. That can for instance happen + // when animating a dialog into a view. + if (ghostView.visibility == View.INVISIBLE) { + ghostView.visibility = View.VISIBLE + backgroundView.visibility = View.VISIBLE + } + + fillGhostedViewState(ghostedViewState) + val leftChange = state.left - ghostedViewState.left + val rightChange = state.right - ghostedViewState.right + val topChange = state.top - ghostedViewState.top + val bottomChange = state.bottom - ghostedViewState.bottom + + val widthRatio = state.width.toFloat() / ghostedViewState.width + val heightRatio = state.height.toFloat() / ghostedViewState.height + val scale = min(widthRatio, heightRatio) + + if (ghostedView.parent is ViewGroup) { + // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted + // view is still attached to a ViewGroup, otherwise calculateMatrix will throw. + GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix) + } + + transitionContainer.getLocationOnScreen(transitionContainerLocation) + ghostViewMatrix.postScale( + scale, + scale, + ghostedViewState.centerX - transitionContainerLocation[0], + ghostedViewState.centerY - transitionContainerLocation[1] + ) + ghostViewMatrix.postTranslate( + (leftChange + rightChange) / 2f, + (topChange + bottomChange) / 2f + ) + ghostView.animationMatrix = ghostViewMatrix + + // We need to take into account the background insets for the background position. + val insets = backgroundInsets + val topWithInsets = state.top - insets.top + val leftWithInsets = state.left - insets.left + val rightWithInsets = state.right + insets.right + val bottomWithInsets = state.bottom + insets.bottom + + backgroundView.top = topWithInsets - transitionContainerLocation[1] + backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1] + backgroundView.left = leftWithInsets - transitionContainerLocation[0] + backgroundView.right = rightWithInsets - transitionContainerLocation[0] + + val backgroundDrawable = backgroundDrawable!! + backgroundDrawable.wrapped?.let { + setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius) + } + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + if (ghostView == null) { + // We didn't actually run the animation. + return + } + + cujType?.let { interactionJankMonitor.end(it) } + + backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha + + GhostView.removeGhost(ghostedView) + backgroundView?.let { transitionContainerOverlay.remove(it) } + + if (ghostedView is LaunchableView) { + // Restore the ghosted view visibility. + ghostedView.setShouldBlockVisibilityChanges(false) + ghostedView.onActivityLaunchAnimationEnd() + } else { + // Make the ghosted view visible. We ensure that the view is considered VISIBLE by + // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 + // for more info). + ghostedView.visibility = View.INVISIBLE + ghostedView.visibility = View.VISIBLE + ghostedView.invalidate() + } + } + + companion object { + private const val CORNER_RADIUS_TOP_INDEX = 0 + private const val CORNER_RADIUS_BOTTOM_INDEX = 4 + + /** + * Return the first [GradientDrawable] found in [drawable], or null if none is found. If + * [drawable] is a [LayerDrawable], this will return the first layer that has a + * [GradientDrawable]. + */ + fun findGradientDrawable(drawable: Drawable): GradientDrawable? { + if (drawable is GradientDrawable) { + return drawable + } + + if (drawable is InsetDrawable) { + return drawable.drawable?.let { findGradientDrawable(it) } + } + + if (drawable is LayerDrawable) { + for (i in 0 until drawable.numberOfLayers) { + val maybeGradient = findGradientDrawable(drawable.getDrawable(i)) + if (maybeGradient != null) { + return maybeGradient + } + } + } + + if (drawable is StateListDrawable) { + return findGradientDrawable(drawable.current) + } + + return null + } + } + + private class WrappedDrawable(val wrapped: Drawable?) : Drawable() { + private var currentAlpha = 0xFF + private var previousBounds = Rect() + + private var cornerRadii = FloatArray(8) { -1f } + private var previousCornerRadii = FloatArray(8) + + override fun draw(canvas: Canvas) { + val wrapped = this.wrapped ?: return + + wrapped.copyBounds(previousBounds) + + wrapped.alpha = currentAlpha + wrapped.bounds = bounds + applyBackgroundRadii() + + wrapped.draw(canvas) + + // The background view (and therefore this drawable) is drawn before the ghost view, so + // the ghosted view background alpha should always be 0 when it is drawn above the + // background. + wrapped.alpha = 0 + wrapped.bounds = previousBounds + restoreBackgroundRadii() + } + + override fun setAlpha(alpha: Int) { + if (alpha != currentAlpha) { + currentAlpha = alpha + invalidateSelf() + } + } + + override fun getAlpha() = currentAlpha + + override fun getOpacity(): Int { + val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT + + val previousAlpha = wrapped.alpha + wrapped.alpha = currentAlpha + val opacity = wrapped.opacity + wrapped.alpha = previousAlpha + return opacity + } + + override fun setColorFilter(filter: ColorFilter?) { + wrapped?.colorFilter = filter + } + + fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) { + updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius) + invalidateSelf() + } + + private fun updateRadii( + radii: FloatArray, + topCornerRadius: Float, + bottomCornerRadius: Float + ) { + radii[0] = topCornerRadius + radii[1] = topCornerRadius + radii[2] = topCornerRadius + radii[3] = topCornerRadius + + radii[4] = bottomCornerRadius + radii[5] = bottomCornerRadius + radii[6] = bottomCornerRadius + radii[7] = bottomCornerRadius + } + + private fun applyBackgroundRadii() { + if (cornerRadii[0] < 0 || wrapped == null) { + return + } + + savePreviousBackgroundRadii(wrapped) + applyBackgroundRadii(wrapped, cornerRadii) + } + + private fun savePreviousBackgroundRadii(background: Drawable) { + // TODO(b/184121838): This method assumes that all GradientDrawable in background will + // have the same radius. Should we save/restore the radii for each layer instead? + val gradient = findGradientDrawable(background) ?: return + + // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we + // try to avoid that? + val radii = gradient.cornerRadii + if (radii != null) { + radii.copyInto(previousCornerRadii) + } else { + // Copy the cornerRadius into previousCornerRadii. + val radius = gradient.cornerRadius + updateRadii(previousCornerRadii, radius, radius) + } + } + + private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) { + if (drawable is GradientDrawable) { + drawable.cornerRadii = radii + return + } + + if (drawable is InsetDrawable) { + drawable.drawable?.let { applyBackgroundRadii(it, radii) } + return + } + + if (drawable !is LayerDrawable) { + return + } + + for (i in 0 until drawable.numberOfLayers) { + applyBackgroundRadii(drawable.getDrawable(i), radii) + } + } + + private fun restoreBackgroundRadii() { + if (cornerRadii[0] < 0 || wrapped == null) { + return + } + + applyBackgroundRadii(wrapped, previousCornerRadii) + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/LaunchableView.kt b/systemUIAnim/src/com/android/systemui/animation/LaunchableView.kt new file mode 100644 index 0000000000..330ab0f7fc --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/LaunchableView.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.graphics.Rect +import android.view.View + +/** A view that can expand/launch into an app or a dialog. */ +interface LaunchableView { + /** + * Set whether this view should block/postpone all calls to [View.setVisibility]. This ensures + * that this view: + * - remains invisible during the launch animation given that it is ghosted and already drawn + * somewhere else. + * - remains invisible as long as a dialog expanded from it is shown. + * - restores its expected visibility once the dialog expanded from it is dismissed. + * + * When `setShouldBlockVisibilityChanges(false)` is called, then visibility of the View should + * be restored to its expected value, i.e. it should have the visibility of the last call to + * `View.setVisibility()` that was made after `setShouldBlockVisibilityChanges(true)`, if any, + * or the original view visibility otherwise. + * + * Note that calls to [View.setTransitionVisibility] shouldn't be blocked. + * + * @param block whether we should block/postpone all calls to `setVisibility`. + */ + fun setShouldBlockVisibilityChanges(block: Boolean) + + /** Perform an action when the activity launch animation ends */ + fun onActivityLaunchAnimationEnd() {} + + /** Provide an optional correction applied to the visible area during a launch animation */ + fun getPaddingForLaunchAnimation(): Rect = Rect() +} + +/** A delegate that can be used by views to make the implementation of [LaunchableView] easier. */ +class LaunchableViewDelegate( + private val view: View, + + /** + * The lambda that should set the actual visibility of [view], usually by calling + * super.setVisibility(visibility). + */ + private val superSetVisibility: (Int) -> Unit, +) : LaunchableView { + private var blockVisibilityChanges = false + private var lastVisibility = view.visibility + + /** Call this when [LaunchableView.setShouldBlockVisibilityChanges] is called. */ + override fun setShouldBlockVisibilityChanges(block: Boolean) { + if (block == blockVisibilityChanges) { + return + } + + blockVisibilityChanges = block + if (block) { + // Save the current visibility for later. + lastVisibility = view.visibility + } else { + // Restore the visibility. To avoid accessibility issues, we change the visibility twice + // which makes sure that we trigger a visibility flag change (see b/204944038#comment17 + // for more info). + if (lastVisibility == View.VISIBLE) { + superSetVisibility(View.INVISIBLE) + superSetVisibility(View.VISIBLE) + } else { + superSetVisibility(View.VISIBLE) + superSetVisibility(lastVisibility) + } + } + } + + /** Call this when [View.setVisibility] is called. */ + fun setVisibility(visibility: Int) { + if (blockVisibilityChanges) { + lastVisibility = visibility + return + } + + superSetVisibility(visibility) + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationDelegate.kt b/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationDelegate.kt new file mode 100644 index 0000000000..d465962d6e --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationDelegate.kt @@ -0,0 +1,30 @@ +package com.android.systemui.animation + +import android.annotation.UiThread +import android.view.IRemoteAnimationFinishedCallback +import android.view.RemoteAnimationTarget +import android.view.WindowManager + +/** + * A component capable of running remote animations. + * + * Expands the IRemoteAnimationRunner API by allowing for different types of more specialized + * callbacks. + */ +interface RemoteAnimationDelegate { + /** + * Called on the UI thread when the animation targets are received. Sets up and kicks off the + * animation. + */ + @UiThread + fun onAnimationStart( + @WindowManager.TransitionOldType transit: Int, + apps: Array?, + wallpapers: Array?, + nonApps: Array?, + callback: T? + ) + + /** Called on the UI thread when a signal is received to cancel the animation. */ + @UiThread fun onAnimationCancelled() +} diff --git a/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java new file mode 100644 index 0000000000..94f884673f --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OLD_NONE; +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_IS_WALLPAPER; + +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.view.WindowManager.TransitionOldType; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransitionStub; +import android.window.TransitionInfo; + +import com.android.wm.shell.shared.CounterRotator; + +public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner.Stub { + private static final String TAG = "RemoteAnimRunnerCompat"; + + public abstract void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, Runnable finishedCallback); + + @Override + public final void onAnimationStart(@TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + + onAnimationStart(transit, apps, wallpapers, + nonApps, () -> { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to call app controlled animation finished callback", e); + } + }); + } + + public IRemoteTransition toRemoteTransition() { + return wrap(this); + } + + /** Wraps a remote animation runner in a remote-transition. */ + public static RemoteTransitionStub wrap(IRemoteAnimationRunner runner) { + return new RemoteTransitionStub() { + final ArrayMap mFinishRunnables = new ArrayMap<>(); + + @Override + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + final ArrayMap leashMap = new ArrayMap<>(); + final RemoteAnimationTarget[] apps = + RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); + final RemoteAnimationTarget[] wallpapers = + RemoteAnimationTargetCompat.wrapNonApps( + info, true /* wallpapers */, t, leashMap); + final RemoteAnimationTarget[] nonApps = + RemoteAnimationTargetCompat.wrapNonApps( + info, false /* wallpapers */, t, leashMap); + + // TODO(b/177438007): Move this set-up logic into launcher's animation impl. + boolean isReturnToHome = false; + TransitionInfo.Change launcherTask = null; + TransitionInfo.Change wallpaper = null; + int launcherLayer = 0; + int rotateDelta = 0; + float displayW = 0; + float displayH = 0; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + // skip changes that we didn't wrap + if (!leashMap.containsKey(change.getLeash())) continue; + if (change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME) { + isReturnToHome = change.getMode() == TRANSIT_OPEN + || change.getMode() == TRANSIT_TO_FRONT; + launcherTask = change; + launcherLayer = info.getChanges().size() - i; + } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + wallpaper = change; + } + if (change.getParent() == null && change.getEndRotation() >= 0 + && change.getEndRotation() != change.getStartRotation()) { + rotateDelta = change.getEndRotation() - change.getStartRotation(); + displayW = change.getEndAbsBounds().width(); + displayH = change.getEndAbsBounds().height(); + } + } + + // Prepare for rotation if there is one + final CounterRotator counterLauncher = new CounterRotator(); + final CounterRotator counterWallpaper = new CounterRotator(); + if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) { + final TransitionInfo.Change parent = info.getChange(launcherTask.getParent()); + if (parent != null) { + counterLauncher.setup(t, parent.getLeash(), rotateDelta, displayW, + displayH); + } else { + Log.e(TAG, "Malformed: " + launcherTask + " has parent=" + + launcherTask.getParent() + " but it's not in info."); + } + if (counterLauncher.getSurface() != null) { + t.setLayer(counterLauncher.getSurface(), launcherLayer); + } + } + + if (isReturnToHome) { + if (counterLauncher.getSurface() != null) { + t.setLayer(counterLauncher.getSurface(), info.getChanges().size() * 3); + } + // Need to "boost" the closing things since that's what launcher expects. + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = leashMap.get(change.getLeash()); + // skip changes that we didn't wrap + if (leash == null) continue; + final int mode = info.getChanges().get(i).getMode(); + // Only deal with independent layers + if (!TransitionInfo.isIndependent(change, info)) continue; + if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { + t.setLayer(leash, info.getChanges().size() * 3 - i); + counterLauncher.addChild(t, leash); + } + } + // Make wallpaper visible immediately since launcher apparently won't do this. + for (int i = wallpapers.length - 1; i >= 0; --i) { + t.show(wallpapers[i].leash); + t.setAlpha(wallpapers[i].leash, 1.f); + } + } else { + if (launcherTask != null) { + counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash())); + } + if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) { + final TransitionInfo.Change parent = info.getChange(wallpaper.getParent()); + if (parent != null) { + counterWallpaper.setup(t, parent.getLeash(), rotateDelta, displayW, + displayH); + } else { + Log.e(TAG, "Malformed: " + wallpaper + " has parent=" + + wallpaper.getParent() + " but it's not in info."); + } + if (counterWallpaper.getSurface() != null) { + t.setLayer(counterWallpaper.getSurface(), -1); + counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash())); + } + } + } + t.apply(); + + final Runnable animationFinishedCallback = () -> { + final SurfaceControl.Transaction finishTransaction = + new SurfaceControl.Transaction(); + counterLauncher.cleanUp(finishTransaction); + counterWallpaper.cleanUp(finishTransaction); + // Release surface references now. This is apparently to free GPU memory + // before GC would. + info.releaseAllSurfaces(); + // Don't release here since launcher might still be using them. Instead + // let launcher release them (eg. via RemoteAnimationTargets) + leashMap.clear(); + try { + finishCallback.onTransitionFinished(null /* wct */, finishTransaction); + finishTransaction.close(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to call app controlled animation finished callback", e); + } + }; + synchronized (mFinishRunnables) { + mFinishRunnables.put(token, animationFinishedCallback); + } + // TODO(bc-unlcok): Pass correct transit type. + runner.onAnimationStart(TRANSIT_OLD_NONE, + apps, wallpapers, nonApps, new IRemoteAnimationFinishedCallback() { + @Override + public void onAnimationFinished() { + synchronized (mFinishRunnables) { + if (mFinishRunnables.remove(token) == null) return; + } + animationFinishedCallback.run(); + } + + @Override + public IBinder asBinder() { + return null; + } + }); + } + + @Override + public void mergeAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt + // to legacy cancel. + final Runnable finishRunnable; + synchronized (mFinishRunnables) { + finishRunnable = mFinishRunnables.remove(mergeTarget); + } + // Since we're not actually animating, release native memory now + t.close(); + info.releaseAllSurfaces(); + if (finishRunnable == null) return; + runner.onAnimationCancelled(); + finishRunnable.run(); + } + }; + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationTargetCompat.java b/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationTargetCompat.java new file mode 100644 index 0000000000..e251af4472 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/RemoteAnimationTargetCompat.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation; + +import android.util.ArrayMap; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionInfo.Change; + +import com.android.wm.shell.shared.TransitionUtil; + +import java.util.ArrayList; +import java.util.function.Predicate; + +/** + * Some utility methods for creating {@link RemoteAnimationTarget} instances. + */ +public class RemoteAnimationTargetCompat { + + /** + * Represents a TransitionInfo object as an array of old-style app targets + * + * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be + * populated by this function. If null, it is ignored. + */ + public static RemoteAnimationTarget[] wrapApps(TransitionInfo info, + SurfaceControl.Transaction t, ArrayMap leashMap) { + // LeafTaskFilter is order-dependent, so the same object needs to be used for all Change + // objects. That's why it's constructed here and captured by the lambda instead of building + // a new one ad hoc every time. + TransitionUtil.LeafTaskFilter taskFilter = new TransitionUtil.LeafTaskFilter(); + return wrap(info, t, leashMap, (change) -> { + // Intra-task activity -> activity transitions should be categorized as apps. + if (change.getActivityComponent() != null) return true; + return taskFilter.test(change); + }); + } + + /** + * Represents a TransitionInfo object as an array of old-style non-app targets + * + * @param wallpapers If true, this will return wallpaper targets; otherwise it returns + * non-wallpaper targets. + * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be + * populated by this function. If null, it is ignored. + */ + public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers, + SurfaceControl.Transaction t, ArrayMap leashMap) { + return wrap(info, t, leashMap, (change) -> { + // Intra-task activity -> activity transitions should be categorized as apps. + if (change.getActivityComponent() != null) return false; + return wallpapers + ? TransitionUtil.isWallpaper(change) : TransitionUtil.isNonApp(change); + }); + } + + private static RemoteAnimationTarget[] wrap(TransitionInfo info, + SurfaceControl.Transaction t, ArrayMap leashMap, + Predicate filter) { + final ArrayList out = new ArrayList<>(); + for (int i = 0; i < info.getChanges().size(); i++) { + TransitionInfo.Change change = info.getChanges().get(i); + if (TransitionUtil.isOrderOnly(change)) continue; + if (filter.test(change)) { + out.add(TransitionUtil.newTarget( + change, info.getChanges().size() - i, info, t, leashMap)); + } + } + return out.toArray(new RemoteAnimationTarget[out.size()]); + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/ShadeInterpolation.kt b/systemUIAnim/src/com/android/systemui/animation/ShadeInterpolation.kt new file mode 100644 index 0000000000..b89a8b0e02 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/ShadeInterpolation.kt @@ -0,0 +1,39 @@ +package com.android.systemui.animation + +import android.util.MathUtils + +object ShadeInterpolation { + + /** + * Interpolate alpha for notification background scrim during shade expansion. + * + * @param fraction Shade expansion fraction + */ + @JvmStatic + fun getNotificationScrimAlpha(fraction: Float): Float { + val mappedFraction = MathUtils.constrainedMap(0f, 1f, 0f, 0.5f, fraction) + return interpolateEaseInOut(mappedFraction) + } + + /** + * Interpolate alpha for shade content during shade expansion. + * + * @param fraction Shade expansion fraction + */ + @JvmStatic + fun getContentAlpha(fraction: Float): Float { + val mappedFraction = MathUtils.constrainedMap(0f, 1f, 0.3f, 1f, fraction) + return interpolateEaseInOut(mappedFraction) + } + + private fun interpolateEaseInOut(fraction: Float): Float { + val mappedFraction = fraction * 1.2f - 0.2f + return if (mappedFraction <= 0) { + 0f + } else { + val oneMinusFrac = 1f - mappedFraction + (1f - 0.5f * (1f - Math.cos((3.14159f * oneMinusFrac * oneMinusFrac).toDouble()))) + .toFloat() + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/TextAnimator.kt b/systemUIAnim/src/com/android/systemui/animation/TextAnimator.kt new file mode 100644 index 0000000000..978943ae7f --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/TextAnimator.kt @@ -0,0 +1,409 @@ +/* + * 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.systemui.animation + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.TimeInterpolator +import android.animation.ValueAnimator +import android.graphics.Canvas +import android.graphics.Typeface +import android.graphics.fonts.Font +import android.graphics.fonts.FontVariationAxis +import android.text.Layout +import android.util.LruCache +import kotlin.math.roundToInt +import android.util.Log + +private const val DEFAULT_ANIMATION_DURATION: Long = 300 +private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 + +typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit + +interface TypefaceVariantCache { + fun getTypefaceForVariant(fvar: String?): Typeface? + + companion object { + fun createVariantTypeface(baseTypeface: Typeface, fVar: String?): Typeface { + if (fVar.isNullOrEmpty()) { + return baseTypeface + } + + val axes = FontVariationAxis.fromFontVariationSettings(fVar) + ?.toMutableList() + ?: mutableListOf() + axes.removeIf { !baseTypeface.isSupportedAxes(it.getOpenTypeTagValue()) } + if (axes.isEmpty()) { + return baseTypeface + } + return Typeface.createFromTypefaceWithVariation(baseTypeface, axes) + } + } +} + +class TypefaceVariantCacheImpl( + var baseTypeface: Typeface, +) : TypefaceVariantCache { + private val cache = LruCache(TYPEFACE_CACHE_MAX_ENTRIES) + override fun getTypefaceForVariant(fvar: String?): Typeface? { + if (fvar == null) { + return baseTypeface + } + cache.get(fvar)?.let { + return it + } + + return TypefaceVariantCache.createVariantTypeface(baseTypeface, fvar).also { + cache.put(fvar, it) + } + } +} + +/** + * This class provides text animation between two styles. + * + * Currently this class can provide text style animation for text weight and text size. For example + * the simple view that draws text with animating text size is like as follows: + *
 
+ * ```
+ *     class SimpleTextAnimation : View {
+ *         @JvmOverloads constructor(...)
+ *
+ *         private val layout: Layout = ... // Text layout, e.g. StaticLayout.
+ *
+ *         // TextAnimator tells us when needs to be invalidate.
+ *         private val animator = TextAnimator(layout) { invalidate() }
+ *
+ *         override fun onDraw(canvas: Canvas) = animator.draw(canvas)
+ *
+ *         // Change the text size with animation.
+ *         fun setTextSize(sizePx: Float, animate: Boolean) {
+ *             animator.setTextStyle("" /* unchanged fvar... */, sizePx, animate)
+ *         }
+ *     }
+ * ```
+ *  
+ */ +class TextAnimator( + layout: Layout, + numberOfAnimationSteps: Int? = null, // Only do this number of discrete animation steps. + private val invalidateCallback: () -> Unit, +) { + var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl(layout.paint.typeface) + get() = field + set(value) { + field = value + textInterpolator.typefaceCache = value + } + + // Following two members are for mutable for testing purposes. + public var textInterpolator: TextInterpolator = + TextInterpolator(layout, typefaceCache, numberOfAnimationSteps) + public var animator: ValueAnimator = + ValueAnimator.ofFloat(1f).apply { + duration = DEFAULT_ANIMATION_DURATION + addUpdateListener { + textInterpolator.progress = + calculateProgress(it.animatedValue as Float, numberOfAnimationSteps) + invalidateCallback() + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase() + override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase() + } + ) + } + + private fun calculateProgress(animProgress: Float, numberOfAnimationSteps: Int?): Float { + if (numberOfAnimationSteps != null) { + // This clamps the progress to the nearest value of "numberOfAnimationSteps" + // discrete values between 0 and 1f. + return (animProgress * numberOfAnimationSteps).roundToInt() / + numberOfAnimationSteps.toFloat() + } + + return animProgress + } + + sealed class PositionedGlyph { + /** Mutable X coordinate of the glyph position relative from drawing offset. */ + var x: Float = 0f + + /** Mutable Y coordinate of the glyph position relative from the baseline. */ + var y: Float = 0f + + /** The current line of text being drawn, in a multi-line TextView. */ + var lineNo: Int = 0 + + /** Mutable text size of the glyph in pixels. */ + var textSize: Float = 0f + + /** Mutable color of the glyph. */ + var color: Int = 0 + + /** Immutable character offset in the text that the current font run start. */ + abstract var runStart: Int + protected set + + /** Immutable run length of the font run. */ + abstract var runLength: Int + protected set + + /** Immutable glyph index of the font run. */ + abstract var glyphIndex: Int + protected set + + /** Immutable font instance for this font run. */ + abstract var font: Font + protected set + + /** Immutable glyph ID for this glyph. */ + abstract var glyphId: Int + protected set + } + + private val fontVariationUtils = FontVariationUtils() + + fun updateLayout(layout: Layout, textSize: Float = -1f) { + textInterpolator.layout = layout + + if (textSize >= 0) { + textInterpolator.targetPaint.textSize = textSize + textInterpolator.basePaint.textSize = textSize + textInterpolator.onTargetPaintModified() + textInterpolator.onBasePaintModified() + } + } + + fun isRunning(): Boolean { + return animator.isRunning + } + + /** + * GlyphFilter applied just before drawing to canvas for tweaking positions and text size. + * + * This callback is called for each glyphs just before drawing the glyphs. This function will be + * called with the intrinsic position, size, color, glyph ID and font instance. You can mutate + * the position, size and color for tweaking animations. Do not keep the reference of passed + * glyph object. The interpolator reuses that object for avoiding object allocations. + * + * Details: The text is drawn with font run units. The font run is a text segment that draws + * with the same font. The {@code runStart} and {@code runLimit} is a range of the font run in + * the text that current glyph is in. Once the font run is determined, the system will convert + * characters into glyph IDs. The {@code glyphId} is the glyph identifier in the font and {@code + * glyphIndex} is the offset of the converted glyph array. Please note that the {@code + * glyphIndex} is not a character index, because the character will not be converted to glyph + * one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be + * composed from multiple characters. + * + * Here is an example of font runs: "fin. 終わり" + * + * Characters : f i n . _ 終 わ り + * Code Points: \u0066 \u0069 \u006E \u002E \u0020 \u7D42 \u308F \u308A + * Font Runs : <-- Roboto-Regular.ttf --><-- NotoSans-CJK.otf --> + * runStart = 0, runLength = 5 runStart = 5, runLength = 3 + * Glyph IDs : 194 48 7 8 4367 1039 1002 + * Glyph Index: 0 1 2 3 0 1 2 + * + * In this example, the "fi" is converted into ligature form, thus the single glyph ID is + * assigned for two characters, f and i. + * + * Example: + * ``` + * private val glyphFilter: GlyphCallback = { glyph, progress -> + * val index = glyph.runStart + * val i = glyph.glyphIndex + * val moveAmount = 1.3f + * val sign = (-1 + 2 * ((i + index) % 2)) + * val turnProgress = if (progress < .5f) progress / 0.5f else (1.0f - progress) / 0.5f + * + * // You can modify (x, y) coordinates, textSize and color during animation. + * glyph.textSize += glyph.textSize * sign * moveAmount * turnProgress + * glyph.y += glyph.y * sign * moveAmount * turnProgress + * glyph.x += glyph.x * sign * moveAmount * turnProgress + * } + * ``` + */ + var glyphFilter: GlyphCallback? + get() = textInterpolator.glyphFilter + set(value) { + textInterpolator.glyphFilter = value + } + + fun draw(c: Canvas) = textInterpolator.draw(c) + + /** + * Set text style with animation. + * + * By passing -1 to weight, the view preserve the current weight. + * By passing -1 to textSize, the view preserve the current text size. + * Bu passing -1 to duration, the default text animation, 1000ms, is used. + * By passing false to animate, the text will be updated without animation. + * + * @param fvar an optional text fontVariationSettings. + * @param textSize an optional font size. + * @param colors an optional colors array that must be the same size as numLines passed to + * the TextInterpolator + * @param strokeWidth an optional paint stroke width + * @param animate an optional boolean indicating true for showing style transition as animation, + * false for immediate style transition. True by default. + * @param duration an optional animation duration in milliseconds. This is ignored if animate is + * false. + * @param interpolator an optional time interpolator. If null is passed, last set interpolator + * will be used. This is ignored if animate is false. + */ + fun setTextStyle( + fvar: String? = "", + textSize: Float = -1f, + color: Int? = null, + strokeWidth: Float = -1f, + animate: Boolean = true, + duration: Long = -1L, + interpolator: TimeInterpolator? = null, + delay: Long = 0, + onAnimationEnd: Runnable? = null, + ) = setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration, + interpolator, delay, onAnimationEnd, updateLayoutOnFailure = true) + + private fun setTextStyleInternal( + fvar: String?, + textSize: Float, + color: Int?, + strokeWidth: Float, + animate: Boolean, + duration: Long, + interpolator: TimeInterpolator?, + delay: Long, + onAnimationEnd: Runnable?, + updateLayoutOnFailure: Boolean, + ) { + try { + if (animate) { + animator.cancel() + textInterpolator.rebase() + } + + if (textSize >= 0) { + textInterpolator.targetPaint.textSize = textSize + } + if (!fvar.isNullOrBlank()) { + textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar) + } + if (color != null) { + textInterpolator.targetPaint.color = color + } + if (strokeWidth >= 0F) { + textInterpolator.targetPaint.strokeWidth = strokeWidth + } + textInterpolator.onTargetPaintModified() + + if (animate) { + animator.startDelay = delay + animator.duration = + if (duration == -1L) { + DEFAULT_ANIMATION_DURATION + } else { + duration + } + interpolator?.let { animator.interpolator = it } + if (onAnimationEnd != null) { + val listener = object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onAnimationEnd.run() + animator.removeListener(this) + } + override fun onAnimationCancel(animation: Animator) { + animator.removeListener(this) + } + } + animator.addListener(listener) + } + animator.start() + } else { + // No animation is requested, thus set base and target state to the same state. + textInterpolator.progress = 1f + textInterpolator.rebase() + invalidateCallback() + } + } catch (ex: IllegalArgumentException) { + if (updateLayoutOnFailure) { + Log.e(TAG, "setTextStyleInternal: Exception caught but retrying. This is usually" + + " due to the layout having changed unexpectedly without being notified.", ex) + updateLayout(textInterpolator.layout) + setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration, + interpolator, delay, onAnimationEnd, updateLayoutOnFailure = false) + } else { + throw ex + } + } + } + + /** + * Set text style with animation. Similar as + * fun setTextStyle( + * fvar: String? = "", + * textSize: Float = -1f, + * color: Int? = null, + * strokeWidth: Float = -1f, + * animate: Boolean = true, + * duration: Long = -1L, + * interpolator: TimeInterpolator? = null, + * delay: Long = 0, + * onAnimationEnd: Runnable? = null + * ) + * + * @param weight an optional style value for `wght` in fontVariationSettings. + * @param width an optional style value for `wdth` in fontVariationSettings. + * @param opticalSize an optional style value for `opsz` in fontVariationSettings. + * @param roundness an optional style value for `ROND` in fontVariationSettings. + */ + fun setTextStyle( + weight: Int = -1, + width: Int = -1, + opticalSize: Int = -1, + roundness: Int = -1, + textSize: Float = -1f, + color: Int? = null, + strokeWidth: Float = -1f, + animate: Boolean = true, + duration: Long = -1L, + interpolator: TimeInterpolator? = null, + delay: Long = 0, + onAnimationEnd: Runnable? = null + ) = setTextStyleInternal( + fvar = fontVariationUtils.updateFontVariation( + weight = weight, + width = width, + opticalSize = opticalSize, + roundness = roundness, + ), + textSize = textSize, + color = color, + strokeWidth = strokeWidth, + animate = animate, + duration = duration, + interpolator = interpolator, + delay = delay, + onAnimationEnd = onAnimationEnd, + updateLayoutOnFailure = true, + ) + + companion object { + private val TAG = TextAnimator::class.simpleName!! + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/TextInterpolator.kt b/systemUIAnim/src/com/android/systemui/animation/TextInterpolator.kt new file mode 100644 index 0000000000..02caeeddd7 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/TextInterpolator.kt @@ -0,0 +1,531 @@ +/* + * 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.systemui.animation + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.fonts.Font +import android.graphics.fonts.FontVariationAxis +import android.graphics.text.PositionedGlyphs +import android.text.Layout +import android.text.TextPaint +import android.text.TextShaper +import android.util.MathUtils +import com.android.internal.graphics.ColorUtils +import java.lang.Math.max + +/** Provide text style linear interpolation for plain text. */ +class TextInterpolator( + layout: Layout, + var typefaceCache: TypefaceVariantCache, + numberOfAnimationSteps: Int? = null, +) { + /** + * Returns base paint used for interpolation. + * + * Once you modified the style parameters, you have to call reshapeText to recalculate base text + * layout. + * + * Do not bypass the cache and update the typeface or font variation directly. + * + * @return a paint object + */ + val basePaint = TextPaint(layout.paint) + + /** + * Returns target paint used for interpolation. + * + * Once you modified the style parameters, you have to call reshapeText to recalculate target + * text layout. + * + * Do not bypass the cache and update the typeface or font variation directly. + * + * @return a paint object + */ + val targetPaint = TextPaint(layout.paint) + + /** + * A class represents a single font run. + * + * A font run is a range that will be drawn with the same font. + */ + private data class FontRun( + val start: Int, // inclusive + val end: Int, // exclusive + var baseFont: Font, + var targetFont: Font + ) { + val length: Int + get() = end - start + } + + /** A class represents text layout of a single run. */ + private class Run( + val glyphIds: IntArray, + val baseX: FloatArray, // same length as glyphIds + val baseY: FloatArray, // same length as glyphIds + val targetX: FloatArray, // same length as glyphIds + val targetY: FloatArray, // same length as glyphIds + val fontRuns: List + ) + + /** A class represents text layout of a single line. */ + private class Line(val runs: List) + + private var lines = listOf() + private val fontInterpolator = FontInterpolator(numberOfAnimationSteps) + + // Recycling object for glyph drawing and tweaking. + private val tmpPaint = TextPaint() + private val tmpPaintForGlyph by lazy { TextPaint() } + private val tmpGlyph by lazy { MutablePositionedGlyph() } + // Will be extended for the longest font run if needed. + private var tmpPositionArray = FloatArray(20) + + /** + * The progress position of the interpolation. + * + * The 0f means the start state, 1f means the end state. + */ + var progress: Float = 0f + + /** + * The layout used for drawing text. + * + * Only non-styled text is supported. Even if the given layout is created from Spanned, the span + * information is not used. + * + * The paint objects used for interpolation are not changed by this method call. + * + * Note: disabling ligature is strongly recommended if you give extra letter spacing since they + * may be disjointed based on letter spacing value and cannot be interpolated. Animator will + * throw runtime exception if they cannot be interpolated. + */ + var layout: Layout = layout + get() = field + set(value) { + field = value + shapeText(value) + } + + var shapedText: String = "" + private set + + init { + // shapeText needs to be called after all members are initialized. + shapeText(layout) + } + + /** + * Recalculate internal text layout for interpolation. + * + * Whenever the target paint is modified, call this method to recalculate internal text layout + * used for interpolation. + */ + fun onTargetPaintModified() { + updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false) + } + + /** + * Recalculate internal text layout for interpolation. + * + * Whenever the base paint is modified, call this method to recalculate internal text layout + * used for interpolation. + */ + fun onBasePaintModified() { + updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true) + } + + /** + * Rebase the base state to the middle of the interpolation. + * + * The text interpolator does not calculate all the text position by text shaper due to + * performance reasons. Instead, the text interpolator shape the start and end state and + * calculate text position of the middle state by linear interpolation. Due to this trick, the + * text positions of the middle state is likely different from the text shaper result. So, if + * you want to start animation from the middle state, you will see the glyph jumps due to this + * trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different from + * text shape result of weight 550. + * + * After calling this method, do not call onBasePaintModified() since it reshape the text and + * update the base state. As in above notice, the text shaping result at current progress is + * different shaped result. By calling onBasePaintModified(), you may see the glyph jump. + * + * By calling this method, the progress will be reset to 0. + * + * This API is useful to continue animation from the middle of the state. For example, if you + * animate weight from 200 to 400, then if you want to move back to 200 at the half of the + * animation, it will look like + *
 
+     * ```
+     *     val interp = TextInterpolator(layout)
+     *
+     *     // Interpolate between weight 200 to 400.
+     *     interp.basePaint.fontVariationSettings = "'wght' 200"
+     *     interp.onBasePaintModified()
+     *     interp.targetPaint.fontVariationSettings = "'wght' 400"
+     *     interp.onTargetPaintModified()
+     *
+     *     // animate
+     *     val animator = ValueAnimator.ofFloat(1f).apply {
+     *         addUpdaterListener {
+     *             interp.progress = it.animateValue as Float
+     *         }
+     *     }.start()
+     *
+     *     // Here, assuming you receive some event and want to start new animation from current
+     *     // state.
+     *     OnSomeEvent {
+     *         animator.cancel()
+     *
+     *         // start another animation from the current state.
+     *         interp.rebase() // Use current state as base state.
+     *         interp.targetPaint.fontVariationSettings = "'wght' 200" // set new target
+     *         interp.onTargetPaintModified() // reshape target
+     *
+     *         // Here the textInterpolator interpolate from 'wght' from 300 to 200 if the current
+     *         // progress is 0.5
+     *         animator.start()
+     *     }
+     * ```
+     *  
+ */ + fun rebase() { + if (progress == 0f) { + return + } else if (progress == 1f) { + basePaint.set(targetPaint) + } else { + lerp(basePaint, targetPaint, progress, tmpPaint) + basePaint.set(tmpPaint) + } + + lines.forEach { line -> + line.runs.forEach { run -> + for (i in run.baseX.indices) { + run.baseX[i] = MathUtils.lerp(run.baseX[i], run.targetX[i], progress) + run.baseY[i] = MathUtils.lerp(run.baseY[i], run.targetY[i], progress) + } + run.fontRuns.forEach { fontRun -> + fontRun.baseFont = + fontInterpolator.lerp(fontRun.baseFont, fontRun.targetFont, progress) + val fvar = FontVariationAxis.toFontVariationSettings(fontRun.baseFont.axes) + basePaint.typeface = typefaceCache.getTypefaceForVariant(fvar) + } + } + } + + progress = 0f + } + + /** + * Draws interpolated text at the given progress. + * + * @param canvas a canvas. + */ + fun draw(canvas: Canvas) { + lerp(basePaint, targetPaint, progress, tmpPaint) + lines.forEachIndexed { lineNo, line -> + line.runs.forEach { run -> + canvas.save() + try { + // Move to drawing origin. + val origin = layout.getDrawOrigin(lineNo) + canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat()) + + run.fontRuns.forEach { fontRun -> + drawFontRun(canvas, run, fontRun, lineNo, tmpPaint) + } + } finally { + canvas.restore() + } + } + } + } + + // Shape text with current paint parameters. + private fun shapeText(layout: Layout) { + val baseLayout = shapeText(layout, basePaint) + val targetLayout = shapeText(layout, targetPaint) + + require(baseLayout.size == targetLayout.size) { + "The new layout result has different line count." + } + + var maxRunLength = 0 + lines = + baseLayout.zip(targetLayout) { baseLine, targetLine -> + val runs = + baseLine.zip(targetLine) { base, target -> + require(base.glyphCount() == target.glyphCount()) { + "Inconsistent glyph count at line ${lines.size}" + } + + val glyphCount = base.glyphCount() + + // Good to recycle the array if the existing array can hold the new layout + // result. + val glyphIds = + IntArray(glyphCount) { + base.getGlyphId(it).also { baseGlyphId -> + require(baseGlyphId == target.getGlyphId(it)) { + "Inconsistent glyph ID at $it in line ${lines.size}" + } + } + } + + val baseX = FloatArray(glyphCount) { base.getGlyphX(it) } + val baseY = FloatArray(glyphCount) { base.getGlyphY(it) } + val targetX = FloatArray(glyphCount) { target.getGlyphX(it) } + val targetY = FloatArray(glyphCount) { target.getGlyphY(it) } + + // Calculate font runs + val fontRun = mutableListOf() + if (glyphCount != 0) { + var start = 0 + var baseFont = base.getFont(start) + var targetFont = target.getFont(start) + require(FontInterpolator.canInterpolate(baseFont, targetFont)) { + "Cannot interpolate font at $start ($baseFont vs $targetFont)" + } + + for (i in 1 until glyphCount) { + val nextBaseFont = base.getFont(i) + val nextTargetFont = target.getFont(i) + + if (baseFont !== nextBaseFont) { + require(targetFont !== nextTargetFont) { + "Base font has changed at $i but target font is unchanged." + } + // Font transition point. push run and reset context. + fontRun.add(FontRun(start, i, baseFont, targetFont)) + maxRunLength = max(maxRunLength, i - start) + baseFont = nextBaseFont + targetFont = nextTargetFont + start = i + require(FontInterpolator.canInterpolate(baseFont, targetFont)) { + "Cannot interpolate font at $start" + + " ($baseFont vs $targetFont)" + } + } else { // baseFont === nextBaseFont + require(targetFont === nextTargetFont) { + "Base font is unchanged at $i but target font has changed." + } + } + } + fontRun.add(FontRun(start, glyphCount, baseFont, targetFont)) + maxRunLength = max(maxRunLength, glyphCount - start) + } + Run(glyphIds, baseX, baseY, targetX, targetY, fontRun) + } + Line(runs) + } + + // Update float array used for drawing. + if (tmpPositionArray.size < maxRunLength * 2) { + tmpPositionArray = FloatArray(maxRunLength * 2) + } + } + + private class MutablePositionedGlyph : TextAnimator.PositionedGlyph() { + override var runStart: Int = 0 + public set + override var runLength: Int = 0 + public set + override var glyphIndex: Int = 0 + public set + override lateinit var font: Font + public set + override var glyphId: Int = 0 + public set + } + + var glyphFilter: GlyphCallback? = null + + // Draws single font run. + private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) { + var arrayIndex = 0 + val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress) + + val glyphFilter = glyphFilter + if (glyphFilter == null) { + for (i in run.start until run.end) { + tmpPositionArray[arrayIndex++] = + MathUtils.lerp(line.baseX[i], line.targetX[i], progress) + tmpPositionArray[arrayIndex++] = + MathUtils.lerp(line.baseY[i], line.targetY[i], progress) + } + c.drawGlyphs(line.glyphIds, run.start, tmpPositionArray, 0, run.length, font, paint) + return + } + + tmpGlyph.font = font + tmpGlyph.runStart = run.start + tmpGlyph.runLength = run.end - run.start + tmpGlyph.lineNo = lineNo + + tmpPaintForGlyph.set(paint) + var prevStart = run.start + + for (i in run.start until run.end) { + tmpGlyph.glyphIndex = i + tmpGlyph.glyphId = line.glyphIds[i] + tmpGlyph.x = MathUtils.lerp(line.baseX[i], line.targetX[i], progress) + tmpGlyph.y = MathUtils.lerp(line.baseY[i], line.targetY[i], progress) + tmpGlyph.textSize = paint.textSize + tmpGlyph.color = paint.color + + glyphFilter(tmpGlyph, progress) + + if (tmpGlyph.textSize != paint.textSize || tmpGlyph.color != paint.color) { + tmpPaintForGlyph.textSize = tmpGlyph.textSize + tmpPaintForGlyph.color = tmpGlyph.color + + c.drawGlyphs( + line.glyphIds, + prevStart, + tmpPositionArray, + 0, + i - prevStart, + font, + tmpPaintForGlyph + ) + prevStart = i + arrayIndex = 0 + } + + tmpPositionArray[arrayIndex++] = tmpGlyph.x + tmpPositionArray[arrayIndex++] = tmpGlyph.y + } + + c.drawGlyphs( + line.glyphIds, + prevStart, + tmpPositionArray, + 0, + run.end - prevStart, + font, + tmpPaintForGlyph + ) + } + + private fun updatePositionsAndFonts( + layoutResult: List>, + updateBase: Boolean + ) { + // Update target positions with newly calculated text layout. + check(layoutResult.size == lines.size) { "The new layout result has different line count." } + + lines.zip(layoutResult) { line, runs -> + line.runs.zip(runs) { lineRun, newGlyphs -> + require(newGlyphs.glyphCount() == lineRun.glyphIds.size) { + "The new layout has different glyph count." + } + + lineRun.fontRuns.forEach { run -> + val newFont = newGlyphs.getFont(run.start) + for (i in run.start until run.end) { + require(newGlyphs.getGlyphId(run.start) == lineRun.glyphIds[run.start]) { + "The new layout has different glyph ID at ${run.start}" + } + require(newFont === newGlyphs.getFont(i)) { + "The new layout has different font run." + + " $newFont vs ${newGlyphs.getFont(i)} at $i" + } + } + + // The passing base font and target font is already interpolatable, so just + // check new font can be interpolatable with base font. + require(FontInterpolator.canInterpolate(newFont, run.baseFont)) { + "New font cannot be interpolated with existing font. $newFont," + + " ${run.baseFont}" + } + + if (updateBase) { + run.baseFont = newFont + } else { + run.targetFont = newFont + } + } + + if (updateBase) { + for (i in lineRun.baseX.indices) { + lineRun.baseX[i] = newGlyphs.getGlyphX(i) + lineRun.baseY[i] = newGlyphs.getGlyphY(i) + } + } else { + for (i in lineRun.baseX.indices) { + lineRun.targetX[i] = newGlyphs.getGlyphX(i) + lineRun.targetY[i] = newGlyphs.getGlyphY(i) + } + } + } + } + } + + // Linear interpolate the paint. + private fun lerp(from: Paint, to: Paint, progress: Float, out: Paint) { + out.set(from) + + // Currently only font size & colors are interpolated. + // TODO(172943390): Add other interpolation or support custom interpolator. + out.textSize = MathUtils.lerp(from.textSize, to.textSize, progress) + out.color = ColorUtils.blendARGB(from.color, to.color, progress) + out.strokeWidth = MathUtils.lerp(from.strokeWidth, to.strokeWidth, progress) + } + + // Shape the text and stores the result to out argument. + private fun shapeText(layout: Layout, paint: TextPaint): List> { + var text = StringBuilder() + val out = mutableListOf>() + for (lineNo in 0 until layout.lineCount) { // Shape all lines. + val lineStart = layout.getLineStart(lineNo) + val lineEnd = layout.getLineEnd(lineNo) + var count = lineEnd - lineStart + // Do not render the last character in the line if it's a newline and unprintable + val last = lineStart + count - 1 + if (last > lineStart && last < layout.text.length && layout.text[last] == '\n') { + count-- + } + + val runs = mutableListOf() + TextShaper.shapeText( + layout.text, + lineStart, + count, + layout.textDirectionHeuristic, + paint + ) { _, _, glyphs, _ -> + runs.add(glyphs) + } + out.add(runs) + + if (lineNo > 0) { + text.append("\n") + } + text.append(layout.text.substring(lineStart, lineEnd)) + } + shapedText = text.toString() + return out + } +} + +private fun Layout.getDrawOrigin(lineNo: Int) = + if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) { + getLineLeft(lineNo) + } else { + getLineRight(lineNo) + } diff --git a/systemUIAnim/src/com/android/systemui/animation/TransitionAnimator.kt b/systemUIAnim/src/com/android/systemui/animation/TransitionAnimator.kt new file mode 100644 index 0000000000..8e824e60d4 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/TransitionAnimator.kt @@ -0,0 +1,576 @@ +/* + * 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.systemui.animation + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.drawable.GradientDrawable +import android.util.Log +import android.util.MathUtils +import android.view.View +import android.view.ViewGroup +import android.view.animation.Interpolator +import androidx.annotation.VisibleForTesting +import com.android.app.animation.Interpolators.LINEAR +import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary +import java.util.concurrent.Executor +import kotlin.math.roundToInt + +private const val TAG = "TransitionAnimator" + +/** A base class to animate a window (activity or dialog) launch to or return from a view . */ +class TransitionAnimator( + private val mainExecutor: Executor, + private val timings: Timings, + private val interpolators: Interpolators, +) { + companion object { + internal const val DEBUG = false + private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) + + /** + * Given the [linearProgress] of a transition animation, return the linear progress of the + * sub-animation starting [delay] ms after the transition animation and that lasts + * [duration]. + */ + @JvmStatic + fun getProgress( + timings: Timings, + linearProgress: Float, + delay: Long, + duration: Long + ): Float { + return MathUtils.constrain( + (linearProgress * timings.totalDuration - delay) / duration, + 0.0f, + 1.0f + ) + } + + internal fun checkReturnAnimationFrameworkFlag() { + check(returnAnimationFrameworkLibrary()) { + "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is " + + "disabled" + } + } + } + + private val transitionContainerLocation = IntArray(2) + private val cornerRadii = FloatArray(8) + + /** + * A controller that takes care of applying the animation to an expanding view. + * + * Note that all callbacks (onXXX methods) are all called on the main thread. + */ + interface Controller { + /** + * The container in which the view that started the animation will be animating together + * with the opening or closing window. + * + * This will be used to: + * - Get the associated [Context]. + * - Compute whether we are expanding to or contracting from fully above the transition + * container. + * - Get the overlay into which we put the window background layer, while the animating + * window is not visible (see [openingWindowSyncView]). + * + * This container can be changed to force this [Controller] to animate the expanding view + * inside a different location, for instance to ensure correct layering during the + * animation. + */ + var transitionContainer: ViewGroup + + /** Whether the animation being controlled is a launch or a return. */ + val isLaunching: Boolean + + /** + * If [isLaunching], the [View] with which the opening app window should be synchronized + * once it starts to be visible. Otherwise, the [View] with which the closing app window + * should be synchronized until it stops being visible. + * + * We will also move the window background layer to this view's overlay once the opening + * window is visible (if [isLaunching]), or from this view's overlay once the closing window + * stop being visible (if ![isLaunching]). + * + * If null, this will default to [transitionContainer]. + */ + val openingWindowSyncView: View? + get() = null + + /** + * Return the [State] of the view that will be animated. We will animate from this state to + * the final window state. + * + * Note: This state will be mutated and passed to [onTransitionAnimationProgress] during the + * animation. + */ + fun createAnimatorState(): State + + /** + * The animation started. This is typically used to initialize any additional resource + * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding + * fully above the [transitionContainer]. + */ + fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {} + + /** The animation made progress and the expandable view [state] should be updated. */ + fun onTransitionAnimationProgress(state: State, progress: Float, linearProgress: Float) {} + + /** + * The animation ended. This will be called *if and only if* [onTransitionAnimationStart] + * was called previously. This is typically used to clean up the resources initialized when + * the animation was started. + */ + fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {} + } + + /** The state of an expandable view during a [TransitionAnimator] animation. */ + open class State( + /** The position of the view in screen space coordinates. */ + var top: Int = 0, + var bottom: Int = 0, + var left: Int = 0, + var right: Int = 0, + var topCornerRadius: Float = 0f, + var bottomCornerRadius: Float = 0f + ) { + private val startTop = top + + val width: Int + get() = right - left + + val height: Int + get() = bottom - top + + open val topChange: Int + get() = top - startTop + + val centerX: Float + get() = left + width / 2f + + val centerY: Float + get() = top + height / 2f + + /** Whether the expanding view should be visible or hidden. */ + var visible: Boolean = true + } + + interface Animation { + /** Cancel the animation. */ + fun cancel() + } + + /** The timings (durations and delays) used by this animator. */ + data class Timings( + /** The total duration of the animation. */ + val totalDuration: Long, + + /** The time to wait before fading out the expanding content. */ + val contentBeforeFadeOutDelay: Long, + + /** The duration of the expanding content fade out. */ + val contentBeforeFadeOutDuration: Long, + + /** + * The time to wait before fading in the expanded content (usually an activity or dialog + * window). + */ + val contentAfterFadeInDelay: Long, + + /** The duration of the expanded content fade in. */ + val contentAfterFadeInDuration: Long + ) + + /** The interpolators used by this animator. */ + data class Interpolators( + /** The interpolator used for the Y position, width, height and corner radius. */ + val positionInterpolator: Interpolator, + + /** + * The interpolator used for the X position. This can be different than + * [positionInterpolator] to create an arc-path during the animation. + */ + val positionXInterpolator: Interpolator = positionInterpolator, + + /** The interpolator used when fading out the expanding content. */ + val contentBeforeFadeOutInterpolator: Interpolator, + + /** The interpolator used when fading in the expanded content. */ + val contentAfterFadeInInterpolator: Interpolator + ) + + /** + * Start a transition animation controlled by [controller] towards [endState]. An intermediary + * layer with [windowBackgroundColor] will fade in then (optionally) fade out above the + * expanding view, and should be the same background color as the opening (or closing) window. + * + * If [fadeWindowBackgroundLayer] is true, then this intermediary layer will fade out during the + * second half of the animation (if [Controller.isLaunching] or fade in during the first half of + * the animation (if ![Controller.isLaunching]), and will have SRC blending mode (ultimately + * punching a hole in the [transition container][Controller.transitionContainer]) iff [drawHole] + * is true. + */ + fun startAnimation( + controller: Controller, + endState: State, + windowBackgroundColor: Int, + fadeWindowBackgroundLayer: Boolean = true, + drawHole: Boolean = false, + ): Animation { + if (!controller.isLaunching) checkReturnAnimationFrameworkFlag() + + // We add an extra layer with the same color as the dialog/app splash screen background + // color, which is usually the same color of the app background. We first fade in this layer + // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the + // transition container and reveal the opening window. + val windowBackgroundLayer = + GradientDrawable().apply { + setColor(windowBackgroundColor) + alpha = 0 + } + + val animator = + createAnimator( + controller, + endState, + windowBackgroundLayer, + fadeWindowBackgroundLayer, + drawHole + ) + animator.start() + + return object : Animation { + override fun cancel() { + animator.cancel() + } + } + } + + @VisibleForTesting + fun createAnimator( + controller: Controller, + endState: State, + windowBackgroundLayer: GradientDrawable, + fadeWindowBackgroundLayer: Boolean = true, + drawHole: Boolean = false + ): ValueAnimator { + val state = controller.createAnimatorState() + + // Start state. + val startTop = state.top + val startBottom = state.bottom + val startLeft = state.left + val startRight = state.right + val startCenterX = (startLeft + startRight) / 2f + val startWidth = startRight - startLeft + val startTopCornerRadius = state.topCornerRadius + val startBottomCornerRadius = state.bottomCornerRadius + + // End state. + var endTop = endState.top + var endBottom = endState.bottom + var endLeft = endState.left + var endRight = endState.right + var endCenterX = (endLeft + endRight) / 2f + var endWidth = endRight - endLeft + val endTopCornerRadius = endState.topCornerRadius + val endBottomCornerRadius = endState.bottomCornerRadius + + fun maybeUpdateEndState() { + if ( + endTop != endState.top || + endBottom != endState.bottom || + endLeft != endState.left || + endRight != endState.right + ) { + endTop = endState.top + endBottom = endState.bottom + endLeft = endState.left + endRight = endState.right + endCenterX = (endLeft + endRight) / 2f + endWidth = endRight - endLeft + } + } + + val transitionContainer = controller.transitionContainer + val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState) + + // Update state. + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = timings.totalDuration + animator.interpolator = LINEAR + + // Whether we should move the [windowBackgroundLayer] into the overlay of + // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or + // from it once the closing app window stops being visible. + // This is necessary as a one-off sync so we can avoid syncing at every frame, especially + // in complex interactions like launching an activity from a dialog. See + // b/214961273#comment2 for more details. + val openingWindowSyncView = controller.openingWindowSyncView + val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay + val moveBackgroundLayerWhenAppVisibilityChanges = + openingWindowSyncView != null && + openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl + + val transitionContainerOverlay = transitionContainer.overlay + var movedBackgroundLayer = false + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator, isReverse: Boolean) { + if (DEBUG) { + Log.d(TAG, "Animation started") + } + controller.onTransitionAnimationStart(isExpandingFullyAbove) + + // Add the drawable to the transition container overlay. Overlays always draw + // drawables after views, so we know that it will be drawn above any view added + // by the controller. + if (controller.isLaunching || openingWindowSyncViewOverlay == null) { + transitionContainerOverlay.add(windowBackgroundLayer) + } else { + openingWindowSyncViewOverlay.add(windowBackgroundLayer) + } + } + + override fun onAnimationEnd(animation: Animator) { + if (DEBUG) { + Log.d(TAG, "Animation ended") + } + + // TODO(b/330672236): Post this to the main thread instead so that it does not + // flicker with Flexiglass enabled. + controller.onTransitionAnimationEnd(isExpandingFullyAbove) + transitionContainerOverlay.remove(windowBackgroundLayer) + + if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) { + openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) + } + } + } + ) + + animator.addUpdateListener { animation -> + maybeUpdateEndState() + + // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non + // reversed animation. + val linearProgress = animation.animatedFraction + val progress = interpolators.positionInterpolator.getInterpolation(linearProgress) + val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress) + + val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress) + val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f + + state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt() + state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt() + state.left = (xCenter - halfWidth).roundToInt() + state.right = (xCenter + halfWidth).roundToInt() + + state.topCornerRadius = + MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress) + state.bottomCornerRadius = + MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress) + + state.visible = + if (controller.isLaunching) { + // The expanding view can/should be hidden once it is completely covered by the + // opening window. + getProgress( + timings, + linearProgress, + timings.contentBeforeFadeOutDelay, + timings.contentBeforeFadeOutDuration + ) < 1 + } else { + getProgress( + timings, + linearProgress, + timings.contentAfterFadeInDelay, + timings.contentAfterFadeInDuration + ) > 0 + } + + if ( + controller.isLaunching && + moveBackgroundLayerWhenAppVisibilityChanges && + !state.visible && + !movedBackgroundLayer + ) { + // The expanding view is not visible, so the opening app is visible. If this is + // the first frame when it happens, trigger a one-off sync and move the + // background layer in its new container. + movedBackgroundLayer = true + + transitionContainerOverlay.remove(windowBackgroundLayer) + openingWindowSyncViewOverlay!!.add(windowBackgroundLayer) + + ViewRootSync.synchronizeNextDraw( + transitionContainer, + openingWindowSyncView, + then = {} + ) + } else if ( + !controller.isLaunching && + moveBackgroundLayerWhenAppVisibilityChanges && + state.visible && + !movedBackgroundLayer + ) { + // The contracting view is now visible, so the closing app is not. If this is + // the first frame when it happens, trigger a one-off sync and move the + // background layer in its new container. + movedBackgroundLayer = true + + openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer) + transitionContainerOverlay.add(windowBackgroundLayer) + + ViewRootSync.synchronizeNextDraw( + openingWindowSyncView, + transitionContainer, + then = {} + ) + } + + val container = + if (movedBackgroundLayer) { + openingWindowSyncView!! + } else { + controller.transitionContainer + } + + applyStateToWindowBackgroundLayer( + windowBackgroundLayer, + state, + linearProgress, + container, + fadeWindowBackgroundLayer, + drawHole, + controller.isLaunching + ) + controller.onTransitionAnimationProgress(state, progress, linearProgress) + } + + return animator + } + + /** Return whether we are expanding fully above the [transitionContainer]. */ + internal fun isExpandingFullyAbove(transitionContainer: View, endState: State): Boolean { + transitionContainer.getLocationOnScreen(transitionContainerLocation) + return endState.top <= transitionContainerLocation[1] && + endState.bottom >= transitionContainerLocation[1] + transitionContainer.height && + endState.left <= transitionContainerLocation[0] && + endState.right >= transitionContainerLocation[0] + transitionContainer.width + } + + private fun applyStateToWindowBackgroundLayer( + drawable: GradientDrawable, + state: State, + linearProgress: Float, + transitionContainer: View, + fadeWindowBackgroundLayer: Boolean, + drawHole: Boolean, + isLaunching: Boolean + ) { + // Update position. + transitionContainer.getLocationOnScreen(transitionContainerLocation) + drawable.setBounds( + state.left - transitionContainerLocation[0], + state.top - transitionContainerLocation[1], + state.right - transitionContainerLocation[0], + state.bottom - transitionContainerLocation[1] + ) + + // Update radius. + cornerRadii[0] = state.topCornerRadius + cornerRadii[1] = state.topCornerRadius + cornerRadii[2] = state.topCornerRadius + cornerRadii[3] = state.topCornerRadius + cornerRadii[4] = state.bottomCornerRadius + cornerRadii[5] = state.bottomCornerRadius + cornerRadii[6] = state.bottomCornerRadius + cornerRadii[7] = state.bottomCornerRadius + drawable.cornerRadii = cornerRadii + + // We first fade in the background layer to hide the expanding view, then fade it out + // with SRC mode to draw a hole punch in the status bar and reveal the opening window. + val fadeInProgress = + getProgress( + timings, + linearProgress, + timings.contentBeforeFadeOutDelay, + timings.contentBeforeFadeOutDuration + ) + + if (isLaunching) { + if (fadeInProgress < 1) { + val alpha = + interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) + drawable.alpha = (alpha * 0xFF).roundToInt() + } else if (fadeWindowBackgroundLayer) { + val fadeOutProgress = + getProgress( + timings, + linearProgress, + timings.contentAfterFadeInDelay, + timings.contentAfterFadeInDuration + ) + val alpha = + 1 - + interpolators.contentAfterFadeInInterpolator.getInterpolation( + fadeOutProgress + ) + drawable.alpha = (alpha * 0xFF).roundToInt() + + if (drawHole) { + drawable.setXfermode(SRC_MODE) + } + } else { + drawable.alpha = 0xFF + } + } else { + if (fadeInProgress < 1 && fadeWindowBackgroundLayer) { + val alpha = + interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) + drawable.alpha = (alpha * 0xFF).roundToInt() + + if (drawHole) { + drawable.setXfermode(SRC_MODE) + } + } else { + val fadeOutProgress = + getProgress( + timings, + linearProgress, + timings.contentAfterFadeInDelay, + timings.contentAfterFadeInDuration + ) + val alpha = + 1 - + interpolators.contentAfterFadeInInterpolator.getInterpolation( + fadeOutProgress + ) + drawable.alpha = (alpha * 0xFF).roundToInt() + drawable.setXfermode(null) + } + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/ViewDialogTransitionAnimatorController.kt b/systemUIAnim/src/com/android/systemui/animation/ViewDialogTransitionAnimatorController.kt new file mode 100644 index 0000000000..307bc95941 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/ViewDialogTransitionAnimatorController.kt @@ -0,0 +1,128 @@ +/* + * 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.systemui.animation + +import android.util.Log +import android.view.GhostView +import android.view.View +import android.view.ViewGroup +import android.view.ViewRootImpl +import com.android.internal.jank.InteractionJankMonitor + +private const val TAG = "ViewDialogTransitionAnimatorController" + +/** A [DialogTransitionAnimator.Controller] that can animate a [View] from/to a dialog. */ +class ViewDialogTransitionAnimatorController +internal constructor( + private val source: View, + override val cuj: DialogCuj?, +) : DialogTransitionAnimator.Controller { + override val viewRoot: ViewRootImpl? + get() = source.viewRootImpl + + override val sourceIdentity: Any = source + + override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { + // Delay the calls to `source.setVisibility()` during the animation. This must be called + // before `GhostView.addGhost()` is called because the latter will change the *transition* + // visibility, which won't be blocked and will affect the normal View visibility that is + // saved by `setShouldBlockVisibilityChanges()` for a later restoration. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + + // Create a temporary ghost of the source (which will make it invisible) and add it + // to the host dialog. + if (source.parent !is ViewGroup) { + // This should usually not happen, but let's make sure we don't call GhostView.addGhost + // and crash if the view was detached right before we started the animation. + Log.w(TAG, "source was detached right before drawing was moved to overlay") + } else { + GhostView.addGhost(source, viewGroup) + } + } + + override fun stopDrawingInOverlay() { + // Note: here we should remove the ghost from the overlay, but in practice this is + // already done by the transition controller created below. + + if (source is LaunchableView) { + // Make sure we allow the source to change its visibility again and restore its previous + // value. + source.setShouldBlockVisibilityChanges(false) + } else { + // We made the source invisible earlier, so let's make it visible again. + source.visibility = View.VISIBLE + } + } + + override fun createTransitionController(): TransitionAnimator.Controller { + val delegate = GhostedViewTransitionAnimatorController(source) + return object : TransitionAnimator.Controller by delegate { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another + // ghost (that ghosts only the source content, and not its background) will + // be added right after this by the delegate and will be animated. + GhostView.removeGhost(source) + delegate.onTransitionAnimationStart(isExpandingFullyAbove) + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) + + // At this point the view visibility is restored by the delegate, so we delay the + // visibility changes again and make it invisible while the dialog is shown. + if (source is LaunchableView) { + source.setShouldBlockVisibilityChanges(true) + source.setTransitionVisibility(View.INVISIBLE) + } else { + source.visibility = View.INVISIBLE + } + } + } + } + + override fun createExitController(): TransitionAnimator.Controller { + return GhostedViewTransitionAnimatorController(source) + } + + override fun shouldAnimateExit(): Boolean { + // The source should be invisible by now, if it's not then something else changed + // its visibility and we probably don't want to run the animation. + if (source.visibility != View.INVISIBLE) { + return false + } + + return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) + } + + override fun onExitAnimationCancelled() { + if (source is LaunchableView) { + // Make sure we allow the source to change its visibility again. + source.setShouldBlockVisibilityChanges(false) + } else { + // If the view is invisible it's probably because of us, so we make it visible + // again. + if (source.visibility == View.INVISIBLE) { + source.visibility = View.VISIBLE + } + } + } + + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { + val type = cuj?.cujType ?: return null + return InteractionJankMonitor.Configuration.Builder.withView(type, source) + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/systemUIAnim/src/com/android/systemui/animation/ViewHierarchyAnimator.kt new file mode 100644 index 0000000000..00d9056529 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/ViewHierarchyAnimator.kt @@ -0,0 +1,1160 @@ +/* + * 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.systemui.animation + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.animation.ValueAnimator +import android.util.IntProperty +import android.view.View +import android.view.ViewGroup +import android.view.animation.Interpolator +import com.android.app.animation.Interpolators +import kotlin.math.max +import kotlin.math.min + +/** + * A class that allows changes in bounds within a view hierarchy to animate seamlessly between the + * start and end state. + */ +class ViewHierarchyAnimator { + companion object { + /** Default values for the animation. These can all be overridden at call time. */ + private const val DEFAULT_DURATION = 500L + private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD + private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE + private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE + private val DEFAULT_FADE_IN_INTERPOLATOR = Interpolators.ALPHA_IN + + /** The properties used to animate the view bounds. */ + private val PROPERTIES = + mapOf( + Bound.LEFT to createViewProperty(Bound.LEFT), + Bound.TOP to createViewProperty(Bound.TOP), + Bound.RIGHT to createViewProperty(Bound.RIGHT), + Bound.BOTTOM to createViewProperty(Bound.BOTTOM) + ) + + private fun createViewProperty(bound: Bound): IntProperty { + return object : IntProperty(bound.label) { + override fun setValue(view: View, value: Int) { + setBound(view, bound, value) + } + + override fun get(view: View): Int { + return getBound(view, bound) ?: bound.getValue(view) + } + } + } + + /** + * Instruct the animator to watch for changes to the layout of [rootView] and its children + * and animate them. It uses the given [interpolator] and [duration]. + * + * If a new layout change happens while an animation is already in progress, the animation + * is updated to continue from the current values to the new end state. + * + * By default, child views whole layout changes are animated as well. However, this can be + * controlled by [animateChildren]. If children are included, a set of [excludedViews] can + * be passed. If any dependent view from [rootView] matches an entry in this set, changes to + * that view will not be animated. + * + * The animator continues to respond to layout changes until [stopAnimating] is called. + * + * Successive calls to this method override the previous settings ([interpolator] and + * [duration]). The changes take effect on the next animation. + * + * Returns true if the [rootView] is already visible and will be animated, false otherwise. + * To animate the addition of a view, see [animateAddition]. + */ + @JvmOverloads + fun animate( + rootView: View, + interpolator: Interpolator = DEFAULT_INTERPOLATOR, + duration: Long = DEFAULT_DURATION, + animateChildren: Boolean = true, + excludedViews: Set = emptySet() + ): Boolean { + return animate( + rootView, + interpolator, + duration, + ephemeral = false, + animateChildren = animateChildren, + excludedViews = excludedViews + ) + } + + /** + * Like [animate], but only takes effect on the next layout update, then unregisters itself + * once the first animation is complete. + */ + @JvmOverloads + fun animateNextUpdate( + rootView: View, + interpolator: Interpolator = DEFAULT_INTERPOLATOR, + duration: Long = DEFAULT_DURATION, + animateChildren: Boolean = true, + excludedViews: Set = emptySet() + ): Boolean { + return animate( + rootView, + interpolator, + duration, + ephemeral = true, + animateChildren = animateChildren, + excludedViews = excludedViews + ) + } + + private fun animate( + rootView: View, + interpolator: Interpolator, + duration: Long, + ephemeral: Boolean, + animateChildren: Boolean, + excludedViews: Set = emptySet() + ): Boolean { + if ( + !occupiesSpace( + rootView.visibility, + rootView.left, + rootView.top, + rootView.right, + rootView.bottom + ) + ) { + return false + } + + val listener = createUpdateListener(interpolator, duration, ephemeral) + addListener( + rootView, + listener, + recursive = true, + animateChildren = animateChildren, + excludedViews = excludedViews + ) + return true + } + + /** + * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation + * using [interpolator] and [duration]. + * + * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise + * it keeps listening for further updates. + */ + private fun createUpdateListener( + interpolator: Interpolator, + duration: Long, + ephemeral: Boolean + ): View.OnLayoutChangeListener { + return createListener(interpolator, duration, ephemeral) + } + + /** + * Instruct the animator to stop watching for changes to the layout of [rootView] and its + * children. + * + * Any animations already in progress continue until their natural conclusion. + */ + fun stopAnimating(rootView: View) { + recursivelyRemoveListener(rootView) + } + + /** + * Instruct the animator to watch for changes to the layout of [rootView] and its children, + * and animate the next time the hierarchy appears after not being visible. It uses the + * given [interpolator] and [duration]. + * + * The start state of the animation is controlled by [origin]. This value can be any of the + * four corners, any of the four edges, or the center of the view. If any margins are added + * on the side(s) of the origin, the translation of those margins can be included by + * specifying [includeMargins]. + * + * Returns true if the [rootView] is invisible and will be animated, false otherwise. To + * animate an already visible view, see [animate] and [animateNextUpdate]. + * + * Then animator unregisters itself once the first addition animation is complete. + * + * @param includeFadeIn true if the animator should also fade in the view and child views. + * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if + * [includeFadeIn] is false. + * @param onAnimationEnd an optional runnable that will be run once the animation + * finishes successfully. Will not be run if the animation is cancelled. + */ + @JvmOverloads + fun animateAddition( + rootView: View, + origin: Hotspot = Hotspot.CENTER, + interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR, + duration: Long = DEFAULT_DURATION, + includeMargins: Boolean = false, + includeFadeIn: Boolean = false, + fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR, + onAnimationEnd: Runnable? = null, + ): Boolean { + if ( + occupiesSpace( + rootView.visibility, + rootView.left, + rootView.top, + rootView.right, + rootView.bottom + ) + ) { + return false + } + + val listener = + createAdditionListener( + origin, + interpolator, + duration, + ignorePreviousValues = !includeMargins, + onAnimationEnd, + ) + addListener(rootView, listener, recursive = true) + + if (!includeFadeIn) { + return true + } + + if (rootView is ViewGroup) { + // First, fade in the container view + val containerDuration = duration / 6 + createAndStartFadeInAnimator( + rootView, containerDuration, startDelay = 0, interpolator = fadeInInterpolator + ) + + // Then, fade in the child views + val childDuration = duration / 3 + for (i in 0 until rootView.childCount) { + val view = rootView.getChildAt(i) + createAndStartFadeInAnimator( + view, + childDuration, + // Wait until the container fades in before fading in the children + startDelay = containerDuration, + interpolator = fadeInInterpolator + ) + } + // For now, we don't recursively fade in additional sub views (e.g. grandchild + // views) since it hasn't been necessary, but we could add that functionality. + } else { + // Fade in the view during the first half of the addition + createAndStartFadeInAnimator( + rootView, + duration / 2, + startDelay = 0, + interpolator = fadeInInterpolator + ) + } + + return true + } + + /** + * Returns a new [View.OnLayoutChangeListener] that on the next call triggers a layout + * addition animation from the given [origin], using [interpolator] and [duration]. + * + * If [ignorePreviousValues] is true, the animation will only span the area covered by the + * new bounds. Otherwise it will include the margins between the previous and new bounds. + */ + private fun createAdditionListener( + origin: Hotspot, + interpolator: Interpolator, + duration: Long, + ignorePreviousValues: Boolean, + onAnimationEnd: Runnable? = null, + ): View.OnLayoutChangeListener { + return createListener( + interpolator, + duration, + ephemeral = true, + origin = origin, + ignorePreviousValues = ignorePreviousValues, + onAnimationEnd, + ) + } + + /** + * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation + * using [interpolator] and [duration]. + * + * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise + * it keeps listening for further updates. + * + * [origin] specifies whether the start values should be determined by a hotspot, and + * [ignorePreviousValues] controls whether the previous values should be taken into account. + */ + private fun createListener( + interpolator: Interpolator, + duration: Long, + ephemeral: Boolean, + origin: Hotspot? = null, + ignorePreviousValues: Boolean = false, + onAnimationEnd: Runnable? = null, + ): View.OnLayoutChangeListener { + return object : View.OnLayoutChangeListener { + override fun onLayoutChange( + view: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + previousLeft: Int, + previousTop: Int, + previousRight: Int, + previousBottom: Int + ) { + if (view == null) return + + val startLeft = getBound(view, Bound.LEFT) ?: previousLeft + val startTop = getBound(view, Bound.TOP) ?: previousTop + val startRight = getBound(view, Bound.RIGHT) ?: previousRight + val startBottom = getBound(view, Bound.BOTTOM) ?: previousBottom + + (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() + + if (!occupiesSpace(view.visibility, left, top, right, bottom)) { + setBound(view, Bound.LEFT, left) + setBound(view, Bound.TOP, top) + setBound(view, Bound.RIGHT, right) + setBound(view, Bound.BOTTOM, bottom) + return + } + + val startValues = + processStartValues( + origin, + left, + top, + right, + bottom, + startLeft, + startTop, + startRight, + startBottom, + ignorePreviousValues + ) + val endValues = + mapOf( + Bound.LEFT to left, + Bound.TOP to top, + Bound.RIGHT to right, + Bound.BOTTOM to bottom + ) + + val boundsToAnimate = mutableSetOf() + if (startValues.getValue(Bound.LEFT) != left) boundsToAnimate.add(Bound.LEFT) + if (startValues.getValue(Bound.TOP) != top) boundsToAnimate.add(Bound.TOP) + if (startValues.getValue(Bound.RIGHT) != right) boundsToAnimate.add(Bound.RIGHT) + if (startValues.getValue(Bound.BOTTOM) != bottom) { + boundsToAnimate.add(Bound.BOTTOM) + } + + if (boundsToAnimate.isNotEmpty()) { + startAnimation( + view, + boundsToAnimate, + startValues, + endValues, + interpolator, + duration, + ephemeral, + onAnimationEnd, + ) + } + } + } + } + + /** + * Animates the removal of [rootView] and its children from the hierarchy. It uses the given + * [interpolator] and [duration]. + * + * The end state of the animation is controlled by [destination]. This value can be any of + * the four corners, any of the four edges, or the center of the view. If any margins are + * added on the side(s) of the [destination], the translation of those margins can be + * included by specifying [includeMargins]. + * + * @param onAnimationEnd an optional runnable that will be run once the animation finishes + * successfully. Will not be run if the animation is cancelled. + */ + @JvmOverloads + fun animateRemoval( + rootView: View, + destination: Hotspot = Hotspot.CENTER, + interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR, + duration: Long = DEFAULT_DURATION, + includeMargins: Boolean = false, + onAnimationEnd: Runnable? = null, + ): Boolean { + if ( + !occupiesSpace( + rootView.visibility, + rootView.left, + rootView.top, + rootView.right, + rootView.bottom + ) + ) { + return false + } + + val parent = rootView.parent as ViewGroup + + // Ensure that rootView's siblings animate nicely around the removal. + val listener = createUpdateListener(interpolator, duration, ephemeral = true) + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + if (child == rootView) continue + addListener(child, listener, recursive = false) + } + + val viewHasSiblings = parent.childCount > 1 + if (viewHasSiblings) { + // Remove the view so that a layout update is triggered for the siblings and they + // animate to their next position while the view's removal is also animating. + parent.removeView(rootView) + // By adding the view to the overlay, we can animate it while it isn't part of the + // view hierarchy. It is correctly positioned because we have its previous bounds, + // and we set them manually during the animation. + parent.overlay.add(rootView) + } + // If this view has no siblings, the parent view may shrink to (0,0) size and mess + // up the animation if we immediately remove the view. So instead, we just leave the + // view in the real hierarchy until the animation finishes. + + val endRunnable = Runnable { + if (viewHasSiblings) { + parent.overlay.remove(rootView) + } else { + parent.removeView(rootView) + } + onAnimationEnd?.run() + } + + val startValues = + mapOf( + Bound.LEFT to rootView.left, + Bound.TOP to rootView.top, + Bound.RIGHT to rootView.right, + Bound.BOTTOM to rootView.bottom + ) + val endValues = + processEndValuesForRemoval( + destination, + rootView, + rootView.left, + rootView.top, + rootView.right, + rootView.bottom, + includeMargins, + ) + + val boundsToAnimate = mutableSetOf() + if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) + if (rootView.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) + if (rootView.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) + if (rootView.bottom != endValues.getValue(Bound.BOTTOM)) { + boundsToAnimate.add(Bound.BOTTOM) + } + + startAnimation( + rootView, + boundsToAnimate, + startValues, + endValues, + interpolator, + duration, + ephemeral = true, + endRunnable, + ) + + if (rootView is ViewGroup) { + // Shift the children so they maintain a consistent position within the shrinking + // view. + shiftChildrenForRemoval(rootView, destination, endValues, interpolator, duration) + + // Fade out the children during the first half of the removal, so they don't clutter + // too much once the view becomes very small. Then we fade out the view itself, in + // case it has its own content and/or background. + val startAlphas = FloatArray(rootView.childCount) + for (i in 0 until rootView.childCount) { + startAlphas[i] = rootView.getChildAt(i).alpha + } + + val animator = ValueAnimator.ofFloat(1f, 0f) + animator.interpolator = Interpolators.ALPHA_OUT + animator.duration = duration / 2 + animator.addUpdateListener { animation -> + for (i in 0 until rootView.childCount) { + rootView.getChildAt(i).alpha = + (animation.animatedValue as Float) * startAlphas[i] + } + } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rootView + .animate() + .alpha(0f) + .setInterpolator(Interpolators.ALPHA_OUT) + .setDuration(duration / 2) + .start() + } + } + ) + animator.start() + } else { + // Fade out the view during the second half of the removal. + rootView + .animate() + .alpha(0f) + .setInterpolator(Interpolators.ALPHA_OUT) + .setDuration(duration / 2) + .setStartDelay(duration / 2) + .start() + } + + return true + } + + /** + * Animates the children of [rootView] so that its layout remains internally consistent as + * it shrinks towards [destination] and changes its bounds to [endValues]. + * + * Uses [interpolator] and [duration], which should match those of the removal animation. + */ + private fun shiftChildrenForRemoval( + rootView: ViewGroup, + destination: Hotspot, + endValues: Map, + interpolator: Interpolator, + duration: Long + ) { + for (i in 0 until rootView.childCount) { + val child = rootView.getChildAt(i) + val childStartValues = + mapOf( + Bound.LEFT to child.left, + Bound.TOP to child.top, + Bound.RIGHT to child.right, + Bound.BOTTOM to child.bottom + ) + val childEndValues = + processChildEndValuesForRemoval( + destination, + child.left, + child.top, + child.right, + child.bottom, + endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT), + endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP) + ) + + val boundsToAnimate = mutableSetOf() + if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) + if (child.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) + if (child.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) + if (child.bottom != endValues.getValue(Bound.BOTTOM)) { + boundsToAnimate.add(Bound.BOTTOM) + } + + startAnimation( + child, + boundsToAnimate, + childStartValues, + childEndValues, + interpolator, + duration, + ephemeral = true + ) + } + } + + /** + * Returns whether the given [visibility] and bounds are consistent with a view being a + * contributing part of the hierarchy. + */ + private fun occupiesSpace( + visibility: Int, + left: Int, + top: Int, + right: Int, + bottom: Int + ): Boolean { + return visibility != View.GONE && left != right && top != bottom + } + + /** + * Computes the actual starting values based on the requested [origin] and on + * [ignorePreviousValues]. + * + * If [origin] is null, the resolved start values will be the same as those passed in, or + * the same as the new values if [ignorePreviousValues] is true. If [origin] is not null, + * the start values are resolved based on it, and [ignorePreviousValues] controls whether or + * not newly introduced margins are included. + * + * Base case + * ``` + * 1) origin=TOP + * x---------x x---------x x---------x x---------x x---------x + * x---------x | | | | | | + * -> -> x---------x -> | | -> | | + * x---------x | | + * x---------x + * 2) origin=BOTTOM_LEFT + * x---------x + * x-------x | | + * -> -> x----x -> | | -> | | + * x--x | | | | | | + * x x--x x----x x-------x x---------x + * 3) origin=CENTER + * x---------x + * x-----x x-------x | | + * x -> x---x -> | | -> | | -> | | + * x-----x x-------x | | + * x---------x + * ``` + * In case the start and end values differ in the direction of the origin, and + * [ignorePreviousValues] is false, the previous values are used and a translation is + * included in addition to the view expansion. + * ``` + * origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70) + * x + * x--x + * x--x x----x + * -> -> | | -> x------x + * x----x | | + * | | + * x------x + * ``` + */ + private fun processStartValues( + origin: Hotspot?, + newLeft: Int, + newTop: Int, + newRight: Int, + newBottom: Int, + previousLeft: Int, + previousTop: Int, + previousRight: Int, + previousBottom: Int, + ignorePreviousValues: Boolean + ): Map { + val startLeft = if (ignorePreviousValues) newLeft else previousLeft + val startTop = if (ignorePreviousValues) newTop else previousTop + val startRight = if (ignorePreviousValues) newRight else previousRight + val startBottom = if (ignorePreviousValues) newBottom else previousBottom + + var left = startLeft + var top = startTop + var right = startRight + var bottom = startBottom + + if (origin != null) { + left = + when (origin) { + Hotspot.CENTER -> (newLeft + newRight) / 2 + Hotspot.BOTTOM_LEFT, + Hotspot.LEFT, + Hotspot.TOP_LEFT -> min(startLeft, newLeft) + Hotspot.TOP, + Hotspot.BOTTOM -> newLeft + Hotspot.TOP_RIGHT, + Hotspot.RIGHT, + Hotspot.BOTTOM_RIGHT -> max(startRight, newRight) + } + top = + when (origin) { + Hotspot.CENTER -> (newTop + newBottom) / 2 + Hotspot.TOP_LEFT, + Hotspot.TOP, + Hotspot.TOP_RIGHT -> min(startTop, newTop) + Hotspot.LEFT, + Hotspot.RIGHT -> newTop + Hotspot.BOTTOM_RIGHT, + Hotspot.BOTTOM, + Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom) + } + right = + when (origin) { + Hotspot.CENTER -> (newLeft + newRight) / 2 + Hotspot.TOP_RIGHT, + Hotspot.RIGHT, + Hotspot.BOTTOM_RIGHT -> max(startRight, newRight) + Hotspot.TOP, + Hotspot.BOTTOM -> newRight + Hotspot.BOTTOM_LEFT, + Hotspot.LEFT, + Hotspot.TOP_LEFT -> min(startLeft, newLeft) + } + bottom = + when (origin) { + Hotspot.CENTER -> (newTop + newBottom) / 2 + Hotspot.BOTTOM_RIGHT, + Hotspot.BOTTOM, + Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom) + Hotspot.LEFT, + Hotspot.RIGHT -> newBottom + Hotspot.TOP_LEFT, + Hotspot.TOP, + Hotspot.TOP_RIGHT -> min(startTop, newTop) + } + } + + return mapOf( + Bound.LEFT to left, + Bound.TOP to top, + Bound.RIGHT to right, + Bound.BOTTOM to bottom + ) + } + + /** + * Computes a removal animation's end values based on the requested [destination] and the + * view's starting bounds. + * + * Examples: + * ``` + * 1) destination=TOP + * x---------x x---------x x---------x x---------x x---------x + * | | | | | | x---------x + * | | -> | | -> x---------x -> -> + * | | x---------x + * x---------x + * 2) destination=BOTTOM_LEFT + * x---------x + * | | x-------x + * | | -> | | -> x----x -> -> + * | | | | | | x--x + * x---------x x-------x x----x x--x x + * 3) destination=CENTER + * x---------x + * | | x-------x x-----x + * | | -> | | -> | | -> x---x -> x + * | | x-------x x-----x + * x---------x + * 4) destination=TOP, includeMargins=true (and view has large top margin) + * x---------x + * x---------x + * x---------x x---------x + * x---------x | | + * x---------x | | x---------x + * | | | | + * | | -> x---------x -> -> -> + * | | + * x---------x + * ``` + */ + private fun processEndValuesForRemoval( + destination: Hotspot, + rootView: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + includeMargins: Boolean = false, + ): Map { + val marginAdjustment = + if (includeMargins && + (rootView.layoutParams is ViewGroup.MarginLayoutParams)) { + val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams + DimenHolder( + left = marginLp.leftMargin, + top = marginLp.topMargin, + right = marginLp.rightMargin, + bottom = marginLp.bottomMargin + ) + } else { + DimenHolder(0, 0, 0, 0) + } + + // These are the end values to use *if* this bound is part of the destination. + val endLeft = left - marginAdjustment.left + val endTop = top - marginAdjustment.top + val endRight = right + marginAdjustment.right + val endBottom = bottom + marginAdjustment.bottom + + // For the below calculations: We need to ensure that the destination bound and the + // bound *opposite* to the destination bound end at the same value, to ensure that the + // view has size 0 for that dimension. + // For example, + // - If destination=TOP, then endTop == endBottom. Left and right stay the same. + // - If destination=RIGHT, then endRight == endLeft. Top and bottom stay the same. + // - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight. + + return when (destination) { + Hotspot.TOP -> mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.LEFT to left, + Bound.RIGHT to right, + ) + Hotspot.TOP_RIGHT -> mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + ) + Hotspot.RIGHT -> mapOf( + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + Bound.TOP to top, + Bound.BOTTOM to bottom, + ) + Hotspot.BOTTOM_RIGHT -> mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + ) + Hotspot.BOTTOM -> mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.LEFT to left, + Bound.RIGHT to right, + ) + Hotspot.BOTTOM_LEFT -> mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + ) + Hotspot.LEFT -> mapOf( + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + Bound.TOP to top, + Bound.BOTTOM to bottom, + ) + Hotspot.TOP_LEFT -> mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + ) + Hotspot.CENTER -> mapOf( + Bound.LEFT to (endLeft + endRight) / 2, + Bound.RIGHT to (endLeft + endRight) / 2, + Bound.TOP to (endTop + endBottom) / 2, + Bound.BOTTOM to (endTop + endBottom) / 2, + ) + } + } + + /** + * Computes the end values for the child of a view being removed, based on the child's + * starting bounds, the removal's [destination], and the [parentWidth] and [parentHeight]. + * + * The end values always represent the child's position after it has been translated so that + * its center is at the [destination]. + * + * Examples: + * ``` + * 1) destination=TOP + * The child maintains its left and right positions, but is shifted up so that its + * center is on the parent's end top edge. + * 2) destination=BOTTOM_LEFT + * The child shifts so that its center is on the parent's end bottom left corner. + * 3) destination=CENTER + * The child shifts so that its own center is on the parent's end center. + * ``` + */ + private fun processChildEndValuesForRemoval( + destination: Hotspot, + left: Int, + top: Int, + right: Int, + bottom: Int, + parentWidth: Int, + parentHeight: Int + ): Map { + val halfWidth = (right - left) / 2 + val halfHeight = (bottom - top) / 2 + + val endLeft = + when (destination) { + Hotspot.CENTER -> (parentWidth / 2) - halfWidth + Hotspot.BOTTOM_LEFT, + Hotspot.LEFT, + Hotspot.TOP_LEFT -> -halfWidth + Hotspot.TOP_RIGHT, + Hotspot.RIGHT, + Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth + Hotspot.TOP, + Hotspot.BOTTOM -> left + } + val endTop = + when (destination) { + Hotspot.CENTER -> (parentHeight / 2) - halfHeight + Hotspot.TOP_LEFT, + Hotspot.TOP, + Hotspot.TOP_RIGHT -> -halfHeight + Hotspot.BOTTOM_RIGHT, + Hotspot.BOTTOM, + Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight + Hotspot.LEFT, + Hotspot.RIGHT -> top + } + val endRight = + when (destination) { + Hotspot.CENTER -> (parentWidth / 2) + halfWidth + Hotspot.TOP_RIGHT, + Hotspot.RIGHT, + Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth + Hotspot.BOTTOM_LEFT, + Hotspot.LEFT, + Hotspot.TOP_LEFT -> halfWidth + Hotspot.TOP, + Hotspot.BOTTOM -> right + } + val endBottom = + when (destination) { + Hotspot.CENTER -> (parentHeight / 2) + halfHeight + Hotspot.BOTTOM_RIGHT, + Hotspot.BOTTOM, + Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight + Hotspot.TOP_LEFT, + Hotspot.TOP, + Hotspot.TOP_RIGHT -> halfHeight + Hotspot.LEFT, + Hotspot.RIGHT -> bottom + } + + return mapOf( + Bound.LEFT to endLeft, + Bound.TOP to endTop, + Bound.RIGHT to endRight, + Bound.BOTTOM to endBottom + ) + } + + private fun addListener( + view: View, + listener: View.OnLayoutChangeListener, + recursive: Boolean = false, + animateChildren: Boolean = true, + excludedViews: Set = emptySet() + ) { + if (excludedViews.contains(view)) return + + // Make sure that only one listener is active at a time. + val previousListener = view.getTag(R.id.tag_layout_listener) + if (previousListener != null && previousListener is View.OnLayoutChangeListener) { + view.removeOnLayoutChangeListener(previousListener) + } + + view.addOnLayoutChangeListener(listener) + view.setTag(R.id.tag_layout_listener, listener) + if (animateChildren && view is ViewGroup && recursive) { + for (i in 0 until view.childCount) { + addListener( + view.getChildAt(i), + listener, + recursive = true, + animateChildren = animateChildren, + excludedViews = excludedViews + ) + } + } + } + + private fun recursivelyRemoveListener(view: View) { + val listener = view.getTag(R.id.tag_layout_listener) + if (listener != null && listener is View.OnLayoutChangeListener) { + view.setTag(R.id.tag_layout_listener, null /* tag */) + view.removeOnLayoutChangeListener(listener) + } + + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + recursivelyRemoveListener(view.getChildAt(i)) + } + } + } + + private fun getBound(view: View, bound: Bound): Int? { + return view.getTag(bound.overrideTag) as? Int + } + + private fun setBound(view: View, bound: Bound, value: Int) { + view.setTag(bound.overrideTag, value) + bound.setValue(view, value) + } + + /** + * Initiates the animation of the requested [bounds] between [startValues] and [endValues] + * by creating the animator, registering it with the [view], and starting it using + * [interpolator] and [duration]. + * + * If [ephemeral] is true, the layout change listener is unregistered at the end of the + * animation, so no more animations happen. + */ + private fun startAnimation( + view: View, + bounds: Set, + startValues: Map, + endValues: Map, + interpolator: Interpolator, + duration: Long, + ephemeral: Boolean, + onAnimationEnd: Runnable? = null, + ) { + val propertyValuesHolders = + buildList { + bounds.forEach { bound -> + add( + PropertyValuesHolder.ofInt( + PROPERTIES[bound], + startValues.getValue(bound), + endValues.getValue(bound) + ) + ) + } + } + .toTypedArray() + + (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() + + val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders) + animator.interpolator = interpolator + animator.duration = duration + animator.addListener( + object : AnimatorListenerAdapter() { + var cancelled = false + + override fun onAnimationEnd(animation: Animator) { + view.setTag(R.id.tag_animator, null /* tag */) + bounds.forEach { view.setTag(it.overrideTag, null /* tag */) } + + // When an animation is cancelled, a new one might be taking over. We + // shouldn't unregister the listener yet. + if (ephemeral && !cancelled) { + // The duration is the same for the whole hierarchy, so it's safe to + // remove the listener recursively. We do this because some descendant + // views might not change bounds, and therefore not animate and leak the + // listener. + recursivelyRemoveListener(view) + } + if (!cancelled) { + onAnimationEnd?.run() + } + } + + override fun onAnimationCancel(animation: Animator) { + cancelled = true + } + } + ) + + bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) } + + view.setTag(R.id.tag_animator, animator) + animator.start() + } + + private fun createAndStartFadeInAnimator( + view: View, + duration: Long, + startDelay: Long, + interpolator: Interpolator + ) { + val animator = ObjectAnimator.ofFloat(view, "alpha", 1f) + animator.startDelay = startDelay + animator.duration = duration + animator.interpolator = interpolator + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + view.setTag(R.id.tag_alpha_animator, null /* tag */) + } + }) + + (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel() + view.setTag(R.id.tag_alpha_animator, animator) + animator.start() + } + } + + /** An enum used to determine the origin of addition animations. */ + enum class Hotspot { + CENTER, + LEFT, + TOP_LEFT, + TOP, + TOP_RIGHT, + RIGHT, + BOTTOM_RIGHT, + BOTTOM, + BOTTOM_LEFT + } + + private enum class Bound(val label: String, val overrideTag: Int) { + LEFT("left", R.id.tag_override_left) { + override fun setValue(view: View, value: Int) { + view.left = value + } + + override fun getValue(view: View): Int { + return view.left + } + }, + TOP("top", R.id.tag_override_top) { + override fun setValue(view: View, value: Int) { + view.top = value + } + + override fun getValue(view: View): Int { + return view.top + } + }, + RIGHT("right", R.id.tag_override_right) { + override fun setValue(view: View, value: Int) { + view.right = value + } + + override fun getValue(view: View): Int { + return view.right + } + }, + BOTTOM("bottom", R.id.tag_override_bottom) { + override fun setValue(view: View, value: Int) { + view.bottom = value + } + + override fun getValue(view: View): Int { + return view.bottom + } + }; + + abstract fun setValue(view: View, value: Int) + abstract fun getValue(view: View): Int + } + + /** Simple data class to hold a set of dimens for left, top, right, bottom. */ + private data class DimenHolder( + val left: Int, + val top: Int, + val right: Int, + val bottom: Int, + ) +} diff --git a/systemUIAnim/src/com/android/systemui/animation/ViewRootSync.kt b/systemUIAnim/src/com/android/systemui/animation/ViewRootSync.kt new file mode 100644 index 0000000000..e4f6db57f6 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/ViewRootSync.kt @@ -0,0 +1,43 @@ +package com.android.systemui.animation + +import android.view.View +import android.window.SurfaceSyncGroup + +/** A util class to synchronize 2 view roots. */ +// TODO(b/200284684): Remove this class. +object ViewRootSync { + + /** + * Synchronize the next draw between the view roots of [view] and [otherView], then run [then]. + * + * Note that in some cases, the synchronization might not be possible (e.g. WM consumed the next + * transactions) or disabled (temporarily, on low ram devices). In this case, [then] will be + * called without synchronizing. + */ + fun synchronizeNextDraw(view: View, otherView: View, then: () -> Unit) { + if ( + !view.isAttachedToWindow || + view.viewRootImpl == null || + !otherView.isAttachedToWindow || + otherView.viewRootImpl == null || + view.viewRootImpl == otherView.viewRootImpl + ) { + // No need to synchronize if either the touch surface or dialog view is not attached + // to a window. + then() + return + } + + val syncGroup = SurfaceSyncGroup("SysUIAnimation") + syncGroup.addSyncCompleteCallback(view.context.mainExecutor) { then() } + syncGroup.add(view.rootSurfaceControl, null /* runnable */) + syncGroup.add(otherView.rootSurfaceControl, null /* runnable */) + syncGroup.markSyncReady() + } + + /** A Java-friendly API for [synchronizeNextDraw]. */ + @JvmStatic + fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) { + synchronizeNextDraw(view, otherView, then::run) + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpec.kt b/systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpec.kt new file mode 100644 index 0000000000..6c982a0450 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpec.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.systemui.animation.back + +import android.util.DisplayMetrics +import android.view.animation.Interpolator +import android.window.BackEvent +import com.android.app.animation.Interpolators +import com.android.systemui.util.dpToPx + +/** Used to convert [BackEvent] into a [BackTransformation]. */ +fun interface BackAnimationSpec { + + /** Computes transformation based on a [backEvent] and sets it to [result]. */ + fun getBackTransformation( + backEvent: BackEvent, + progressY: Float, // TODO(b/265060720): Remove progressY. Could be retrieved from backEvent + result: BackTransformation, + ) + + companion object +} + +/** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */ +fun BackAnimationSpec.Companion.createFloatingSurfaceAnimationSpec( + displayMetricsProvider: () -> DisplayMetrics, + maxMarginXdp: Float, + maxMarginYdp: Float, + minScale: Float, + translateXEasing: Interpolator = Interpolators.BACK_GESTURE, + translateYEasing: Interpolator = Interpolators.LINEAR, + scaleEasing: Interpolator = Interpolators.BACK_GESTURE, +): BackAnimationSpec { + return BackAnimationSpec { backEvent, progressY, result -> + val displayMetrics = displayMetricsProvider() + val screenWidthPx = displayMetrics.widthPixels + val screenHeightPx = displayMetrics.heightPixels + + val maxMarginXPx = maxMarginXdp.dpToPx(displayMetrics) + val maxMarginYPx = maxMarginYdp.dpToPx(displayMetrics) + val maxTranslationXByScale = (screenWidthPx - screenWidthPx * minScale) / 2 + val maxTranslationX = maxTranslationXByScale - maxMarginXPx + val maxTranslationYByScale = (screenHeightPx - screenHeightPx * minScale) / 2 + val maxTranslationY = maxTranslationYByScale - maxMarginYPx + val minScaleReversed = 1f - minScale + + val direction = if (backEvent.swipeEdge == BackEvent.EDGE_LEFT) 1 else -1 + val progressX = backEvent.progress + + val ratioTranslateX = translateXEasing.getInterpolation(progressX) + val ratioTranslateY = translateYEasing.getInterpolation(progressY) + val ratioScale = scaleEasing.getInterpolation(progressX) + + result.apply { + translateX = ratioTranslateX * direction * maxTranslationX + translateY = ratioTranslateY * maxTranslationY + scale = 1f - (ratioScale * minScaleReversed) + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt new file mode 100644 index 0000000000..536f2972ab --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.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.systemui.animation.back + +import android.util.DisplayMetrics + +/** + * SysUI transitions - Dismiss app (ST1) Return to launching surface or place of origin + * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-1-dismiss-app + */ +fun BackAnimationSpec.Companion.dismissAppForSysUi( + displayMetricsProvider: () -> DisplayMetrics, +): BackAnimationSpec = + BackAnimationSpec.createFloatingSurfaceAnimationSpec( + displayMetricsProvider = displayMetricsProvider, + maxMarginXdp = 8f, + maxMarginYdp = 8f, + minScale = 0.8f, + ) + +/** + * SysUI transitions - Cross task (ST2) Return to previous task/app, keeping the current one open + * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-2-cross-task + */ +fun BackAnimationSpec.Companion.crossTaskForSysUi( + displayMetricsProvider: () -> DisplayMetrics, +): BackAnimationSpec = + BackAnimationSpec.createFloatingSurfaceAnimationSpec( + displayMetricsProvider = displayMetricsProvider, + maxMarginXdp = 8f, + maxMarginYdp = 8f, + minScale = 0.8f, + ) + +/** + * SysUI transitions - Inner area dismiss (ST3) Dismiss non-detachable surface + * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-3-inner-area-dismiss + */ +fun BackAnimationSpec.Companion.innerAreaDismissForSysUi( + displayMetricsProvider: () -> DisplayMetrics, +): BackAnimationSpec = + BackAnimationSpec.createFloatingSurfaceAnimationSpec( + displayMetricsProvider = displayMetricsProvider, + maxMarginXdp = 0f, + maxMarginYdp = 0f, + minScale = 0.9f, + ) + +/** + * SysUI transitions - Floating system surfaces (ST4) + * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-4-floating-system-surfaces + */ +fun BackAnimationSpec.Companion.floatingSystemSurfacesForSysUi( + displayMetricsProvider: () -> DisplayMetrics, +): BackAnimationSpec = + BackAnimationSpec.createFloatingSurfaceAnimationSpec( + displayMetricsProvider = displayMetricsProvider, + maxMarginXdp = 8f, + maxMarginYdp = 8f, + minScale = 0.9f, + ) + +/** + * SysUI transitions - Bottomsheet (AT3) + * https://carbon.googleplex.com/predictive-back-for-apps/pages/at-3-bottom-sheets + */ +fun BackAnimationSpec.Companion.bottomSheetForSysUi( + displayMetricsProvider: () -> DisplayMetrics, +): BackAnimationSpec = BackAnimationSpec.createBottomsheetAnimationSpec(displayMetricsProvider) diff --git a/systemUIAnim/src/com/android/systemui/animation/back/BackTransformation.kt b/systemUIAnim/src/com/android/systemui/animation/back/BackTransformation.kt new file mode 100644 index 0000000000..029f62c6e4 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/back/BackTransformation.kt @@ -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.systemui.animation.back + +import android.view.View + +/** + * This object that represents the transformation to apply to the target. The properties of this + * object are mutable for performance reasons (avoid recreating this object) + */ +data class BackTransformation( + var translateX: Float = Float.NaN, + var translateY: Float = Float.NaN, + var scale: Float = Float.NaN, + var scalePivotPosition: ScalePivotPosition? = null, +) + +/** Enum that describes the location of the scale pivot position */ +enum class ScalePivotPosition { + // more options may be added in the future + CENTER, + BOTTOM_CENTER; + + fun applyTo(view: View) { + val pivotX = + when (this) { + CENTER -> view.width / 2f + BOTTOM_CENTER -> view.width / 2f + } + val pivotY = + when (this) { + CENTER -> view.height / 2f + BOTTOM_CENTER -> view.height.toFloat() + } + view.pivotX = pivotX + view.pivotY = pivotY + } +} + +/** Apply the transformation to the [targetView] */ +fun BackTransformation.applyTo(targetView: View) { + if (translateX.isFinite()) targetView.translationX = translateX + if (translateY.isFinite()) targetView.translationY = translateY + scalePivotPosition?.applyTo(targetView) + if (scale.isFinite()) { + targetView.scaleX = scale + targetView.scaleY = scale + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt b/systemUIAnim/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt new file mode 100644 index 0000000000..b1945a1c37 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation.back + +import android.util.DisplayMetrics +import android.view.animation.Interpolator +import com.android.app.animation.Interpolators +import com.android.systemui.util.dpToPx + +private const val MAX_SCALE_DELTA_DP = 48 + +/** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */ +fun BackAnimationSpec.Companion.createBottomsheetAnimationSpec( + displayMetricsProvider: () -> DisplayMetrics, + scaleEasing: Interpolator = Interpolators.BACK_GESTURE, +): BackAnimationSpec { + return BackAnimationSpec { backEvent, _, result -> + val displayMetrics = displayMetricsProvider() + val screenWidthPx = displayMetrics.widthPixels + val minScale = 1 - MAX_SCALE_DELTA_DP.dpToPx(displayMetrics) / screenWidthPx + val progressX = backEvent.progress + val ratioScale = scaleEasing.getInterpolation(progressX) + result.apply { + scale = 1f - ratioScale * (1f - minScale) + scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt b/systemUIAnim/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt new file mode 100644 index 0000000000..8740d14672 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt @@ -0,0 +1,103 @@ +/* + * 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.systemui.animation.back + +import android.annotation.IntRange +import android.util.DisplayMetrics +import android.view.View +import android.window.BackEvent +import android.window.OnBackAnimationCallback +import android.window.OnBackInvokedDispatcher +import android.window.OnBackInvokedDispatcher.Priority + +/** + * Generates an [OnBackAnimationCallback] given a [backAnimationSpec]. [onBackProgressed] will be + * called on each update passing the current [BackTransformation]. + * + * Optionally, you can specify [onBackStarted], [onBackInvoked], and [onBackCancelled] callbacks. + * + * @sample com.android.systemui.util.registerAnimationOnBackInvoked + */ +fun onBackAnimationCallbackFrom( + backAnimationSpec: BackAnimationSpec, + displayMetrics: DisplayMetrics, // TODO(b/265060720): We could remove this + onBackProgressed: (BackTransformation) -> Unit, + onBackStarted: (BackEvent) -> Unit = {}, + onBackInvoked: () -> Unit = {}, + onBackCancelled: () -> Unit = {}, +): OnBackAnimationCallback { + return object : OnBackAnimationCallback { + private var initialY = 0f + private val lastTransformation = BackTransformation() + + override fun onBackStarted(backEvent: BackEvent) { + initialY = backEvent.touchY + onBackStarted(backEvent) + } + + override fun onBackProgressed(backEvent: BackEvent) { + val progressY = (backEvent.touchY - initialY) / displayMetrics.heightPixels + + backAnimationSpec.getBackTransformation( + backEvent = backEvent, + progressY = progressY, + result = lastTransformation, + ) + + onBackProgressed(lastTransformation) + } + + override fun onBackInvoked() { + onBackInvoked() + } + + override fun onBackCancelled() { + onBackCancelled() + } + } +} + +/** + * Register [OnBackAnimationCallback] when View is attached and unregister it when View is detached + * + * @sample com.android.systemui.util.registerAnimationOnBackInvoked + */ +fun View.registerOnBackInvokedCallbackOnViewAttached( + onBackInvokedDispatcher: OnBackInvokedDispatcher, + onBackAnimationCallback: OnBackAnimationCallback, + @Priority @IntRange(from = 0) priority: Int = OnBackInvokedDispatcher.PRIORITY_DEFAULT, +) { + addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + onBackInvokedDispatcher.registerOnBackInvokedCallback( + priority, + onBackAnimationCallback + ) + } + + override fun onViewDetachedFromWindow(v: View) { + removeOnAttachStateChangeListener(this) + onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackAnimationCallback) + } + } + ) + + if (isAttachedToWindow) { + onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackAnimationCallback) + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt new file mode 100644 index 0000000000..7538f188fb --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt @@ -0,0 +1,55 @@ +/* + * 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.systemui.animation.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate + +/** A [FrameLayout] that also implements [LaunchableView]. */ +open class LaunchableFrameLayout : FrameLayout, LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + ) + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/view/LaunchableImageView.kt b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableImageView.kt new file mode 100644 index 0000000000..e42b589f05 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableImageView.kt @@ -0,0 +1,56 @@ +/* + * 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.systemui.animation.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate + +/** An [ImageView] that also implements [LaunchableView]. */ +open class LaunchableImageView : ImageView, LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + ) + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt new file mode 100644 index 0000000000..bce262291f --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt @@ -0,0 +1,55 @@ +/* + * 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.systemui.animation.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate + +/** A [LinearLayout] that also implements [LaunchableView]. */ +open class LaunchableLinearLayout : LinearLayout, LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + ) + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } +} diff --git a/systemUIAnim/src/com/android/systemui/animation/view/LaunchableTextView.kt b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableTextView.kt new file mode 100644 index 0000000000..147669528c --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/animation/view/LaunchableTextView.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.systemui.animation.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate + +/** A [TextView] that also implements [LaunchableView]. */ +open class LaunchableTextView : TextView, LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + ) + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/PaintDrawCallback.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/PaintDrawCallback.kt new file mode 100644 index 0000000000..d50979ccd0 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/PaintDrawCallback.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects + +import android.graphics.Paint +import android.graphics.RenderEffect + +/** + * A callback with a [Paint] object that contains shader info, which is triggered every frame while + * animation is playing. Note that the [Paint] object here is always the same instance. + * + * This approach is more performant than other ones because [RenderEffect] forces an intermediate + * render pass of the View to a texture to feed into it. + * + * The usage of this callback is as follows: + *
{@code
+ *     private var paint: Paint? = null
+ *     // Override [View.onDraw].
+ *     override fun onDraw(canvas: Canvas) {
+ *         // RuntimeShader requires hardwareAcceleration.
+ *         if (!canvas.isHardwareAccelerated) return
+ *
+ *         paint?.let { canvas.drawPaint(it) }
+ *     }
+ *
+ *     // Given that this is called [PaintDrawCallback.onDraw]
+ *     fun draw(paint: Paint) {
+ *         this.paint = paint
+ *
+ *         // Must call invalidate to trigger View#onDraw
+ *         invalidate()
+ *     }
+ * }
+ * + * Please refer to [RenderEffectDrawCallback] for alternative approach. + */ +interface PaintDrawCallback { + fun onDraw(paint: Paint) +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/RenderEffectDrawCallback.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/RenderEffectDrawCallback.kt new file mode 100644 index 0000000000..db7ee58090 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/RenderEffectDrawCallback.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects + +import android.graphics.RenderEffect + +/** + * A callback with a [RenderEffect] object that contains shader info, which is triggered every frame + * while animation is playing. Note that the [RenderEffect] instance is different each time to + * update shader uniforms. + * + * The usage of this callback is as follows: + *
{@code
+ *     private val xEffectDrawingCallback = RenderEffectDrawCallback() {
+ *         val myOtherRenderEffect = createOtherRenderEffect()
+ *         val chainEffect = RenderEffect.createChainEffect(renderEffect, myOtherRenderEffect)
+ *         myView.setRenderEffect(chainEffect)
+ *     }
+ *
+ *     private val xEffect = XEffect(config, xEffectDrawingCallback)
+ * }
+ */ +interface RenderEffectDrawCallback { + fun onDraw(renderEffect: RenderEffect) +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxConfig.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxConfig.kt new file mode 100644 index 0000000000..72f0e86f9d --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxConfig.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects.glowboxeffect + +/** Parameters used to play [GlowBoxEffect]. */ +data class GlowBoxConfig( + /** Start center position X in px. */ + val startCenterX: Float, + /** Start center position Y in px. */ + val startCenterY: Float, + /** End center position X in px. */ + val endCenterX: Float, + /** End center position Y in px. */ + val endCenterY: Float, + /** Width of the box in px. */ + val width: Float, + /** Height of the box in px. */ + val height: Float, + /** Color of the box in ARGB, Apply alpha value if needed. */ + val color: Int, + /** Amount of blur (or glow) of the box. */ + val blurAmount: Float, + /** + * Duration of the animation. Note that the full duration of the animation is + * [duration] + [easeInDuration] + [easeOutDuration]. + */ + val duration: Long, + /** Ease in duration of the animation. */ + val easeInDuration: Long, + /** Ease out duration of the animation. */ + val easeOutDuration: Long, +) diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffect.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffect.kt new file mode 100644 index 0000000000..5e590c1ca0 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffect.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects.glowboxeffect + +import android.animation.ValueAnimator +import android.graphics.Paint +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnEnd +import com.android.systemui.surfaceeffects.PaintDrawCallback +import com.android.systemui.surfaceeffects.utils.MathUtils.lerp + +/** Glow box effect where the box moves from start to end positions defined in the [config]. */ +class GlowBoxEffect( + private var config: GlowBoxConfig, + private val paintDrawCallback: PaintDrawCallback, + private val stateChangedCallback: AnimationStateChangedCallback? = null +) { + private val glowBoxShader = + GlowBoxShader().apply { + setSize(config.width, config.height) + setCenter(config.startCenterX, config.startCenterY) + setBlur(config.blurAmount) + setColor(config.color) + } + private var animator: ValueAnimator? = null + @VisibleForTesting var state: AnimationState = AnimationState.NOT_PLAYING + private val paint = Paint().apply { shader = glowBoxShader } + + fun updateConfig(newConfig: GlowBoxConfig) { + this.config = newConfig + + with(glowBoxShader) { + setSize(config.width, config.height) + setCenter(config.startCenterX, config.startCenterY) + setBlur(config.blurAmount) + setColor(config.color) + } + } + + fun play() { + if (state != AnimationState.NOT_PLAYING) { + return + } + + playEaseIn() + } + + /** Finishes the animation with ease out. */ + fun finish(force: Boolean = false) { + // If it's playing ease out, cancel immediately. + if (force && state == AnimationState.EASE_OUT) { + animator?.cancel() + return + } + + // If it's playing either ease in or main, fast-forward to ease out. + if (state == AnimationState.EASE_IN || state == AnimationState.MAIN) { + animator?.pause() + playEaseOut() + } + + // At this point, animation state should be ease out. Cancel it if force is true. + if (force) { + animator?.cancel() + } + } + + private fun playEaseIn() { + if (state == AnimationState.EASE_IN) { + return + } + state = AnimationState.EASE_IN + stateChangedCallback?.onStart() + + animator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = config.easeInDuration + addUpdateListener { + val progress = it.animatedValue as Float + glowBoxShader.setCenter( + lerp(config.startCenterX, config.endCenterX, progress), + lerp(config.startCenterY, config.endCenterY, progress) + ) + + draw() + } + + doOnEnd { + animator = null + playMain() + } + + start() + } + } + + private fun playMain() { + if (state == AnimationState.MAIN) { + return + } + state = AnimationState.MAIN + + animator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = config.duration + addUpdateListener { draw() } + + doOnEnd { + animator = null + playEaseOut() + } + + start() + } + } + + private fun playEaseOut() { + if (state == AnimationState.EASE_OUT) return + state = AnimationState.EASE_OUT + + animator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = config.easeOutDuration + addUpdateListener { + val progress = it.animatedValue as Float + glowBoxShader.setCenter( + lerp(config.endCenterX, config.startCenterX, progress), + lerp(config.endCenterY, config.startCenterY, progress) + ) + + draw() + } + + doOnEnd { + animator = null + state = AnimationState.NOT_PLAYING + stateChangedCallback?.onEnd() + } + + start() + } + } + + private fun draw() { + paintDrawCallback.onDraw(paint) + } + + /** + * The animation state of the effect. The animation state transitions as follows: [EASE_IN] -> + * [MAIN] -> [EASE_OUT] -> [NOT_PLAYING]. + */ + enum class AnimationState { + EASE_IN, + MAIN, + EASE_OUT, + NOT_PLAYING, + } + + interface AnimationStateChangedCallback { + /** + * Triggered when the animation starts, specifically when the states goes from + * [AnimationState.NOT_PLAYING] to [AnimationState.EASE_IN]. + */ + fun onStart() + /** + * Triggered when the animation ends, specifically when the states goes from + * [AnimationState.EASE_OUT] to [AnimationState.NOT_PLAYING]. + */ + fun onEnd() + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxShader.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxShader.kt new file mode 100644 index 0000000000..36934086cc --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxShader.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects.glowboxeffect + +import android.graphics.RuntimeShader +import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary + +/** Soft box shader. */ +class GlowBoxShader : RuntimeShader(GLOW_SHADER) { + // language=AGSL + private companion object { + private const val SHADER = + """ + uniform half2 in_center; + uniform half2 in_size; + uniform half in_blur; + layout(color) uniform half4 in_color; + + float4 main(float2 fragcoord) { + half glow = soften(sdBox(fragcoord - in_center, in_size), in_blur); + return in_color * (1. - glow); + } + """ + + private const val GLOW_SHADER = + SdfShaderLibrary.BOX_SDF + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SHADER + } + + fun setCenter(x: Float, y: Float) { + setFloatUniform("in_center", x, y) + } + + fun setSize(width: Float, height: Float) { + setFloatUniform("in_size", width, height) + } + + fun setBlur(blurAmount: Float) { + setFloatUniform("in_blur", blurAmount) + } + + fun setColor(color: Int) { + setColorUniform("in_color", color) + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt new file mode 100644 index 0000000000..211b84f253 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects.loadingeffect + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.Paint +import android.graphics.RenderEffect +import android.view.View +import com.android.systemui.surfaceeffects.PaintDrawCallback +import com.android.systemui.surfaceeffects.RenderEffectDrawCallback +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader + +/** + * Plays loading effect with the given configuration. + * + * @param baseType immutable base shader type. This is used for constructing the shader. Reconstruct + * the [LoadingEffect] if the base type needs to be changed. + * @param config immutable parameters that are used for drawing the effect. + * @param paintCallback triggered every frame when animation is playing. Use this to draw the effect + * with [Canvas.drawPaint]. + * @param renderEffectCallback triggered every frame when animation is playing. Use this to draw the + * effect with [RenderEffect]. + * @param animationStateChangedCallback triggered when the [AnimationState] changes. Optional. + * + * The client is responsible to actually draw the [Paint] or [RenderEffect] returned in the + * callback. Note that [View.invalidate] must be called on each callback. There are a few ways to + * render the effect: + * 1) Use [Canvas.drawPaint]. (Preferred. Significantly cheaper!) + * 2) Set [RenderEffect] to the [View]. (Good for chaining effects.) + * 3) Use [RenderNode.setRenderEffect]. (This may be least preferred, as 2 should do what you want.) + * + *

First approach is more performant than other ones because [RenderEffect] forces an + * intermediate render pass of the View to a texture to feed into it. + * + *

If going with the first approach, your custom [View] would look like as follow: + *

{@code
+ *     private var paint: Paint? = null
+ *     // Override [View.onDraw].
+ *     override fun onDraw(canvas: Canvas) {
+ *         // RuntimeShader requires hardwareAcceleration.
+ *         if (!canvas.isHardwareAccelerated) return
+ *
+ *         paint?.let { canvas.drawPaint(it) }
+ *     }
+ *
+ *     // This is called [Callback.onDraw]
+ *     fun draw(paint: Paint) {
+ *         this.paint = paint
+ *
+ *         // Must call invalidate to trigger View#onDraw
+ *         invalidate()
+ *     }
+ * }
+ * + *

If going with the second approach, it doesn't require an extra custom [View], and it is as + * simple as calling [View.setRenderEffect] followed by [View.invalidate]. You can also chain the + * effect with other [RenderEffect]. + * + *

Third approach is an option, but it's more of a boilerplate so you would like to stick with + * the second option. If you want to go with this option for some reason, below is the example: + *

{@code
+ *     // Initialize the shader and paint to use to pass into the [Canvas].
+ *     private val renderNode = RenderNode("LoadingEffect")
+ *
+ *     // Override [View.onDraw].
+ *     override fun onDraw(canvas: Canvas) {
+ *         // RuntimeShader requires hardwareAcceleration.
+ *         if (!canvas.isHardwareAccelerated) return
+ *
+ *         if (renderNode.hasDisplayList()) {
+ *             canvas.drawRenderNode(renderNode)
+ *         }
+ *     }
+ *
+ *     // This is called [Callback.onDraw]
+ *     fun draw(renderEffect: RenderEffect) {
+ *         renderNode.setPosition(0, 0, width, height)
+ *         renderNode.setRenderEffect(renderEffect)
+ *
+ *         val recordingCanvas = renderNode.beginRecording()
+ *         // We need at least 1 drawing instruction.
+ *         recordingCanvas.drawColor(Color.TRANSPARENT)
+ *         renderNode.endRecording()
+ *
+ *         // Must call invalidate to trigger View#onDraw
+ *         invalidate()
+ *     }
+ * }
+ */ +class LoadingEffect +private constructor( + baseType: TurbulenceNoiseShader.Companion.Type, + private val config: TurbulenceNoiseAnimationConfig, + private val paintCallback: PaintDrawCallback?, + private val renderEffectCallback: RenderEffectDrawCallback?, + private val animationStateChangedCallback: AnimationStateChangedCallback? = null +) { + constructor( + baseType: TurbulenceNoiseShader.Companion.Type, + config: TurbulenceNoiseAnimationConfig, + paintCallback: PaintDrawCallback, + animationStateChangedCallback: AnimationStateChangedCallback? = null + ) : this( + baseType, + config, + paintCallback, + renderEffectCallback = null, + animationStateChangedCallback + ) + constructor( + baseType: TurbulenceNoiseShader.Companion.Type, + config: TurbulenceNoiseAnimationConfig, + renderEffectCallback: RenderEffectDrawCallback, + animationStateChangedCallback: AnimationStateChangedCallback? = null + ) : this( + baseType, + config, + paintCallback = null, + renderEffectCallback, + animationStateChangedCallback + ) + + private val turbulenceNoiseShader: TurbulenceNoiseShader = + TurbulenceNoiseShader(baseType).apply { applyConfig(config) } + private var currentAnimator: ValueAnimator? = null + private var state: AnimationState = AnimationState.NOT_PLAYING + set(value) { + if (field != value) { + animationStateChangedCallback?.onStateChanged(field, value) + field = value + } + } + + // We create a paint instance only if the client renders it with Paint. + private val paint = + if (paintCallback != null) { + Paint().apply { this.shader = turbulenceNoiseShader } + } else { + null + } + + /** Plays LoadingEffect. */ + fun play() { + if (state != AnimationState.NOT_PLAYING) { + return // Ignore if any of the animation is playing. + } + + playEaseIn() + } + + // TODO(b/237282226): Support force finish. + /** Finishes the main animation, which triggers the ease-out animation. */ + fun finish() { + if (state == AnimationState.MAIN) { + // Calling Animator#end sets the animation state back to the initial state. Using pause + // to avoid visual artifacts. + currentAnimator?.pause() + currentAnimator = null + + playEaseOut() + } + } + + /** Updates the noise color dynamically. */ + fun updateColor(newColor: Int) { + turbulenceNoiseShader.setColor(newColor) + } + + /** Updates the noise color that's screen blended on top. */ + fun updateScreenColor(newColor: Int) { + turbulenceNoiseShader.setScreenColor(newColor) + } + + /** + * Retrieves the noise offset x, y, z values. This is useful for replaying the animation + * smoothly from the last animation, by passing in the last values to the next animation. + */ + fun getNoiseOffset(): Array { + return arrayOf( + turbulenceNoiseShader.noiseOffsetX, + turbulenceNoiseShader.noiseOffsetY, + turbulenceNoiseShader.noiseOffsetZ + ) + } + + private fun playEaseIn() { + if (state != AnimationState.NOT_PLAYING) { + return + } + state = AnimationState.EASE_IN + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.easeInDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = turbulenceNoiseShader.noiseOffsetX + val initialY = turbulenceNoiseShader.noiseOffsetY + val initialZ = turbulenceNoiseShader.noiseOffsetZ + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + val progress = updateListener.animatedValue as Float + + turbulenceNoiseShader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + // TODO: Replace it with a better curve. + turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier) + + draw() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + playMain() + } + } + ) + + animator.start() + this.currentAnimator = animator + } + + private fun playMain() { + if (state != AnimationState.EASE_IN) { + return + } + state = AnimationState.MAIN + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.maxDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = turbulenceNoiseShader.noiseOffsetX + val initialY = turbulenceNoiseShader.noiseOffsetY + val initialZ = turbulenceNoiseShader.noiseOffsetZ + + turbulenceNoiseShader.setOpacity(config.luminosityMultiplier) + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + turbulenceNoiseShader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + draw() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + playEaseOut() + } + } + ) + + animator.start() + this.currentAnimator = animator + } + + private fun playEaseOut() { + if (state != AnimationState.MAIN) { + return + } + state = AnimationState.EASE_OUT + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.easeOutDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = turbulenceNoiseShader.noiseOffsetX + val initialY = turbulenceNoiseShader.noiseOffsetY + val initialZ = turbulenceNoiseShader.noiseOffsetZ + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + val progress = updateListener.animatedValue as Float + + turbulenceNoiseShader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + // TODO: Replace it with a better curve. + turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier) + + draw() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + state = AnimationState.NOT_PLAYING + } + } + ) + + animator.start() + this.currentAnimator = animator + } + + private fun draw() { + paintCallback?.onDraw(paint!!) + renderEffectCallback?.onDraw( + RenderEffect.createRuntimeShaderEffect( + turbulenceNoiseShader, + TurbulenceNoiseShader.BACKGROUND_UNIFORM + ) + ) + } + + /** + * States of the loading effect animation. + * + *

The state is designed to be follow the order below: [AnimationState.EASE_IN], + * [AnimationState.MAIN], [AnimationState.EASE_OUT]. Note that ease in and out don't necessarily + * mean the acceleration and deceleration in the animation curve. They simply mean each stage of + * the animation. (i.e. Intro, core, and rest) + */ + enum class AnimationState { + EASE_IN, + MAIN, + EASE_OUT, + NOT_PLAYING + } + + /** Optional callback that is triggered when the animation state changes. */ + interface AnimationStateChangedCallback { + /** + * A callback that's triggered when the [AnimationState] changes. Example usage is + * performing a cleanup when [AnimationState] becomes [NOT_PLAYING]. + */ + fun onStateChanged(oldState: AnimationState, newState: AnimationState) {} + } + + private companion object { + private const val MS_TO_SEC = 0.001f + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectView.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectView.kt new file mode 100644 index 0000000000..aad593eb6a --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectView.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects.loadingeffect + +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View + +/** Custom View for drawing the [LoadingEffect] with [Canvas.drawPaint]. */ +open class LoadingEffectView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + + private var paint: Paint? = null + private var blendMode: BlendMode = BlendMode.SRC_OVER + + override fun onDraw(canvas: Canvas) { + if (!canvas.isHardwareAccelerated) { + return + } + paint?.let { canvas.drawPaint(it) } + } + + /** Designed to be called on [LoadingEffect.PaintDrawCallback.onDraw]. */ + fun draw(paint: Paint) { + this.paint = paint + this.paint!!.blendMode = blendMode + + invalidate() + } + + /** Sets the blend mode of the [Paint]. */ + fun setBlendMode(blendMode: BlendMode) { + this.blendMode = blendMode + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt new file mode 100644 index 0000000000..d8e17c9c82 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt @@ -0,0 +1,49 @@ +/* + * 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.systemui.surfaceeffects.ripple + +import androidx.annotation.VisibleForTesting + +/** Controller that handles playing [RippleAnimation]. */ +class MultiRippleController(private val multipleRippleView: MultiRippleView) { + + companion object { + /** Max number of ripple animations at a time. */ + @VisibleForTesting const val MAX_RIPPLE_NUMBER = 10 + } + + /** Updates all the ripple colors during the animation. */ + fun updateColor(color: Int) { + multipleRippleView.ripples.forEach { anim -> anim.updateColor(color) } + } + + fun play(rippleAnimation: RippleAnimation) { + if (multipleRippleView.ripples.size >= MAX_RIPPLE_NUMBER) { + return + } + + multipleRippleView.ripples.add(rippleAnimation) + + rippleAnimation.play { + // Remove ripple once the animation is done + multipleRippleView.ripples.remove(rippleAnimation) + } + + // Trigger drawing + multipleRippleView.invalidate() + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt new file mode 100644 index 0000000000..6c175ddf1e --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt @@ -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. + */ + +package com.android.systemui.surfaceeffects.ripple + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import androidx.annotation.VisibleForTesting + +/** + * A view that allows multiple ripples to play. + * + * Use [MultiRippleController] to play ripple animations. + */ +class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + val ripples = ArrayList() + private val ripplePaint = Paint() + + companion object { + private const val TAG = "MultiRippleView" + } + + override fun onDraw(canvas: Canvas) { + if (!canvas.isHardwareAccelerated) { + // Drawing with the ripple shader requires hardware acceleration, so skip if it's + // unsupported. + return + } + + var shouldInvalidate = false + + ripples.forEach { anim -> + ripplePaint.shader = anim.rippleShader + canvas.drawPaint(ripplePaint) + + shouldInvalidate = shouldInvalidate || anim.isPlaying() + } + + if (shouldInvalidate) { + invalidate() + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt new file mode 100644 index 0000000000..d4372507e2 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt @@ -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.systemui.surfaceeffects.ripple + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import androidx.annotation.VisibleForTesting +import androidx.core.graphics.ColorUtils + +/** A single ripple animation. */ +class RippleAnimation(private val config: RippleAnimationConfig) { + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + val rippleShader: RippleShader = RippleShader(config.rippleShape) + private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f) + + init { + applyConfigToShader() + } + + /** Updates the ripple color during the animation. */ + fun updateColor(color: Int) { + config.apply { config.color = color } + applyConfigToShader() + } + + @JvmOverloads + fun play(onAnimationEnd: Runnable? = null) { + if (isPlaying()) { + return // Ignore if ripple effect is already playing + } + + animator.duration = config.duration + animator.addUpdateListener { updateListener -> + val now = updateListener.currentPlayTime + val progress = updateListener.animatedValue as Float + rippleShader.rawProgress = progress + rippleShader.distortionStrength = if (config.shouldDistort) 1 - progress else 0f + rippleShader.time = now.toFloat() + } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onAnimationEnd?.run() + } + } + ) + animator.start() + } + + /** Indicates whether the animation is playing. */ + fun isPlaying(): Boolean = animator.isRunning + + private fun applyConfigToShader() { + with(rippleShader) { + setCenter(config.centerX, config.centerY) + rippleSize.setMaxSize(config.maxWidth, config.maxHeight) + pixelDensity = config.pixelDensity + color = ColorUtils.setAlphaComponent(config.color, config.opacity) + sparkleStrength = config.sparkleStrength + + assignFadeParams(baseRingFadeParams, config.baseRingFadeParams) + assignFadeParams(sparkleRingFadeParams, config.sparkleRingFadeParams) + assignFadeParams(centerFillFadeParams, config.centerFillFadeParams) + } + } + + private fun assignFadeParams( + destFadeParams: RippleShader.FadeParams, + srcFadeParams: RippleShader.FadeParams? + ) { + srcFadeParams?.let { + destFadeParams.fadeInStart = it.fadeInStart + destFadeParams.fadeInEnd = it.fadeInEnd + destFadeParams.fadeOutStart = it.fadeOutStart + destFadeParams.fadeOutEnd = it.fadeOutEnd + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt new file mode 100644 index 0000000000..91c0a5b635 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt @@ -0,0 +1,29 @@ +package com.android.systemui.surfaceeffects.ripple + +import android.graphics.Color + +/** + * A struct that holds the ripple animation configurations. + * + *

This configuration is designed to play a SINGLE animation. Do not reuse or modify the + * configuration parameters to play different animations, unless the value has to change within the + * single animation (e.g. Change color or opacity during the animation). Note that this data class + * is pulled out to make the [RippleAnimation] constructor succinct. + */ +data class RippleAnimationConfig( + val rippleShape: RippleShader.RippleShape = RippleShader.RippleShape.CIRCLE, + val duration: Long = 0L, + val centerX: Float = 0f, + val centerY: Float = 0f, + val maxWidth: Float = 0f, + val maxHeight: Float = 0f, + val pixelDensity: Float = 1f, + var color: Int = Color.WHITE, + val opacity: Int = RippleShader.RIPPLE_DEFAULT_ALPHA, + val sparkleStrength: Float = RippleShader.RIPPLE_SPARKLE_STRENGTH, + // Null means it uses default fade parameter values. + val baseRingFadeParams: RippleShader.FadeParams? = null, + val sparkleRingFadeParams: RippleShader.FadeParams? = null, + val centerFillFadeParams: RippleShader.FadeParams? = null, + val shouldDistort: Boolean = true +) diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt new file mode 100644 index 0000000000..7e56f4b3d2 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt @@ -0,0 +1,460 @@ +/* + * 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.systemui.surfaceeffects.ripple + +import android.graphics.RuntimeShader +import android.util.Log +import android.view.animation.Interpolator +import android.view.animation.PathInterpolator +import androidx.annotation.VisibleForTesting +import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary + +/** + * Shader class that renders an expanding ripple effect. The ripple contains three elements: + * 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away + * 2. an expanding ring that appears throughout the effect + * 3. an expanding ring-shaped area that reveals noise over #2. + * + * The ripple shader will be default to the circle shape if not specified. + * + * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java. + */ +class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : + RuntimeShader(buildShader(rippleShape)) { + + /** Shapes that the [RippleShader] supports. */ + enum class RippleShape { + CIRCLE, + ROUNDED_BOX, + ELLIPSE + } + // language=AGSL + companion object { + private val TAG = RippleShader::class.simpleName + + // Default fade in/ out values. The value range is [0,1]. + const val DEFAULT_FADE_IN_START = 0f + const val DEFAULT_FADE_OUT_END = 1f + + const val DEFAULT_BASE_RING_FADE_IN_END = 0.1f + const val DEFAULT_BASE_RING_FADE_OUT_START = 0.3f + + const val DEFAULT_SPARKLE_RING_FADE_IN_END = 0.1f + const val DEFAULT_SPARKLE_RING_FADE_OUT_START = 0.4f + + const val DEFAULT_CENTER_FILL_FADE_IN_END = 0f + const val DEFAULT_CENTER_FILL_FADE_OUT_START = 0f + const val DEFAULT_CENTER_FILL_FADE_OUT_END = 0.6f + + const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f + const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt() + const val RIPPLE_DEFAULT_ALPHA: Int = 115 // full opacity is 255. + + private const val SHADER_UNIFORMS = + """ + uniform vec2 in_center; + uniform vec2 in_size; + uniform float in_cornerRadius; + uniform float in_thickness; + uniform float in_time; + uniform float in_distort_radial; + uniform float in_distort_xy; + uniform float in_fadeSparkle; + uniform float in_fadeFill; + uniform float in_fadeRing; + uniform float in_blur; + uniform float in_pixelDensity; + layout(color) uniform vec4 in_color; + uniform float in_sparkle_strength; + """ + + private const val SHADER_CIRCLE_MAIN = + """ + vec4 main(vec2 p) { + vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); + float radius = in_size.x * 0.5; + float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur); + float inside = soften(sdCircle(p_distorted-in_center, radius * 1.25), in_blur); + float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) + * (1.-sparkleRing) * in_fadeSparkle; + + float rippleInsideAlpha = (1.-inside) * in_fadeFill; + float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; + float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; + vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; + return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); + } + """ + + private const val SHADER_ROUNDED_BOX_MAIN = + """ + vec4 main(vec2 p) { + float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius, + in_thickness), in_blur); + float inside = soften(sdRoundedBox(p-in_center, in_size * 1.25, in_cornerRadius), + in_blur); + float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) + * (1.-sparkleRing) * in_fadeSparkle; + + float rippleInsideAlpha = (1.-inside) * in_fadeFill; + float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; + float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; + vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; + return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); + } + """ + + private const val SHADER_ELLIPSE_MAIN = + """ + vec4 main(vec2 p) { + vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); + + float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur); + float inside = soften(sdEllipse(p_distorted-in_center, in_size * 1.2), in_blur); + float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) + * (1.-sparkleRing) * in_fadeSparkle; + + float rippleInsideAlpha = (1.-inside) * in_fadeFill; + float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; + float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; + vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; + return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); + } + """ + + private const val CIRCLE_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.CIRCLE_SDF + + SHADER_CIRCLE_MAIN + private const val ROUNDED_BOX_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.ROUNDED_BOX_SDF + + SHADER_ROUNDED_BOX_MAIN + private const val ELLIPSE_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.ELLIPSE_SDF + + SHADER_ELLIPSE_MAIN + + private fun buildShader(rippleShape: RippleShape): String = + when (rippleShape) { + RippleShape.CIRCLE -> CIRCLE_SHADER + RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER + RippleShape.ELLIPSE -> ELLIPSE_SHADER + } + + private fun subProgress(start: Float, end: Float, progress: Float): Float { + // Avoid division by 0. + if (start == end) { + // If start and end are the same and progress has exceeded the start/ end point, + // treat it as 1, otherwise 0. + return if (progress > start) 1f else 0f + } + + val min = Math.min(start, end) + val max = Math.max(start, end) + val sub = Math.min(Math.max(progress, min), max) + return (sub - start) / (end - start) + } + + private fun getFade(fadeParams: FadeParams, rawProgress: Float): Float { + val fadeIn = subProgress(fadeParams.fadeInStart, fadeParams.fadeInEnd, rawProgress) + val fadeOut = + 1f - subProgress(fadeParams.fadeOutStart, fadeParams.fadeOutEnd, rawProgress) + + return Math.min(fadeIn, fadeOut) + } + + private fun lerp(start: Float, stop: Float, amount: Float): Float { + return start + (stop - start) * amount + } + + // Copied from [Interpolators#STANDARD]. This is to remove dependency on AnimationLib. + private val STANDARD: Interpolator = PathInterpolator(0.2f, 0f, 0f, 1f) + } + + /** Sets the center position of the ripple. */ + fun setCenter(x: Float, y: Float) { + setFloatUniform("in_center", x, y) + } + + /** + * Blur multipliers for the ripple. + * + *

It interpolates from [blurStart] to [blurEnd] based on the [progress]. Increase number to + * add more blur. + */ + var blurStart: Float = 1.25f + var blurEnd: Float = 0.5f + + /** Size of the ripple. */ + val rippleSize = RippleSize() + + /** + * Linear progress of the ripple. Float value between [0, 1]. + * + *

Note that the progress here is expected to be linear without any curve applied. + */ + var rawProgress: Float = 0.0f + set(value) { + field = value + progress = STANDARD.getInterpolation(value) + + setFloatUniform("in_fadeSparkle", getFade(sparkleRingFadeParams, value)) + setFloatUniform("in_fadeRing", getFade(baseRingFadeParams, value)) + setFloatUniform("in_fadeFill", getFade(centerFillFadeParams, value)) + } + + /** Progress with Standard easing curve applied. */ + private var progress: Float = 0.0f + set(value) { + field = value + + rippleSize.update(value) + + setFloatUniform("in_size", rippleSize.currentWidth, rippleSize.currentHeight) + setFloatUniform("in_thickness", rippleSize.currentHeight * 0.5f) + // Corner radius is always max of the min between the current width and height. + setFloatUniform( + "in_cornerRadius", + Math.min(rippleSize.currentWidth, rippleSize.currentHeight) + ) + setFloatUniform("in_blur", lerp(1.25f, 0.5f, value)) + } + + /** Play time since the start of the effect. */ + var time: Float = 0.0f + set(value) { + field = value + setFloatUniform("in_time", value) + } + + /** A hex value representing the ripple color, in the format of ARGB */ + var color: Int = 0xffffff + set(value) { + field = value + setColorUniform("in_color", value) + } + + /** + * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with + * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's + * opaque white and looks the most grainy. + */ + var sparkleStrength: Float = 0.0f + set(value) { + field = value + setFloatUniform("in_sparkle_strength", value) + } + + /** Distortion strength of the ripple. Expected value between[0, 1]. */ + var distortionStrength: Float = 0.0f + set(value) { + field = value + setFloatUniform("in_distort_radial", 75 * rawProgress * value) + setFloatUniform("in_distort_xy", 75 * value) + } + + /** + * Pixel density of the screen that the effects are rendered to. + * + *

This value should come from [resources.displayMetrics.density]. + */ + var pixelDensity: Float = 1.0f + set(value) { + field = value + setFloatUniform("in_pixelDensity", value) + } + + /** Parameters that are used to fade in/ out of the sparkle ring. */ + val sparkleRingFadeParams = + FadeParams( + DEFAULT_FADE_IN_START, + DEFAULT_SPARKLE_RING_FADE_IN_END, + DEFAULT_SPARKLE_RING_FADE_OUT_START, + DEFAULT_FADE_OUT_END + ) + + /** + * Parameters that are used to fade in/ out of the base ring. + * + *

Note that the shader draws the sparkle ring on top of the base ring. + */ + val baseRingFadeParams = + FadeParams( + DEFAULT_FADE_IN_START, + DEFAULT_BASE_RING_FADE_IN_END, + DEFAULT_BASE_RING_FADE_OUT_START, + DEFAULT_FADE_OUT_END + ) + + /** Parameters that are used to fade in/ out of the center fill. */ + val centerFillFadeParams = + FadeParams( + DEFAULT_FADE_IN_START, + DEFAULT_CENTER_FILL_FADE_IN_END, + DEFAULT_CENTER_FILL_FADE_OUT_START, + DEFAULT_CENTER_FILL_FADE_OUT_END + ) + + /** + * Parameters used for fade in and outs of the ripple. + * + *

Note that all the fade in/ outs are "linear" progression. + * + * ``` + * (opacity) + * 1 + * │ + * maxAlpha ← ―――――――――――― + * │ / \ + * │ / \ + * minAlpha ←――――/ \―――― (alpha change) + * │ + * │ + * 0 ―――↑―――↑―――――――――↑―――↑――――1 (progress) + * fadeIn fadeOut + * Start & End Start & End + * ``` + * + *

If no fade in/ out is needed, set [fadeInStart] and [fadeInEnd] to 0; [fadeOutStart] and + * [fadeOutEnd] to 1. + */ + data class FadeParams( + /** + * The starting point of the fade out which ends at [fadeInEnd], given that the animation + * goes from 0 to 1. + */ + var fadeInStart: Float = DEFAULT_FADE_IN_START, + /** + * The endpoint of the fade in when the fade in starts at [fadeInStart], given that the + * animation goes from 0 to 1. + */ + var fadeInEnd: Float, + /** + * The starting point of the fade out which ends at 1, given that the animation goes from 0 + * to 1. + */ + var fadeOutStart: Float, + + /** The endpoint of the fade out, given that the animation goes from 0 to 1. */ + var fadeOutEnd: Float = DEFAULT_FADE_OUT_END, + ) + + /** + * Desired size of the ripple at a point t in [progress]. + * + *

Note that [progress] is curved and normalized. Below is an example usage: + * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 0.2f, width= 500f, height= + * 700f), SizeAtProgress(t= 1f, width= 100f, height= 300f) + * + *

For simple ripple effects, you will want to use [setMaxSize] as it is translated into: + * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 1f, width= maxWidth, height= + * maxHeight) + */ + data class SizeAtProgress( + /** Time t in [0,1] progress range. */ + var t: Float, + /** Target width size of the ripple at time [t]. */ + var width: Float, + /** Target height size of the ripple at time [t]. */ + var height: Float + ) + + /** Updates and stores the ripple size. */ + inner class RippleSize { + @VisibleForTesting var sizes = mutableListOf() + @VisibleForTesting var currentSizeIndex = 0 + @VisibleForTesting val initialSize = SizeAtProgress(0f, 0f, 0f) + + var currentWidth: Float = 0f + private set + var currentHeight: Float = 0f + private set + + /** + * Sets the max size of the ripple. + * + *

Use this if the ripple shape simply changes linearly. + */ + fun setMaxSize(width: Float, height: Float) { + setSizeAtProgresses(initialSize, SizeAtProgress(1f, width, height)) + } + + /** + * Sets the list of [sizes]. + * + *

Note that setting this clears the existing sizes. + */ + fun setSizeAtProgresses(vararg sizes: SizeAtProgress) { + // Reset everything. + this.sizes.clear() + currentSizeIndex = 0 + + this.sizes.addAll(sizes) + this.sizes.sortBy { it.t } + } + + /** + * Updates the current ripple size based on the progress. + * + *

Should be called when progress updates. + */ + fun update(progress: Float) { + val targetIndex = updateTargetIndex(progress) + val prevIndex = Math.max(targetIndex - 1, 0) + + val targetSize = sizes[targetIndex] + val prevSize = sizes[prevIndex] + + val subProgress = subProgress(prevSize.t, targetSize.t, progress) + + currentWidth = targetSize.width * subProgress + prevSize.width + currentHeight = targetSize.height * subProgress + prevSize.height + } + + private fun updateTargetIndex(progress: Float): Int { + if (sizes.isEmpty()) { + // It could be empty on init. + if (progress > 0f) { + Log.e( + TAG, + "Did you forget to set the ripple size? Use [setMaxSize] or " + + "[setSizeAtProgresses] before playing the animation." + ) + } + // If there's no size is set, we set everything to 0 and return early. + setSizeAtProgresses(initialSize) + return currentSizeIndex + } + + var candidate = sizes[currentSizeIndex] + + while (progress > candidate.t) { + currentSizeIndex = Math.min(currentSizeIndex + 1, sizes.size - 1) + candidate = sizes[currentSizeIndex] + } + + return currentSizeIndex + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt new file mode 100644 index 0000000000..b899127095 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt @@ -0,0 +1,252 @@ +/* + * 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.systemui.surfaceeffects.ripple + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.Configuration +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import androidx.core.graphics.ColorUtils +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape + +/** + * A generic expanding ripple effect. + * + * Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter], + * then call [startRipple] to trigger the ripple expansion. + */ +open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + + protected lateinit var rippleShader: RippleShader + lateinit var rippleShape: RippleShape + private set + + private val ripplePaint = Paint() + protected val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f) + + var duration: Long = 1750 + + fun setMaxSize(maxWidth: Float, maxHeight: Float) { + rippleShader.rippleSize.setMaxSize(maxWidth, maxHeight) + } + + private var centerX: Float = 0.0f + private var centerY: Float = 0.0f + fun setCenter(x: Float, y: Float) { + this.centerX = x + this.centerY = y + rippleShader.setCenter(x, y) + } + + override fun onConfigurationChanged(newConfig: Configuration?) { + rippleShader.pixelDensity = resources.displayMetrics.density + super.onConfigurationChanged(newConfig) + } + + override fun onAttachedToWindow() { + rippleShader.pixelDensity = resources.displayMetrics.density + super.onAttachedToWindow() + } + + /** Initializes the shader. Must be called before [startRipple]. */ + fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) { + this.rippleShape = rippleShape + rippleShader = RippleShader(rippleShape) + + rippleShader.color = RippleShader.RIPPLE_DEFAULT_COLOR + rippleShader.rawProgress = 0f + rippleShader.sparkleStrength = RippleShader.RIPPLE_SPARKLE_STRENGTH + rippleShader.pixelDensity = resources.displayMetrics.density + + ripplePaint.shader = rippleShader + } + + /** + * Sets the fade parameters for the base ring. + * + *

Base ring indicates a blurred ring below the sparkle ring. See + * [RippleShader.baseRingFadeParams]. + */ + @JvmOverloads + fun setBaseRingFadeParams( + fadeInStart: Float = rippleShader.baseRingFadeParams.fadeInStart, + fadeInEnd: Float = rippleShader.baseRingFadeParams.fadeInEnd, + fadeOutStart: Float = rippleShader.baseRingFadeParams.fadeOutStart, + fadeOutEnd: Float = rippleShader.baseRingFadeParams.fadeOutEnd + ) { + setFadeParams( + rippleShader.baseRingFadeParams, + fadeInStart, + fadeInEnd, + fadeOutStart, + fadeOutEnd + ) + } + + /** + * Sets the fade parameters for the sparkle ring. + * + *

Sparkle ring refers to the ring that's drawn on top of the base ring. See + * [RippleShader.sparkleRingFadeParams]. + */ + @JvmOverloads + fun setSparkleRingFadeParams( + fadeInStart: Float = rippleShader.sparkleRingFadeParams.fadeInStart, + fadeInEnd: Float = rippleShader.sparkleRingFadeParams.fadeInEnd, + fadeOutStart: Float = rippleShader.sparkleRingFadeParams.fadeOutStart, + fadeOutEnd: Float = rippleShader.sparkleRingFadeParams.fadeOutEnd + ) { + setFadeParams( + rippleShader.sparkleRingFadeParams, + fadeInStart, + fadeInEnd, + fadeOutStart, + fadeOutEnd + ) + } + + /** + * Sets the fade parameters for the center fill. + * + *

One common use case is set all the params to 1, which completely removes the center fill. + * See [RippleShader.centerFillFadeParams]. + */ + @JvmOverloads + fun setCenterFillFadeParams( + fadeInStart: Float = rippleShader.centerFillFadeParams.fadeInStart, + fadeInEnd: Float = rippleShader.centerFillFadeParams.fadeInEnd, + fadeOutStart: Float = rippleShader.centerFillFadeParams.fadeOutStart, + fadeOutEnd: Float = rippleShader.centerFillFadeParams.fadeOutEnd + ) { + setFadeParams( + rippleShader.centerFillFadeParams, + fadeInStart, + fadeInEnd, + fadeOutStart, + fadeOutEnd + ) + } + + private fun setFadeParams( + fadeParams: RippleShader.FadeParams, + fadeInStart: Float, + fadeInEnd: Float, + fadeOutStart: Float, + fadeOutEnd: Float + ) { + with(fadeParams) { + this.fadeInStart = fadeInStart + this.fadeInEnd = fadeInEnd + this.fadeOutStart = fadeOutStart + this.fadeOutEnd = fadeOutEnd + } + } + + /** + * Sets blur multiplier at start and end of the progress. + * + *

It interpolates between [start] and [end]. No need to set it if using default blur. + */ + fun setBlur(start: Float, end: Float) { + rippleShader.blurStart = start + rippleShader.blurEnd = end + } + + /** + * Sets the list of [RippleShader.SizeAtProgress]. + * + *

Note that this clears the list before it sets with the new data. + */ + fun setSizeAtProgresses(vararg targetSizes: RippleShader.SizeAtProgress) { + rippleShader.rippleSize.setSizeAtProgresses(*targetSizes) + } + + @JvmOverloads + fun startRipple(onAnimationEnd: Runnable? = null) { + if (animator.isRunning) { + return // Ignore if ripple effect is already playing + } + animator.duration = duration + animator.addUpdateListener { updateListener -> + val now = updateListener.currentPlayTime + val progress = updateListener.animatedValue as Float + rippleShader.rawProgress = progress + rippleShader.distortionStrength = 1 - progress + rippleShader.time = now.toFloat() + invalidate() + } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onAnimationEnd?.run() + } + } + ) + animator.start() + } + + /** + * Set the color to be used for the ripple. + * + * The alpha value of the color will be applied to the ripple. The alpha range is [0-255]. + */ + fun setColor(color: Int, alpha: Int = RippleShader.RIPPLE_DEFAULT_ALPHA) { + rippleShader.color = ColorUtils.setAlphaComponent(color, alpha) + } + + /** Set the intensity of the sparkles. */ + fun setSparkleStrength(strength: Float) { + rippleShader.sparkleStrength = strength + } + + /** Indicates whether the ripple animation is playing. */ + fun rippleInProgress(): Boolean = animator.isRunning + + override fun onDraw(canvas: Canvas) { + if (!canvas.isHardwareAccelerated) { + // Drawing with the ripple shader requires hardware acceleration, so skip if it's + // unsupported. + return + } + // To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the + // active effect area. Values here should be kept in sync with the animation implementation + // in the ripple shader. + if (rippleShape == RippleShape.CIRCLE) { + val maskRadius = rippleShader.rippleSize.currentWidth + canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint) + } else if (rippleShape == RippleShape.ELLIPSE) { + val maskWidth = rippleShader.rippleSize.currentWidth * 2 + val maskHeight = rippleShader.rippleSize.currentHeight * 2 + canvas.drawRect( + /* left= */ centerX - maskWidth, + /* top= */ centerY - maskHeight, + /* right= */ centerX + maskWidth, + /* bottom= */ centerY + maskHeight, + ripplePaint + ) + } else { // RippleShape.RoundedBox + // No masking for the rounded box, as it has more blur which requires larger bounds. + // Masking creates sharp bounds even when the masking is 4 times bigger. + canvas.drawPaint(ripplePaint) + } + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt new file mode 100644 index 0000000000..c94fad7246 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt @@ -0,0 +1,36 @@ +/* + * 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.systemui.surfaceeffects.shaders + +import android.graphics.RuntimeShader + +/** Simply renders a solid color. */ +class SolidColorShader(color: Int) : RuntimeShader(SHADER) { + // language=AGSL + private companion object { + private const val SHADER = + """ + layout(color) uniform vec4 in_color; + vec4 main(vec2 p) { + return in_color; + } + """ + } + + init { + setColorUniform("in_color", color) + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt new file mode 100644 index 0000000000..df07856e32 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt @@ -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 com.android.systemui.surfaceeffects.shaders + +import android.graphics.Color +import android.graphics.RuntimeShader +import android.graphics.Shader +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary + +/** + * Renders sparkles based on the luma matte. + * + * For example, you can pass in simplex noise as the luma matte and have a cloud looking sparkles. + * + * You may want to utilize this shader by: (Preferred) 1. Create a RuntimeShaderEffect and set the + * [RenderEffect] to the target [View]. + * 2. Create a custom [View], set the shader to the [Paint] and use [Canvas.drawPaint] in [onDraw]. + */ +class SparkleShader : RuntimeShader(SPARKLE_SHADER) { + // language=AGSL + companion object { + private const val UNIFORMS = + """ + // Used it for RenderEffect. For example: + // myView.setRenderEffect( + // RenderEffect.createRuntimeShaderEffect(SparkleShader(), "in_src") + // ) + uniform shader in_src; + uniform half in_time; + uniform half in_pixelate; + uniform shader in_lumaMatte; + layout(color) uniform vec4 in_color; + """ + private const val MAIN_SHADER = + """vec4 main(vec2 p) { + half3 src = in_src.eval(p).rgb; + half luma = getLuminosity(in_lumaMatte.eval(p).rgb); + half sparkle = sparkles(p - mod(p, in_pixelate), in_time); + half3 mask = maskLuminosity(in_color.rgb * sparkle, luma); + + return vec4(src * mask * in_color.a, in_color.a); + } + """ + private const val SPARKLE_SHADER = UNIFORMS + ShaderUtilLibrary.SHADER_LIB + MAIN_SHADER + + /** Highly recommended to use this value unless specified by design spec. */ + const val DEFAULT_SPARKLE_PIXELATE_AMOUNT = 0.8f + } + + init { + // Initializes the src and luma matte to be white. + setInputShader("in_src", SolidColorShader(Color.WHITE)) + setLumaMatteColor(Color.WHITE) + } + + /** + * Sets the time of the sparkle animation. + * + * This is used for animating sparkles. Note that this only makes the sparkles sparkle in place. + * In order to move the sparkles in x, y directions, move the luma matte input instead. + */ + fun setTime(time: Float) { + setFloatUniform("in_time", time) + } + + /** + * Sets pixelated amount of the sparkle. + * + * This value *must* be based on [resources.displayMetrics.density]. Otherwise, this will result + * in having different sparkle sizes on different screens. + * + * Expected to be used as follows: + *

+     *     {@code
+     *     val pixelDensity = context.resources.displayMetrics.density
+     *     // Sparkles will be 0.8 of the pixel size.
+     *     val sparkleShader = SparkleShader().apply { setPixelateAmount(pixelDensity * 0.8f) }
+     *     }
+     * 
+ */ + fun setPixelateAmount(pixelateAmount: Float) { + setFloatUniform("in_pixelate", pixelateAmount) + } + + /** + * Sets the luma matte for the sparkles. The luminosity determines the sparkle's visibility. + * Useful for setting a complex mask (e.g. simplex noise, texture, etc.) + */ + fun setLumaMatte(lumaMatte: Shader) { + setInputShader("in_lumaMatte", lumaMatte) + } + + /** Sets the luma matte for the sparkles. Useful for setting a solid color. */ + fun setLumaMatteColor(color: Int) { + setInputShader("in_lumaMatte", SolidColorShader(color)) + } + + /** Sets the color of the sparkles. Expect to have the alpha value encoded. */ + fun setColor(color: Int) { + setColorUniform("in_color", color) + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt new file mode 100644 index 0000000000..4efab58347 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt @@ -0,0 +1,128 @@ +/* + * 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.systemui.surfaceeffects.shaderutil + +/** Library class that contains 2D signed distance functions. */ +class SdfShaderLibrary { + // language=AGSL + companion object { + const val CIRCLE_SDF = + """ + float sdCircle(vec2 p, float r) { + return (length(p)-r) / r; + } + + float circleRing(vec2 p, float radius) { + float thicknessHalf = radius * 0.25; + + float outerCircle = sdCircle(p, radius + thicknessHalf); + float innerCircle = sdCircle(p, radius); + + return subtract(outerCircle, innerCircle); + } + """ + + const val BOX_SDF = + """ + float sdBox(vec2 p, vec2 size) { + size = size * 0.5; + vec2 d = abs(p) - size; + return length(max(d, 0.)) + min(max(d.x, d.y), 0.) / size.y; + } + """ + + const val ROUNDED_BOX_SDF = + """ + float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) { + size *= 0.5; + cornerRadius *= 0.5; + vec2 d = abs(p) - size + cornerRadius; + + float outside = length(max(d, 0.0)); + float inside = min(max(d.x, d.y), 0.0); + + return (outside + inside - cornerRadius) / size.y; + } + + float roundedBoxRing(vec2 p, vec2 size, float cornerRadius, + float borderThickness) { + float outerRoundBox = sdRoundedBox(p, size + vec2(borderThickness), + cornerRadius + borderThickness); + float innerRoundBox = sdRoundedBox(p, size, cornerRadius); + return subtract(outerRoundBox, innerRoundBox); + } + """ + + // Used non-trigonometry parametrization and Halley's method (iterative) for root finding. + // This is more expensive than the regular circle SDF, recommend to use the circle SDF if + // possible. + const val ELLIPSE_SDF = + """float sdEllipse(vec2 p, vec2 wh) { + wh *= 0.5; + + // symmetry + (wh.x > wh.y) ? wh = wh.yx, p = abs(p.yx) : p = abs(p); + + vec2 u = wh*p, v = wh*wh; + + float U1 = u.y/2.0; + float U2 = v.y-v.x; + float U3 = u.x-U2; + float U4 = u.x+U2; + float U5 = 4.0*U1; + float U6 = 6.0*U1; + float U7 = 3.0*U3; + + float t = 0.5; + for (int i = 0; i < 3; i ++) { + float F1 = t*(t*t*(U1*t+U3)+U4)-U1; + float F2 = t*t*(U5*t+U7)+U4; + float F3 = t*(U6*t+U7); + + t += (F1*F2)/(F1*F3-F2*F2); + } + + t = clamp(t, 0.0, 1.0); + + float d = distance(p, wh*vec2(1.0-t*t,2.0*t)/(t*t+1.0)); + d /= wh.y; + + return (dot(p/wh,p/wh)>1.0) ? d : -d; + } + + float ellipseRing(vec2 p, vec2 wh) { + vec2 thicknessHalf = wh * 0.25; + + float outerEllipse = sdEllipse(p, wh + thicknessHalf); + float innerEllipse = sdEllipse(p, wh); + + return subtract(outerEllipse, innerEllipse); + } + """ + + const val SHADER_SDF_OPERATION_LIB = + """ + float soften(float d, float blur) { + float blurHalf = blur * 0.5; + return smoothstep(-blurHalf, blurHalf, d); + } + + float subtract(float outer, float inner) { + return max(outer, -inner); + } + """ + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt new file mode 100644 index 0000000000..867bbb7d74 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt @@ -0,0 +1,201 @@ +/* + * 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.systemui.surfaceeffects.shaderutil + +/** Common utility functions that are used for computing shaders. */ +object ShaderUtilLibrary { + // language=AGSL + const val SHADER_LIB = + """ + float triangleNoise(vec2 n) { + n = fract(n * vec2(5.3987, 5.4421)); + n += dot(n.yx, n.xy + vec2(21.5351, 14.3137)); + float xy = n.x * n.y; + // compute in [0..2[ and remap to [-1.0..1.0[ + return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0; + } + + const float PI = 3.1415926535897932384626; + + float sparkles(vec2 uv, float t) { + float n = triangleNoise(uv); + float s = 0.0; + for (float i = 0; i < 4; i += 1) { + float l = i * 0.01; + float h = l + 0.1; + float o = smoothstep(n - l, h, n); + o *= abs(sin(PI * o * (t + 0.55 * i))); + s += o; + } + return s; + } + + vec2 distort(vec2 p, float time, float distort_amount_radial, + float distort_amount_xy) { + float angle = atan(p.y, p.x); + return p + vec2(sin(angle * 8 + time * 0.003 + 1.641), + cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial + + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123), + cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy; + } + + // Perceived luminosity (L′), not absolute luminosity. + half getLuminosity(vec3 c) { + return 0.3 * c.r + 0.59 * c.g + 0.11 * c.b; + } + + // Creates a luminosity mask and clamp to the legal range. + vec3 maskLuminosity(vec3 dest, float lum) { + dest.rgb *= vec3(lum); + // Clip back into the legal range + dest = clamp(dest, vec3(0.), vec3(1.0)); + return dest; + } + + // Integer mod. GLSL es 1.0 doesn't have integer mod :( + int imod(int a, int b) { + return a - (b * (a / b)); + } + + ivec3 imod(ivec3 a, int b) { + return ivec3(imod(a.x, b), imod(a.y, b), imod(a.z, b)); + } + + // Integer based hash function with the return range of [-1, 1]. + vec3 hash(vec3 p) { + ivec3 v = ivec3(p); + v = v * 1671731 + 10139267; + + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + + ivec3 v2 = v / 65536; // v >> 16 + v = imod((10 - imod((v + v2), 10)), 10); // v ^ v2 + + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + + // Use sin and cos to map the range to [-1, 1]. + return vec3(sin(float(v.x)), cos(float(v.y)), sin(float(v.z))); + } + + // Skew factors (non-uniform). + const half SKEW = 0.3333333; // 1/3 + const half UNSKEW = 0.1666667; // 1/6 + + // Return range roughly [-1,1]. + // It's because the hash function (that returns a random gradient vector) returns + // different magnitude of vectors. Noise doesn't have to be in the precise range thus + // skipped normalize. + half simplex3d(vec3 p) { + // Skew the input coordinate, so that we get squashed cubical grid + vec3 s = floor(p + (p.x + p.y + p.z) * SKEW); + + // Unskew back + vec3 u = s - (s.x + s.y + s.z) * UNSKEW; + + // Unskewed coordinate that is relative to p, to compute the noise contribution + // based on the distance. + vec3 c0 = p - u; + + // We have six simplices (in this case tetrahedron, since we are in 3D) that we + // could possibly in. + // Here, we are finding the correct tetrahedron (simplex shape), and traverse its + // four vertices (c0..3) when computing noise contribution. + // The way we find them is by comparing c0's x,y,z values. + // For example in 2D, we can find the triangle (simplex shape in 2D) that we are in + // by comparing x and y values. i.e. x>y lower, xy0>z0: (1,0,0), (1,1,0), (1,1,1) + // x0>z0>y0: (1,0,0), (1,0,1), (1,1,1) + // z0>x0>y0: (0,0,1), (1,0,1), (1,1,1) + // z0>y0>x0: (0,0,1), (0,1,1), (1,1,1) + // y0>z0>x0: (0,1,0), (0,1,1), (1,1,1) + // y0>x0>z0: (0,1,0), (1,1,0), (1,1,1) + // + // The rule is: + // * For offset1, set 1 at the max component, otherwise 0. + // * For offset2, set 0 at the min component, otherwise 1. + // * For offset3, set 1 for all. + // + // Encode x0-y0, y0-z0, z0-x0 in a vec3 + vec3 en = c0 - c0.yzx; + // Each represents whether x0>y0, y0>z0, z0>x0 + en = step(vec3(0.), en); + // en.zxy encodes z0>x0, x0>y0, y0>x0 + vec3 offset1 = en * (1. - en.zxy); // find max + vec3 offset2 = 1. - en.zxy * (1. - en); // 1-(find min) + vec3 offset3 = vec3(1.); + + vec3 c1 = c0 - offset1 + UNSKEW; + vec3 c2 = c0 - offset2 + UNSKEW * 2.; + vec3 c3 = c0 - offset3 + UNSKEW * 3.; + + // Kernel summation: dot(max(0, r^2-d^2))^4, noise contribution) + // + // First compute d^2, squared distance to the point. + vec4 w; // w = max(0, r^2 - d^2)) + w.x = dot(c0, c0); + w.y = dot(c1, c1); + w.z = dot(c2, c2); + w.w = dot(c3, c3); + + // Noise contribution should decay to zero before they cross the simplex boundary. + // Usually r^2 is 0.5 or 0.6; + // 0.5 ensures continuity but 0.6 increases the visual quality for the application + // where discontinuity isn't noticeable. + w = max(0.6 - w, 0.); + + // Noise contribution from each point. + vec4 nc; + nc.x = dot(hash(s), c0); + nc.y = dot(hash(s + offset1), c1); + nc.z = dot(hash(s + offset2), c2); + nc.w = dot(hash(s + offset3), c3); + + nc *= w*w*w*w; + + // Add all the noise contributions. + // Should multiply by the possible max contribution to adjust the range in [-1,1]. + return dot(vec4(32.), nc); + } + + // Random rotations. + // The way you create fractal noise is layering simplex noise with some rotation. + // To make random cloud looking noise, the rotations should not align. (Otherwise it + // creates patterned noise). + // Below rotations only rotate in one axis. + const mat3 rot1 = mat3(1.0, 0. ,0., 0., 0.15, -0.98, 0., 0.98, 0.15); + const mat3 rot2 = mat3(-0.95, 0. ,-0.3, 0., 1., 0., 0.3, 0., -0.95); + const mat3 rot3 = mat3(1.0, 0. ,0., 0., -0.44, -0.89, 0., 0.89, -0.44); + + // Octave = 4 + // Divide each coefficient by 3 to produce more grainy noise. + half simplex3d_fractal(vec3 p) { + return 0.675 * simplex3d(p * rot1) + 0.225 * simplex3d(2.0 * p * rot2) + + 0.075 * simplex3d(4.0 * p * rot3) + 0.025 * simplex3d(8.0 * p); + } + + // Screen blend + vec3 screen(vec3 dest, vec3 src) { + return dest + src - dest * src; + } + """ +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt new file mode 100644 index 0000000000..ba8f1ace02 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt @@ -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.systemui.surfaceeffects.turbulencenoise + +import android.graphics.Color +import java.util.Random + +/** Turbulence noise animation configuration. */ +data class TurbulenceNoiseAnimationConfig( + /** The number of grids that is used to generate noise. */ + val gridCount: Float = DEFAULT_NOISE_GRID_COUNT, + + /** Multiplier for the noise luma matte. Increase this for brighter effects. */ + val luminosityMultiplier: Float = DEFAULT_LUMINOSITY_MULTIPLIER, + + /** Initial noise offsets. */ + val noiseOffsetX: Float = random.nextFloat(), + val noiseOffsetY: Float = random.nextFloat(), + val noiseOffsetZ: Float = random.nextFloat(), + + /** + * Noise move speed variables. + * + * Its sign determines the direction; magnitude determines the speed.
    + * + * ``` + *
  • [noiseMoveSpeedX] positive: right to left; negative: left to right. + *
  • [noiseMoveSpeedY] positive: bottom to top; negative: top to bottom. + *
  • [noiseMoveSpeedZ] its sign doesn't matter much, as it moves in Z direction. Use it + * to add turbulence in place. + * ``` + * + *
+ */ + val noiseMoveSpeedX: Float = 0f, + val noiseMoveSpeedY: Float = 0f, + val noiseMoveSpeedZ: Float = DEFAULT_NOISE_SPEED_Z, + + /** Color of the effect. */ + val color: Int = DEFAULT_COLOR, + /** Background color of the effect. */ + val screenColor: Int = DEFAULT_SCREEN_COLOR, + val width: Float = 0f, + val height: Float = 0f, + val maxDuration: Float = DEFAULT_MAX_DURATION_IN_MILLIS, + val easeInDuration: Float = DEFAULT_EASING_DURATION_IN_MILLIS, + val easeOutDuration: Float = DEFAULT_EASING_DURATION_IN_MILLIS, + val pixelDensity: Float = 1f, + /** + * Variants in noise. Higher number means more contrast; lower number means less contrast but + * make the noise dimmed. You may want to increase the [lumaMatteBlendFactor] to compensate. + * Expected range [0, 1]. + */ + val lumaMatteBlendFactor: Float = DEFAULT_LUMA_MATTE_BLEND_FACTOR, + /** + * Offset for the overall brightness in noise. Higher number makes the noise brighter. You may + * want to use this if you have made the noise softer using [lumaMatteBlendFactor]. Expected + * range [0, 1]. + */ + val lumaMatteOverallBrightness: Float = DEFAULT_LUMA_MATTE_OVERALL_BRIGHTNESS, + /** Whether to flip the luma mask. */ + val shouldInverseNoiseLuminosity: Boolean = false, +) { + companion object { + const val DEFAULT_MAX_DURATION_IN_MILLIS = 30_000f // Max 30 sec + const val DEFAULT_EASING_DURATION_IN_MILLIS = 750f + const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f + const val DEFAULT_NOISE_GRID_COUNT = 1.2f + const val DEFAULT_NOISE_SPEED_Z = 0.3f + const val DEFAULT_COLOR = Color.WHITE + const val DEFAULT_LUMA_MATTE_BLEND_FACTOR = 1f + const val DEFAULT_LUMA_MATTE_OVERALL_BRIGHTNESS = 0f + const val DEFAULT_SCREEN_COLOR = Color.BLACK + private val random = Random() + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt new file mode 100644 index 0000000000..e862f0c43a --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.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.systemui.surfaceeffects.turbulencenoise + +import android.view.View +import androidx.annotation.VisibleForTesting + +/** Plays [TurbulenceNoiseView] in ease-in, main (no easing), and ease-out order. */ +class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) { + + companion object { + /** + * States of the turbulence noise animation. + * + *

The state is designed to be follow the order below: [AnimationState.EASE_IN], + * [AnimationState.MAIN], [AnimationState.EASE_OUT]. + */ + enum class AnimationState { + EASE_IN, + MAIN, + EASE_OUT, + NOT_PLAYING + } + } + + /** Current state of the animation. */ + @VisibleForTesting + var state: AnimationState = AnimationState.NOT_PLAYING + set(value) { + field = value + if (state == AnimationState.NOT_PLAYING) { + turbulenceNoiseView.visibility = View.INVISIBLE + turbulenceNoiseView.clearConfig() + } else { + turbulenceNoiseView.visibility = View.VISIBLE + } + } + + init { + turbulenceNoiseView.visibility = View.INVISIBLE + } + + /** Updates the color of the noise. */ + fun updateNoiseColor(color: Int) { + if (state == AnimationState.NOT_PLAYING) { + return + } + turbulenceNoiseView.updateColor(color) + } + + /** + * Plays [TurbulenceNoiseView] with the given config. + * + *

It plays ease-in, main, and ease-out animations in sequence. + */ + fun play( + baseType: TurbulenceNoiseShader.Companion.Type, + config: TurbulenceNoiseAnimationConfig + ) { + if (state != AnimationState.NOT_PLAYING) { + return // Ignore if any of the animation is playing. + } + + turbulenceNoiseView.initShader(baseType, config) + playEaseInAnimation() + } + + // TODO(b/237282226): Support force finish. + /** Finishes the main animation, which triggers the ease-out animation. */ + fun finish() { + if (state == AnimationState.MAIN) { + turbulenceNoiseView.finish(nextAnimation = this::playEaseOutAnimation) + } + } + + private fun playEaseInAnimation() { + if (state != AnimationState.NOT_PLAYING) { + return + } + state = AnimationState.EASE_IN + + turbulenceNoiseView.playEaseIn(this::playMainAnimation) + } + + private fun playMainAnimation() { + if (state != AnimationState.EASE_IN) { + return + } + state = AnimationState.MAIN + + turbulenceNoiseView.play(this::playEaseOutAnimation) + } + + private fun playEaseOutAnimation() { + if (state != AnimationState.MAIN) { + return + } + state = AnimationState.EASE_OUT + + turbulenceNoiseView.playEaseOut(onAnimationEnd = { state = AnimationState.NOT_PLAYING }) + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt new file mode 100644 index 0000000000..025c8b9dce --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt @@ -0,0 +1,294 @@ +/* + * 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.systemui.surfaceeffects.turbulencenoise + +import android.graphics.RuntimeShader +import com.android.systemui.surfaceeffects.shaders.SolidColorShader +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary +import java.lang.Float.max + +/** + * Shader that renders turbulence simplex noise, by default no octave. + * + * @param baseType the base [Type] of the shader. + */ +class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : + RuntimeShader(getShader(baseType)) { + // language=AGSL + companion object { + /** Uniform name for the background buffer (e.g. image, solid color, etc.). */ + const val BACKGROUND_UNIFORM = "in_src" + private const val UNIFORMS = + """ + uniform shader ${BACKGROUND_UNIFORM}; + uniform float in_gridNum; + uniform vec3 in_noiseMove; + uniform vec2 in_size; + uniform float in_aspectRatio; + uniform float in_opacity; + uniform float in_pixelDensity; + uniform float in_inverseLuma; + uniform half in_lumaMatteBlendFactor; + uniform half in_lumaMatteOverallBrightness; + layout(color) uniform vec4 in_color; + layout(color) uniform vec4 in_screenColor; + """ + + private const val SIMPLEX_SHADER = + """ + vec4 main(vec2 p) { + vec2 uv = p / in_size.xy; + uv.x *= in_aspectRatio; + + // Compute turbulence effect with the uv distorted with simplex noise. + vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; + vec3 color = getColorTurbulenceMask(simplex3d(noiseP) * in_inverseLuma); + + // Blend the result with the background color. + color = in_src.eval(p).rgb + color * 0.6; + + // Add dither with triangle distribution to avoid color banding. Dither in the + // shader here as we are in gamma space. + float dither = triangleNoise(p * in_pixelDensity) / 255.; + color += dither.rrr; + + // Return the pre-multiplied alpha result, i.e. [R*A, G*A, B*A, A]. + return vec4(color * in_opacity, in_opacity); + } + """ + + private const val FRACTAL_SHADER = + """ + vec4 main(vec2 p) { + vec2 uv = p / in_size.xy; + uv.x *= in_aspectRatio; + + vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; + vec3 color = getColorTurbulenceMask(simplex3d_fractal(noiseP) * in_inverseLuma); + + // Blend the result with the background color. + color = in_src.eval(p).rgb + color * 0.6; + + // Skip dithering. + return vec4(color * in_opacity, in_opacity); + } + """ + + /** + * This effect has two layers: color turbulence effect with sparkles on top. + * 1. Gets the luma matte using Simplex noise. + * 2. Generate a colored turbulence layer with the luma matte. + * 3. Generate a colored sparkle layer with the same luma matter. + * 4. Apply a screen color to the background image. + * 5. Composite the previous result with the color turbulence. + * 6. Composite the latest result with the sparkles. + */ + private const val SIMPLEX_SPARKLE_SHADER = + """ + vec4 main(vec2 p) { + vec2 uv = p / in_size.xy; + uv.x *= in_aspectRatio; + + vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; + // Luma is used for both color and sparkle masks. + float luma = simplex3d(noiseP) * in_inverseLuma; + + // Get color layer (color mask with in_color applied) + vec3 colorLayer = getColorTurbulenceMask(simplex3d(noiseP) * in_inverseLuma); + float dither = triangleNoise(p * in_pixelDensity) / 255.; + colorLayer += dither.rrr; + + // Get sparkle layer (sparkle mask with particles & in_color applied) + vec3 sparkleLayer = getSparkleTurbulenceMask(luma, p); + + // Composite with the background. + half4 bgColor = in_src.eval(p); + half sparkleOpacity = smoothstep(0, 0.75, in_opacity); + + half3 effect = screen(bgColor.rgb, in_screenColor.rgb); + effect = screen(effect, colorLayer * 0.22); + effect += sparkleLayer * sparkleOpacity; + + return mix(bgColor, vec4(effect, 1.), in_opacity); + } + """ + + private const val COMMON_FUNCTIONS = + /** + * Below two functions generate turbulence layers (color or sparkles applied) with the + * given luma matte. They both return a mask with in_color applied. + */ + """ + vec3 getColorTurbulenceMask(float luma) { + // Bring it to [0, 1] range. + luma = luma * 0.5 + 0.5; + + half colorLuma = + saturate(luma * in_lumaMatteBlendFactor + in_lumaMatteOverallBrightness) + * in_opacity; + vec3 colorLayer = maskLuminosity(in_color.rgb, colorLuma); + + return colorLayer; + } + + vec3 getSparkleTurbulenceMask(float luma, vec2 p) { + half lumaIntensity = 1.75; + half lumaBrightness = -1.3; + half sparkleLuma = max(luma * lumaIntensity + lumaBrightness, 0.); + + float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_noiseMove.z); + vec3 sparkleLayer = maskLuminosity(in_color.rgb * sparkle, sparkleLuma); + + return sparkleLayer; + } + """ + private const val SIMPLEX_NOISE_SHADER = + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SHADER + private const val FRACTAL_NOISE_SHADER = + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + FRACTAL_SHADER + private const val SPARKLE_NOISE_SHADER = + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SPARKLE_SHADER + + enum class Type { + /** Effect with a simple color noise turbulence. */ + SIMPLEX_NOISE, + /** Effect with a simple color noise turbulence, with fractal. */ + SIMPLEX_NOISE_FRACTAL, + /** Effect with color & sparkle turbulence with screen color layer. */ + SIMPLEX_NOISE_SPARKLE + } + + fun getShader(type: Type): String { + return when (type) { + Type.SIMPLEX_NOISE -> SIMPLEX_NOISE_SHADER + Type.SIMPLEX_NOISE_FRACTAL -> FRACTAL_NOISE_SHADER + Type.SIMPLEX_NOISE_SPARKLE -> SPARKLE_NOISE_SHADER + } + } + } + + /** Convenient way for updating multiple uniform values via config object. */ + fun applyConfig(config: TurbulenceNoiseAnimationConfig) { + setGridCount(config.gridCount) + setPixelDensity(config.pixelDensity) + setColor(config.color) + setScreenColor(config.screenColor) + setSize(config.width, config.height) + setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness) + setInverseNoiseLuminosity(config.shouldInverseNoiseLuminosity) + setNoiseMove(config.noiseOffsetX, config.noiseOffsetY, config.noiseOffsetZ) + } + + /** Sets the number of grid for generating noise. */ + fun setGridCount(gridNumber: Float = 1.0f) { + setFloatUniform("in_gridNum", gridNumber) + } + + /** + * Sets the pixel density of the screen. + * + * Used it for noise dithering. + */ + fun setPixelDensity(pixelDensity: Float) { + setFloatUniform("in_pixelDensity", pixelDensity) + } + + /** Sets the noise color of the effect. Alpha is ignored. */ + fun setColor(color: Int) { + setColorUniform("in_color", color) + } + + /** + * Sets the color that is used for blending on top of the background color/image. Only relevant + * to [Type.SIMPLEX_NOISE_SPARKLE]. + */ + fun setScreenColor(color: Int) { + setColorUniform("in_screenColor", color) + } + + /** + * Sets the background color of the effect. Alpha is ignored. If you are using [RenderEffect], + * no need to call this function since the background image of the View will be used. + */ + fun setBackgroundColor(color: Int) { + setInputShader(BACKGROUND_UNIFORM, SolidColorShader(color)) + } + + /** + * Sets the opacity of the effect. Not intended to set by the client as it is used for + * ease-in/out animations. + * + * Expected value range is [1, 0]. + */ + fun setOpacity(opacity: Float) { + setFloatUniform("in_opacity", opacity) + } + + /** Sets the size of the shader. */ + fun setSize(width: Float, height: Float) { + setFloatUniform("in_size", width, height) + setFloatUniform("in_aspectRatio", width / max(height, 0.001f)) + } + + /** + * Sets blend and brightness factors of the luma matte. + * + * @param lumaMatteBlendFactor increases or decreases the amount of variance in noise. Setting + * this a lower number removes variations. I.e. the turbulence noise will look more blended. + * Expected input range is [0, 1]. + * @param lumaMatteOverallBrightness adds the overall brightness of the turbulence noise. + * Expected input range is [0, 1]. + * + * Example usage: You may want to apply a small number to [lumaMatteBlendFactor], such as 0.2, + * which makes the noise look softer. However it makes the overall noise look dim, so you want + * offset something like 0.3 for [lumaMatteOverallBrightness] to bring back its overall + * brightness. + */ + fun setLumaMatteFactors( + lumaMatteBlendFactor: Float = 1f, + lumaMatteOverallBrightness: Float = 0f + ) { + setFloatUniform("in_lumaMatteBlendFactor", lumaMatteBlendFactor) + setFloatUniform("in_lumaMatteOverallBrightness", lumaMatteOverallBrightness) + } + + /** + * Sets whether to inverse the luminosity of the noise. + * + * By default noise will be used as a luma matte as is. This means that you will see color in + * the brighter area. If you want to invert it, meaning blend color onto the darker side, set to + * true. + */ + fun setInverseNoiseLuminosity(inverse: Boolean) { + setFloatUniform("in_inverseLuma", if (inverse) -1f else 1f) + } + + /** Current noise movements in x, y, and z axes. */ + var noiseOffsetX: Float = 0f + private set + var noiseOffsetY: Float = 0f + private set + var noiseOffsetZ: Float = 0f + private set + + /** Sets noise move offset in x, y, and z direction. */ + fun setNoiseMove(x: Float, y: Float, z: Float) { + noiseOffsetX = x + noiseOffsetY = y + noiseOffsetZ = z + setFloatUniform("in_noiseMove", noiseOffsetX, noiseOffsetY, noiseOffsetZ) + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt new file mode 100644 index 0000000000..5e72e3bd1e --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt @@ -0,0 +1,236 @@ +/* + * 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.systemui.surfaceeffects.turbulencenoise + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import androidx.annotation.VisibleForTesting + +/** + * View that renders turbulence noise effect. + * + *

Use [TurbulenceNoiseController] to control the turbulence animation. If you want to make some + * other turbulence noise effects, either add functionality to [TurbulenceNoiseController] or create + * another controller instead of extend or modify the [TurbulenceNoiseView]. + * + *

Please keep the [TurbulenceNoiseView] (or View in general) not aware of the state. + * + *

Please avoid inheriting the View if possible. Instead, reconsider adding a controller for a + * new case. + */ +class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + + companion object { + private const val MS_TO_SEC = 0.001f + } + + private val paint = Paint() + @VisibleForTesting var turbulenceNoiseShader: TurbulenceNoiseShader? = null + @VisibleForTesting var noiseConfig: TurbulenceNoiseAnimationConfig? = null + @VisibleForTesting var currentAnimator: ValueAnimator? = null + + override fun onDraw(canvas: Canvas) { + if (!canvas.isHardwareAccelerated) { + // Drawing with the turbulence noise shader requires hardware acceleration, so skip + // if it's unsupported. + return + } + + canvas.drawPaint(paint) + } + + /** Updates the color during the animation. No-op if there's no animation playing. */ + internal fun updateColor(color: Int) { + turbulenceNoiseShader?.setColor(color) + } + + /** Plays the turbulence noise with no easing. */ + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + fun play(onAnimationEnd: Runnable? = null) { + if (noiseConfig == null) { + return + } + val config = noiseConfig!! + val shader = turbulenceNoiseShader!! + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.maxDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = shader.noiseOffsetX + val initialY = shader.noiseOffsetY + val initialZ = shader.noiseOffsetZ + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + shader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + shader.setOpacity(config.luminosityMultiplier) + + invalidate() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + onAnimationEnd?.run() + } + } + ) + + animator.start() + currentAnimator = animator + } + + /** Plays the turbulence noise with linear ease-in. */ + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + fun playEaseIn(onAnimationEnd: Runnable? = null) { + if (noiseConfig == null) { + return + } + val config = noiseConfig!! + val shader = turbulenceNoiseShader!! + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.easeInDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = shader.noiseOffsetX + val initialY = shader.noiseOffsetY + val initialZ = shader.noiseOffsetZ + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + val progress = updateListener.animatedValue as Float + + shader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + // TODO: Replace it with a better curve. + shader.setOpacity(progress * config.luminosityMultiplier) + + invalidate() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + onAnimationEnd?.run() + } + } + ) + + animator.start() + currentAnimator = animator + } + + /** Plays the turbulence noise with linear ease-out. */ + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + fun playEaseOut(onAnimationEnd: Runnable? = null) { + if (noiseConfig == null) { + return + } + val config = noiseConfig!! + val shader = turbulenceNoiseShader!! + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.easeOutDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = shader.noiseOffsetX + val initialY = shader.noiseOffsetY + val initialZ = shader.noiseOffsetZ + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + val progress = updateListener.animatedValue as Float + + shader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + // TODO: Replace it with a better curve. + shader.setOpacity((1f - progress) * config.luminosityMultiplier) + + invalidate() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + onAnimationEnd?.run() + } + } + ) + + animator.start() + currentAnimator = animator + } + + /** Finishes the current animation if playing and plays the next animation if given. */ + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + fun finish(nextAnimation: Runnable? = null) { + // Calling Animator#end sets the animation state back to the initial state. Using pause to + // avoid visual artifacts. + currentAnimator?.pause() + currentAnimator = null + + nextAnimation?.run() + } + + /** Applies shader uniforms. Must be called before playing animation. */ + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + fun initShader( + baseType: TurbulenceNoiseShader.Companion.Type, + config: TurbulenceNoiseAnimationConfig + ) { + noiseConfig = config + if (turbulenceNoiseShader == null || turbulenceNoiseShader?.baseType != baseType) { + turbulenceNoiseShader = TurbulenceNoiseShader(baseType) + + paint.shader = turbulenceNoiseShader!! + } + turbulenceNoiseShader!!.applyConfig(config) + } + + /** Sets the blend mode of the View. */ + fun setBlendMode(blendMode: BlendMode) { + paint.blendMode = blendMode + } + + internal fun clearConfig() { + noiseConfig = null + } +} diff --git a/systemUIAnim/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt b/systemUIAnim/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt new file mode 100644 index 0000000000..7ed3b87f68 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.surfaceeffects.utils + +/** Copied from android.utils.MathUtils */ +object MathUtils { + fun lerp(start: Float, stop: Float, amount: Float): Float { + return start + (stop - start) * amount + } +} diff --git a/systemUIAnim/src/com/android/systemui/util/AnimatorExtensions.kt b/systemUIAnim/src/com/android/systemui/util/AnimatorExtensions.kt new file mode 100644 index 0000000000..35dbb89ad8 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/util/AnimatorExtensions.kt @@ -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 com.android.systemui.util + +import androidx.core.animation.Animator + +/** + * Add an action which will be invoked when the animation has ended. + * + * @return the [Animator.AnimatorListener] added to the Animator + * @see Animator.end + */ +inline fun Animator.doOnEnd( + crossinline action: (animator: Animator) -> Unit +): Animator.AnimatorListener = addListener(onEnd = action) + +/** + * Add an action which will be invoked when the animation has started. + * + * @return the [Animator.AnimatorListener] added to the Animator + * @see Animator.start + */ +inline fun Animator.doOnStart( + crossinline action: (animator: Animator) -> Unit +): Animator.AnimatorListener = addListener(onStart = action) + +/** + * Add an action which will be invoked when the animation has been cancelled. + * + * @return the [Animator.AnimatorListener] added to the Animator + * @see Animator.cancel + */ +inline fun Animator.doOnCancel( + crossinline action: (animator: Animator) -> Unit +): Animator.AnimatorListener = addListener(onCancel = action) + +/** + * Add an action which will be invoked when the animation has repeated. + * + * @return the [Animator.AnimatorListener] added to the Animator + */ +inline fun Animator.doOnRepeat( + crossinline action: (animator: Animator) -> Unit +): Animator.AnimatorListener = addListener(onRepeat = action) + +/** + * Add a listener to this Animator using the provided actions. + * + * @return the [Animator.AnimatorListener] added to the Animator + */ +inline fun Animator.addListener( + crossinline onEnd: (animator: Animator) -> Unit = {}, + crossinline onStart: (animator: Animator) -> Unit = {}, + crossinline onCancel: (animator: Animator) -> Unit = {}, + crossinline onRepeat: (animator: Animator) -> Unit = {} +): Animator.AnimatorListener { + val listener = + object : Animator.AnimatorListener { + override fun onAnimationRepeat(animator: Animator) = onRepeat(animator) + override fun onAnimationEnd(animator: Animator) = onEnd(animator) + override fun onAnimationCancel(animator: Animator) = onCancel(animator) + override fun onAnimationStart(animator: Animator) = onStart(animator) + } + addListener(listener) + return listener +} diff --git a/systemUIAnim/src/com/android/systemui/util/Dialog.kt b/systemUIAnim/src/com/android/systemui/util/Dialog.kt new file mode 100644 index 0000000000..9dd23289d8 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/util/Dialog.kt @@ -0,0 +1,162 @@ +/* + * 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.systemui.util + +import android.app.Dialog +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import android.window.OnBackInvokedDispatcher +import com.android.systemui.animation.back.BackAnimationSpec +import com.android.systemui.animation.back.BackTransformation +import com.android.systemui.animation.back.applyTo +import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi +import com.android.systemui.animation.back.onBackAnimationCallbackFrom +import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached +import com.android.systemui.animation.view.LaunchableFrameLayout + +/** + * Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec]. + * The [BackTransformation] will be applied on the [targetView]. + */ +@JvmOverloads +fun Dialog.registerAnimationOnBackInvoked( + targetView: View, + backAnimationSpec: BackAnimationSpec = + BackAnimationSpec.floatingSystemSurfacesForSysUi( + displayMetricsProvider = { targetView.resources.displayMetrics }, + ), +) { + targetView.registerOnBackInvokedCallbackOnViewAttached( + onBackInvokedDispatcher = onBackInvokedDispatcher, + onBackAnimationCallback = + onBackAnimationCallbackFrom( + backAnimationSpec = backAnimationSpec, + displayMetrics = targetView.resources.displayMetrics, + onBackProgressed = { backTransformation -> backTransformation.applyTo(targetView) }, + onBackInvoked = { dismiss() }, + ), + ) +} + +/** + * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate + * outside its bounds. No-op if the dialog is already fullscreen. + * + *

Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view + * and a layout listener. The new view matches the original dialog DecorView in size, position, and + * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The + * layout listener is listening to changes to the modified DecorView. It is the responsibility of + * the caller to deregister the listener when the dialog is dismissed. + */ +fun Dialog.maybeForceFullscreen(): Pair? { + // Create the dialog so that its onCreate() method is called, which usually sets the dialog + // content. + create() + + val window = window!! + val decorView = window.decorView as ViewGroup + + val isWindowFullscreen = + window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT + if (isWindowFullscreen) { + return null + } + + // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible + // to animate outside its bounds. + // + // Before that, we add a new View as a child of the DecorView with the same size and gravity as + // that DecorView, then we add all original children of the DecorView to that new View. Finally + // we remove the background of the DecorView and add it to the new View, then we make the + // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window. + // + // On top of that, we also add a fullscreen transparent background between the DecorView and the + // view that we added so that we can dismiss the dialog when this view is clicked. This is + // necessary because DecorView overrides onTouchEvent and therefore we can't set the click + // listener directly on the (now fullscreen) DecorView. + val fullscreenTransparentBackground = FrameLayout(context) + decorView.addView( + fullscreenTransparentBackground, + 0 /* index */, + FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + ) + + val dialogContentWithBackground = LaunchableFrameLayout(context) + dialogContentWithBackground.background = decorView.background + + // Make the window background transparent. Note that setting the window (or DecorView) + // background drawable to null leads to issues with background color (not being transparent) or + // with insets that are not refreshed. Therefore we need to set it to something not null, hence + // we are using android.R.color.transparent here. + window.setBackgroundDrawableResource(android.R.color.transparent) + + // Close the dialog when clicking outside of it. + fullscreenTransparentBackground.setOnClickListener { dismiss() } + dialogContentWithBackground.isClickable = true + + // Make sure the transparent and dialog backgrounds are not focusable by accessibility + // features. + fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + fullscreenTransparentBackground.addView( + dialogContentWithBackground, + FrameLayout.LayoutParams( + window.attributes.width, + window.attributes.height, + window.attributes.gravity + ) + ) + + // Move all original children of the DecorView to the new View we just added. + for (i in 1 until decorView.childCount) { + val view = decorView.getChildAt(1) + decorView.removeViewAt(1) + dialogContentWithBackground.addView(view) + } + + // Make the window fullscreen and add a layout listener to ensure it stays fullscreen. + window.setLayout(MATCH_PARENT, MATCH_PARENT) + val decorViewLayoutListener = + View.OnLayoutChangeListener { + v, + left, + top, + right, + bottom, + oldLeft, + oldTop, + oldRight, + oldBottom -> + if ( + window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT + ) { + // The dialog size changed, copy its size to dialogContentWithBackground and make + // the dialog window full screen again. + val layoutParams = dialogContentWithBackground.layoutParams + layoutParams.width = window.attributes.width + layoutParams.height = window.attributes.height + dialogContentWithBackground.layoutParams = layoutParams + window.setLayout(MATCH_PARENT, MATCH_PARENT) + } + } + decorView.addOnLayoutChangeListener(decorViewLayoutListener) + + return dialogContentWithBackground to decorViewLayoutListener +} diff --git a/systemUIAnim/src/com/android/systemui/util/Dimension.kt b/systemUIAnim/src/com/android/systemui/util/Dimension.kt new file mode 100644 index 0000000000..4bc9972dd5 --- /dev/null +++ b/systemUIAnim/src/com/android/systemui/util/Dimension.kt @@ -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. + */ + +package com.android.systemui.util + +import android.content.Context +import android.content.res.Resources +import android.util.DisplayMetrics +import android.util.TypedValue + +/** Convert [this] number of dps to device pixels. */ +fun Number.dpToPx(context: Context): Float = dpToPx(resources = context.resources) + +/** Convert [this] number of dps to device pixels. */ +fun Number.dpToPx(resources: Resources): Float = dpToPx(displayMetrics = resources.displayMetrics) + +/** Convert [this] number of dps to device pixels. */ +fun Number.dpToPx(displayMetrics: DisplayMetrics): Float = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, toFloat(), displayMetrics) diff --git a/systemUICommon/.gitignore b/systemUICommon/.gitignore new file mode 100644 index 0000000000..f9a33dbbcc --- /dev/null +++ b/systemUICommon/.gitignore @@ -0,0 +1,9 @@ +.idea/ +.gradle/ +gradle/ +build/ +gradlew* +local.properties +*.iml +android.properties +buildSrc \ No newline at end of file diff --git a/systemUICommon/Android.bp b/systemUICommon/Android.bp new file mode 100644 index 0000000000..6fc13d7e0d --- /dev/null +++ b/systemUICommon/Android.bp @@ -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 { + default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_", + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + + name: "SystemUICommon", + use_resource_processor: true, + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + + static_libs: [ + "androidx.core_core-ktx", + ], + + manifest: "AndroidManifest.xml", + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/systemUICommon/AndroidManifest.xml b/systemUICommon/AndroidManifest.xml new file mode 100644 index 0000000000..e07df26b5a --- /dev/null +++ b/systemUICommon/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/systemUICommon/OWNERS b/systemUICommon/OWNERS new file mode 100644 index 0000000000..9b8a79e6f3 --- /dev/null +++ b/systemUICommon/OWNERS @@ -0,0 +1,2 @@ +darrellshi@google.com +evanlaird@google.com diff --git a/systemUICommon/README.md b/systemUICommon/README.md new file mode 100644 index 0000000000..1cc5277aa8 --- /dev/null +++ b/systemUICommon/README.md @@ -0,0 +1,5 @@ +# SystemUICommon + +`SystemUICommon` is a module within SystemUI that hosts standalone helper libraries. It is intended to be used by other modules, and therefore should not have other SystemUI dependencies to avoid circular dependencies. + +To maintain the structure of this module, please refrain from adding components at the top level. Instead, add them to specific sub-packages, such as `systemui/common/buffer/`. This will help to keep the module organized and easy to navigate. diff --git a/systemUICommon/build.gradle b/systemUICommon/build.gradle new file mode 100644 index 0000000000..38842570a7 --- /dev/null +++ b/systemUICommon/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace "com.android.systemui.common" + + sourceSets { + main { + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + } + } +} + +addFrameworkJar('framework-15.jar') diff --git a/systemUICommon/src/com/android/systemui/common/buffer/RingBuffer.kt b/systemUICommon/src/com/android/systemui/common/buffer/RingBuffer.kt new file mode 100644 index 0000000000..4734a3887f --- /dev/null +++ b/systemUICommon/src/com/android/systemui/common/buffer/RingBuffer.kt @@ -0,0 +1,118 @@ +/* + * 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.systemui.common.buffer + +import kotlin.math.max + +/** + * A simple ring buffer of recycled items + * + * Use [advance] to add items to the buffer. + * + * As the buffer is used, it will grow, allocating new instances of T using [factory] until it + * reaches [maxSize]. After this point, no new instances will be created. Instead, calls to + * [advance] will recycle the "oldest" instance from the start of the buffer, placing it at the end. + * + * The items in the buffer are "recycled" in that they are reused, but it is up to the caller of + * [advance] to properly reset any data that was previously stored on those items. + * + * @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring. + * @param factory A function that creates a fresh instance of T. Used by the buffer while it's + * growing to [maxSize]. + */ +class RingBuffer(private val maxSize: Int, private val factory: () -> T) : Iterable { + + private val buffer = MutableList(maxSize) { null } + + /** + * An abstract representation that points to the "end" of the buffer, i.e. one beyond the + * location of the last item. Increments every time [advance] is called and is never wrapped. + * + * Use [indexOf] to calculate the associated index into the backing array. Before the buffer has + * been completely filled, this will point to the next empty slot to fill; afterwards it will + * point to the next item that should be recycled (which, because the buffer is a ring, is the + * "start" of the buffer). + * + * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms, + * omega will overflow after a little under three million years of continuous operation. + */ + private var omega: Long = 0 + + /** + * The number of items currently stored in the buffer. Calls to [advance] will cause this value + * to increase by one until it reaches [maxSize]. + */ + val size: Int + get() = if (omega < maxSize) omega.toInt() else maxSize + + /** + * Adds an item to the end of the buffer. The caller should reset the returned item's contents + * and then fill it with appropriate data. + * + * If the buffer is not yet full, uses [factory] to create a new item. Otherwise, it recycles + * the oldest item from the front of the buffer and moves it to the end. + * + * Importantly, recycled items are returned as-is, without being reset. They will retain any + * data that was previously stored on them. Callers must make sure to clear any historical data, + * if necessary. + */ + fun advance(): T { + val index = indexOf(omega) + omega += 1 + val entry = buffer[index] ?: factory().also { buffer[index] = it } + return entry + } + + /** + * Returns the value stored at [index], which can range from 0 (the "start", or oldest element + * of the buffer) to [size] - 1 (the "end", or newest element of the buffer). + */ + operator fun get(index: Int): T { + if (index < 0 || index >= size) { + throw IndexOutOfBoundsException("Index $index is out of bounds") + } + + // If omega is larger than the maxSize, then the buffer is full, and omega is equivalent + // to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is + // not yet full and our start should be 0. However, in modspace, maxSize and 0 are + // equivalent, so we can get away with using it as the start value instead. + val start = max(omega, maxSize.toLong()) + + return buffer[indexOf(start + index)]!! + } + + override fun iterator(): Iterator { + return object : Iterator { + private var position: Int = 0 + + override fun next(): T { + if (position >= size) { + throw NoSuchElementException() + } + return get(position).also { position += 1 } + } + + override fun hasNext(): Boolean { + return position < size + } + } + } + + private fun indexOf(position: Long): Int { + return (position % maxSize).toInt() + } +} diff --git a/systemUILog/.gitignore b/systemUILog/.gitignore new file mode 100644 index 0000000000..f9a33dbbcc --- /dev/null +++ b/systemUILog/.gitignore @@ -0,0 +1,9 @@ +.idea/ +.gradle/ +gradle/ +build/ +gradlew* +local.properties +*.iml +android.properties +buildSrc \ No newline at end of file diff --git a/systemUILog/Android.bp b/systemUILog/Android.bp new file mode 100644 index 0000000000..627ac4b7c3 --- /dev/null +++ b/systemUILog/Android.bp @@ -0,0 +1,38 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUILogLib", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "androidx.core_core-ktx", + "androidx.annotation_annotation", + "error_prone_annotations", + "SystemUICommon", + ], + manifest: "AndroidManifest.xml", + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/systemUILog/AndroidManifest.xml b/systemUILog/AndroidManifest.xml new file mode 100644 index 0000000000..4021e1a5f7 --- /dev/null +++ b/systemUILog/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/systemUILog/build.gradle b/systemUILog/build.gradle new file mode 100644 index 0000000000..c1c63a67bb --- /dev/null +++ b/systemUILog/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace "com.android.systemui.log" + buildFeatures { + aidl true + } + sourceSets { + main { + java.srcDirs = ['src'] + aidl.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + } + } +} + +addFrameworkJar('framework-15.jar') +compileOnlyCommonJars() + +dependencies { + compileOnly projects.systemUIPluginCore + compileOnly projects.systemUICommon + implementation 'com.google.errorprone:error_prone_annotations:2.33.0' +} diff --git a/systemUILog/src/com/android/systemui/log/ConstantStringsLogger.kt b/systemUILog/src/com/android/systemui/log/ConstantStringsLogger.kt new file mode 100644 index 0000000000..bc35095a34 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/ConstantStringsLogger.kt @@ -0,0 +1,33 @@ +/* + * 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.systemui.log + +import com.google.errorprone.annotations.CompileTimeConstant + +/** + * Handy for adding basic logging with CompileTimeConstant strings - so logging with no variables. + * Most likely you want to delegate it to [ConstantStringsLoggerImpl]. + */ +interface ConstantStringsLogger { + fun v(@CompileTimeConstant msg: String) + + fun d(@CompileTimeConstant msg: String) + + fun w(@CompileTimeConstant msg: String) + + fun e(@CompileTimeConstant msg: String) +} diff --git a/systemUILog/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt b/systemUILog/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt new file mode 100644 index 0000000000..a4f4e13473 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt @@ -0,0 +1,30 @@ +/* + * 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.systemui.log + +import com.android.systemui.log.core.LogLevel +import com.google.errorprone.annotations.CompileTimeConstant + +class ConstantStringsLoggerImpl(val buffer: LogBuffer, val tag: String) : ConstantStringsLogger { + override fun v(@CompileTimeConstant msg: String) = buffer.log(tag, LogLevel.VERBOSE, msg) + + override fun d(@CompileTimeConstant msg: String) = buffer.log(tag, LogLevel.DEBUG, msg) + + override fun w(@CompileTimeConstant msg: String) = buffer.log(tag, LogLevel.WARNING, msg) + + override fun e(@CompileTimeConstant msg: String) = buffer.log(tag, LogLevel.ERROR, msg) +} diff --git a/systemUILog/src/com/android/systemui/log/LogBuffer.kt b/systemUILog/src/com/android/systemui/log/LogBuffer.kt new file mode 100644 index 0000000000..4b5e9de2cc --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/LogBuffer.kt @@ -0,0 +1,267 @@ +/* + * 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.systemui.log + +import android.os.Trace +import android.util.Log +import com.android.systemui.common.buffer.RingBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.core.LogMessage +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.log.core.MessageInitializer +import com.android.systemui.log.core.MessagePrinter +import com.google.errorprone.annotations.CompileTimeConstant +import java.io.PrintWriter +import kotlin.math.max + +/** + * A simple ring buffer of recyclable log messages + * + * The goal of this class is to enable logging that is both extremely chatty and extremely + * lightweight. If done properly, logging a message will not result in any heap allocations or + * string generation. Messages are only converted to strings if the log is actually dumped (usually + * as the result of taking a bug report). + * + * You can dump the entire buffer at any time by running: + * ``` + * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService + * ``` + * + * ...where `bufferName` is the (case-sensitive) [name] passed to the constructor. + * + * By default, only messages of WARN level or higher are echoed to logcat, but this can be adjusted + * locally (usually for debugging purposes). + * + * To enable logcat echoing for an entire buffer: + * ``` + * $ adb shell cmd statusbar echo -b : + * ``` + * + * To enable logcat echoing for a specific tag: + * ``` + * $ adb shell cmd statusbar echo -t : + * ``` + * + * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or + * the first letter of any of the previous. + * + * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI + * LogBufferFactory. + * + * @param name The name of this buffer, printed when the buffer is dumped and in some other + * situations. + * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start + * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches + * the maximum, it behaves like a ring buffer. + */ +class LogBuffer +@JvmOverloads +constructor( + private val name: String, + private val maxSize: Int, + private val logcatEchoTracker: LogcatEchoTracker, + private val systrace: Boolean = true, +) : MessageBuffer { + private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() } + + var frozen = false + private set + + private val mutable + get() = !frozen && maxSize > 0 + + /** + * Logs a message to the log buffer + * + * May also log the message to logcat if echoing is enabled for this buffer or tag. + * + * The actual string of the log message is not constructed until it is needed. To accomplish + * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is + * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data + * on the message's fields. The message is then inserted into the buffer where it waits until it + * is either pushed out by newer messages or it needs to printed. If and when this latter moment + * occurs, the [messagePrinter] function is called on the message. It reads whatever data the + * initializer stored and converts it to a human-readable log message. + * + * @param tag A string of at most 23 characters, used for grouping logs into categories or + * subjects. If this message is echoed to logcat, this will be the tag that is used. + * @param level Which level to log the message at, both to the buffer and to logcat if it's + * echoed. In general, a module should split most of its logs into either INFO or DEBUG level. + * INFO level should be reserved for information that other parts of the system might care + * about, leaving the specifics of code's day-to-day operations to DEBUG. + * @param messageInitializer A function that will be called immediately to store relevant data + * on the log message. The value of `this` will be the LogMessage to be initialized. + * @param messagePrinter A function that will be called if and when the message needs to be + * dumped to logcat or a bug report. It should read the data stored by the initializer and + * convert it to a human-readable string. The value of `this` will be the LogMessage to be + * printed. **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and + * NEVER any variables in its enclosing scope. Otherwise, the runtime will need to allocate a + * new instance of the printer for each call, thwarting our attempts at avoiding any sort of + * allocation. + * @param exception Provide any exception that need to be logged. This is saved as + * [LogMessage.exception] + */ + @JvmOverloads + inline fun log( + tag: String, + level: LogLevel, + messageInitializer: MessageInitializer, + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + ) { + val message = obtain(tag, level, messagePrinter, exception) + messageInitializer(message) + commit(message) + } + + /** + * Logs a compile-time string constant [message] to the log buffer. Use sparingly. + * + * May also log the message to logcat if echoing is enabled for this buffer or tag. This is for + * simpler use-cases where [message] is a compile time string constant. For use-cases where the + * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in + * an initializer and a message printer. + * + * Log buffers are limited by the number of entries, so logging more frequently will limit the + * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a + * bug report more actionable, so using the [log] with a messagePrinter to add more detail to + * every log may do more to improve overall logging than adding more logs with this method. + */ + @JvmOverloads + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(tag, level, { str1 = message }, { str1!! }, exception) + + /** + * You should call [log] instead of this method. + * + * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size, + * grows the buffer by one. + * + * After calling [obtain], the message will now be at the end of the buffer. The caller must + * store any relevant data on the message and then call [commit]. + */ + @Synchronized + override fun obtain( + tag: String, + level: LogLevel, + messagePrinter: MessagePrinter, + exception: Throwable?, + ): LogMessage { + if (!mutable) { + return FROZEN_MESSAGE + } + val message = buffer.advance() + message.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception) + return message + } + + /** + * You should call [log] instead of this method. + * + * After acquiring a message via [obtain], call this method to signal to the buffer that you + * have finished filling in its data fields. The message will be echoed to logcat if necessary. + */ + @Synchronized + override fun commit(message: LogMessage) { + if (!mutable) { + return + } + echoToDesiredEndpoints(message) + } + + /** Sends message to echo after determining whether to use Logcat and/or systrace. */ + private fun echoToDesiredEndpoints(message: LogMessage) { + val includeInLogcat = + logcatEchoTracker.isBufferLoggable(name, message.level) || + logcatEchoTracker.isTagLoggable(message.tag, message.level) + + val includeInSystrace = systrace && Trace.isTagEnabled(Trace.TRACE_TAG_APP) + + if (includeInLogcat || includeInSystrace) { + val strMessage = message.messagePrinter(message) + if (includeInLogcat) { + echoToLogcat(message, strMessage) + } + if (includeInSystrace) { + echoToSystrace(message.level, message.tag, strMessage) + } + } + } + + /** Converts the entire buffer to a newline-delimited string */ + @Synchronized + fun dump(pw: PrintWriter, tailLength: Int) { + val iterationStart = + if (tailLength <= 0) { + 0 + } else { + max(0, buffer.size - tailLength) + } + + for (i in iterationStart until buffer.size) { + buffer[i].dump(pw) + } + } + + /** + * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls + * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if + * necessary. + */ + @Synchronized + fun freeze() { + if (!frozen) { + log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 frozen" }) + frozen = true + } + } + + /** Undoes the effects of calling [freeze]. */ + @Synchronized + fun unfreeze() { + if (frozen) { + frozen = false + log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 unfrozen" }) + } + } + + private fun echoToSystrace(level: LogLevel, tag: String, strMessage: String) { + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + "UI Events", + "$name - ${level.shortString} $tag: $strMessage" + ) + } + + private fun echoToLogcat(message: LogMessage, strMessage: String) { + when (message.level) { + LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception) + LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception) + LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception) + LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception) + LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception) + LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception) + } + } +} + +private const val TAG = "LogBuffer" +private val FROZEN_MESSAGE = LogMessageImpl.create() diff --git a/systemUILog/src/com/android/systemui/log/LogMessageImpl.kt b/systemUILog/src/com/android/systemui/log/LogMessageImpl.kt new file mode 100644 index 0000000000..33cc199e71 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/LogMessageImpl.kt @@ -0,0 +1,96 @@ +/* + * 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.systemui.log + +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.core.LogMessage +import com.android.systemui.log.core.MessagePrinter + +/** Recyclable implementation of [LogMessage]. */ +data class LogMessageImpl( + override var level: LogLevel, + override var tag: String, + override var timestamp: Long, + override var messagePrinter: MessagePrinter, + override var exception: Throwable?, + override var str1: String?, + override var str2: String?, + override var str3: String?, + override var int1: Int, + override var int2: Int, + override var long1: Long, + override var long2: Long, + override var double1: Double, + override var bool1: Boolean, + override var bool2: Boolean, + override var bool3: Boolean, + override var bool4: Boolean, +) : LogMessage { + + fun reset( + tag: String, + level: LogLevel, + timestamp: Long, + renderer: MessagePrinter, + exception: Throwable? = null, + ) { + this.level = level + this.tag = tag + this.timestamp = timestamp + this.messagePrinter = renderer + this.exception = exception + str1 = null + str2 = null + str3 = null + int1 = 0 + int2 = 0 + long1 = 0 + long2 = 0 + double1 = 0.0 + bool1 = false + bool2 = false + bool3 = false + bool4 = false + } + + companion object Factory { + fun create(): LogMessageImpl { + return LogMessageImpl( + LogLevel.DEBUG, + DEFAULT_TAG, + 0, + DEFAULT_PRINTER, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0.0, + false, + false, + false, + false + ) + } + } +} + +private const val DEFAULT_TAG = "UnknownTag" +private val DEFAULT_PRINTER: MessagePrinter = { "Unknown message: $this" } diff --git a/systemUILog/src/com/android/systemui/log/LogcatEchoTracker.kt b/systemUILog/src/com/android/systemui/log/LogcatEchoTracker.kt new file mode 100644 index 0000000000..4410e5dda5 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/LogcatEchoTracker.kt @@ -0,0 +1,28 @@ +/* + * 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.systemui.log + +import com.android.systemui.log.core.LogLevel + +/** Keeps track of which [LogBuffer] messages should also appear in logcat. */ +interface LogcatEchoTracker { + /** Whether [bufferName] should echo messages of [level] or higher to logcat. */ + fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean + + /** Whether [tagName] should echo messages of [level] or higher to logcat. */ + fun isTagLoggable(tagName: String, level: LogLevel): Boolean +} diff --git a/systemUILog/src/com/android/systemui/log/core/LogLevel.kt b/systemUILog/src/com/android/systemui/log/core/LogLevel.kt new file mode 100644 index 0000000000..d30d8e9fe0 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/core/LogLevel.kt @@ -0,0 +1,29 @@ +/* + * 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.systemui.log.core + +import android.util.Log + +/** Enum version of @Log.Level */ +enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) { + VERBOSE(Log.VERBOSE, "V"), + DEBUG(Log.DEBUG, "D"), + INFO(Log.INFO, "I"), + WARNING(Log.WARN, "W"), + ERROR(Log.ERROR, "E"), + WTF(Log.ASSERT, "WTF") +} diff --git a/systemUILog/src/com/android/systemui/log/core/LogMessage.kt b/systemUILog/src/com/android/systemui/log/core/LogMessage.kt new file mode 100644 index 0000000000..3bd6473738 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/core/LogMessage.kt @@ -0,0 +1,101 @@ +/* + * 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.systemui.log.core + +import android.icu.text.SimpleDateFormat +import java.io.PrintWriter +import java.util.Locale + +/** + * Generic data class for storing messages logged to a [LogBuffer] + * + * Each LogMessage has a few standard fields ([level], [tag], and [timestamp]). The rest are generic + * data slots that may or may not be used, depending on the nature of the specific message being + * logged. + * + * When a message is logged, the code doing the logging stores data in one or more of the generic + * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the + * [messagePrinter] function reads the data stored in the generic fields and converts that to a + * human- readable string. Thus, for every log type there must be a specialized initializer function + * that stores data specific to that log type and a specialized printer function that prints that + * data. + * + * See [LogBuffer.log] for more information. + */ +interface LogMessage { + val level: LogLevel + val tag: String + val timestamp: Long + val messagePrinter: MessagePrinter + val exception: Throwable? + + var str1: String? + var str2: String? + var str3: String? + var int1: Int + var int2: Int + var long1: Long + var long2: Long + var double1: Double + var bool1: Boolean + var bool2: Boolean + var bool3: Boolean + var bool4: Boolean + + /** Function that dumps the [LogMessage] to the provided [writer]. */ + fun dump(writer: PrintWriter) { + val formattedTimestamp = DATE_FORMAT.format(timestamp) + val shortLevel = level.shortString + val messageToPrint = messagePrinter(this) + printLikeLogcat(writer, formattedTimestamp, shortLevel, tag, messageToPrint) + exception?.printStackTrace(writer) + } +} + +/** + * A function that will be called immediately to store relevant data on the log message. The value + * of `this` will be the LogMessage to be initialized. + */ +typealias MessageInitializer = LogMessage.() -> Unit + +/** + * A function that will be called if and when the message needs to be dumped to logcat or a bug + * report. It should read the data stored by the initializer and convert it to a human-readable + * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer + * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing + * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call, + * thwarting our attempts at avoiding any sort of allocation. + */ +typealias MessagePrinter = LogMessage.() -> String + +private fun printLikeLogcat( + pw: PrintWriter, + formattedTimestamp: String, + shortLogLevel: String, + tag: String, + message: String +) { + pw.print(formattedTimestamp) + pw.print(" ") + pw.print(shortLogLevel) + pw.print(" ") + pw.print(tag) + pw.print(": ") + pw.println(message) +} + +private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) diff --git a/systemUILog/src/com/android/systemui/log/core/LogcatOnlyMessageBuffer.kt b/systemUILog/src/com/android/systemui/log/core/LogcatOnlyMessageBuffer.kt new file mode 100644 index 0000000000..006b521ad8 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/core/LogcatOnlyMessageBuffer.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.systemui.log.core + +import android.util.Log +import com.android.systemui.log.LogMessageImpl + +/** + * A simple implementation of [MessageBuffer] that forwards messages to [android.util.Log] + * immediately. This defeats the intention behind [LogBuffer] and should only be used when + * [LogBuffer]s are unavailable in a certain context. + */ +class LogcatOnlyMessageBuffer( + val targetLogLevel: LogLevel, +) : MessageBuffer { + private val singleMessage = LogMessageImpl.Factory.create() + private var isObtained: Boolean = false + + @Synchronized + override fun obtain( + tag: String, + level: LogLevel, + messagePrinter: MessagePrinter, + exception: Throwable?, + ): LogMessage { + if (isObtained) { + throw UnsupportedOperationException( + "Message has already been obtained. Call order is incorrect." + ) + } + + singleMessage.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception) + isObtained = true + return singleMessage + } + + @Synchronized + override fun commit(message: LogMessage) { + if (singleMessage != message) { + throw IllegalArgumentException("Message argument is not the expected message.") + } + if (!isObtained) { + throw UnsupportedOperationException( + "Message has not been obtained. Call order is incorrect." + ) + } + + if (message.level >= targetLogLevel) { + val strMessage = message.messagePrinter(message) + when (message.level) { + LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception) + LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception) + LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception) + LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception) + LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception) + LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception) + } + } + + isObtained = false + } +} diff --git a/systemUILog/src/com/android/systemui/log/core/Logger.kt b/systemUILog/src/com/android/systemui/log/core/Logger.kt new file mode 100644 index 0000000000..5729ab2704 --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/core/Logger.kt @@ -0,0 +1,224 @@ +/* + * 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.systemui.log.core + +import com.google.errorprone.annotations.CompileTimeConstant + +/** Logs messages to the [MessageBuffer] with [tag]. */ +open class Logger(val buffer: MessageBuffer, val tag: String) { + /** + * Logs a message to the buffer. + * + * The actual string of the log message is not constructed until it is needed. To accomplish + * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is + * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data + * on the message's fields. The message is then inserted into the buffer where it waits until it + * is either pushed out by newer messages or it needs to printed. If and when this latter moment + * occurs, the [messagePrinter] function is called on the message. It reads whatever data the + * initializer stored and converts it to a human-readable log message. + * + * @param level Which level to log the message at, both to the buffer and to logcat if it's + * echoed. In general, a module should split most of its logs into either INFO or DEBUG level. + * INFO level should be reserved for information that other parts of the system might care + * about, leaving the specifics of code's day-to-day operations to DEBUG. + * @param messagePrinter A function that will be called if and when the message needs to be + * dumped to logcat or a bug report. It should read the data stored by the initializer and + * convert it to a human-readable string. The value of `this` will be the [LogMessage] to be + * printed. **IMPORTANT:** The printer should ONLY ever reference fields on the [LogMessage] + * and NEVER any variables in its enclosing scope. Otherwise, the runtime will need to + * allocate a new instance of the printer for each call, thwarting our attempts at avoiding + * any sort of allocation. + * @param exception Provide any exception that need to be logged. This is saved as + * [LogMessage.exception] + * @param messageInitializer A function that will be called immediately to store relevant data + * on the log message. The value of `this` will be the [LogMessage] to be initialized. + */ + @JvmOverloads + inline fun log( + level: LogLevel, + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + messageInitializer: MessageInitializer, + ) { + val message = buffer.obtain(tag, level, messagePrinter, exception) + messageInitializer(message) + buffer.commit(message) + } + + /** + * Logs a compile-time string constant [message] to the log buffer. Use sparingly. + * + * This is for simpler use-cases where [message] is a compile time string constant. For + * use-cases where the log message is built during runtime, use the [log] overloaded method that + * takes in an initializer and a message printer. + * + * Buffers are limited by the number of entries, so logging more frequently will limit the time + * window that the [MessageBuffer] covers in a bug report. Richer logs, on the other hand, make + * a bug report more actionable, so using the [log] with a [MessagePrinter] to add more details + * to every log may do more to improve overall logging than adding more logs with this method. + */ + @JvmOverloads + fun log( + level: LogLevel, + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(level, { str1!! }, exception) { str1 = message } + + /** + * Logs a message to the buffer at [LogLevel.VERBOSE]. + * + * @see log + */ + @JvmOverloads + inline fun v( + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + messageInitializer: MessageInitializer, + ) = log(LogLevel.VERBOSE, messagePrinter, exception, messageInitializer) + + /** + * Logs a compile-time string constant [message] to the log buffer at [LogLevel.VERBOSE]. Use + * sparingly. + * + * @see log + */ + @JvmOverloads + fun v( + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(LogLevel.VERBOSE, message, exception) + + /** + * Logs a message to the buffer at [LogLevel.DEBUG]. + * + * @see log + */ + @JvmOverloads + inline fun d( + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + messageInitializer: MessageInitializer, + ) = log(LogLevel.DEBUG, messagePrinter, exception, messageInitializer) + + /** + * Logs a compile-time string constant [message] to the log buffer at [LogLevel.DEBUG]. Use + * sparingly. + * + * @see log + */ + @JvmOverloads + fun d( + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(LogLevel.DEBUG, message, exception) + + /** + * Logs a message to the buffer at [LogLevel.INFO]. + * + * @see log + */ + @JvmOverloads + inline fun i( + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + messageInitializer: MessageInitializer, + ) = log(LogLevel.INFO, messagePrinter, exception, messageInitializer) + + /** + * Logs a compile-time string constant [message] to the log buffer at [LogLevel.INFO]. Use + * sparingly. + * + * @see log + */ + @JvmOverloads + fun i( + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(LogLevel.INFO, message, exception) + + /** + * Logs a message to the buffer at [LogLevel.WARNING]. + * + * @see log + */ + @JvmOverloads + inline fun w( + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + messageInitializer: MessageInitializer, + ) = log(LogLevel.WARNING, messagePrinter, exception, messageInitializer) + + /** + * Logs a compile-time string constant [message] to the log buffer at [LogLevel.WARNING]. Use + * sparingly. + * + * @see log + */ + @JvmOverloads + fun w( + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(LogLevel.WARNING, message, exception) + + /** + * Logs a message to the buffer at [LogLevel.ERROR]. + * + * @see log + */ + @JvmOverloads + inline fun e( + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + messageInitializer: MessageInitializer, + ) = log(LogLevel.ERROR, messagePrinter, exception, messageInitializer) + + /** + * Logs a compile-time string constant [message] to the log buffer at [LogLevel.ERROR]. Use + * sparingly. + * + * @see log + */ + @JvmOverloads + fun e( + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(LogLevel.ERROR, message, exception) + + /** + * Logs a message to the buffer at [LogLevel.WTF]. + * + * @see log + */ + @JvmOverloads + inline fun wtf( + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, + messageInitializer: MessageInitializer, + ) = log(LogLevel.WTF, messagePrinter, exception, messageInitializer) + + /** + * Logs a compile-time string constant [message] to the log buffer at [LogLevel.WTF]. Use + * sparingly. + * + * @see log + */ + @JvmOverloads + fun wtf( + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(LogLevel.WTF, message, exception) +} diff --git a/systemUILog/src/com/android/systemui/log/core/MessageBuffer.kt b/systemUILog/src/com/android/systemui/log/core/MessageBuffer.kt new file mode 100644 index 0000000000..bb91633c4d --- /dev/null +++ b/systemUILog/src/com/android/systemui/log/core/MessageBuffer.kt @@ -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.systemui.log.core + +/** + * [MessageBuffer] is an interface that represents a buffer of log messages, and provides methods to + * [obtain] a log message and [commit] it to the buffer. + */ +interface MessageBuffer { + /** + * Obtains the next [LogMessage] from the buffer. + * + * After calling [obtain], the caller must store any relevant data on the message and then call + * [commit]. + */ + fun obtain( + tag: String, + level: LogLevel, + messagePrinter: MessagePrinter, + exception: Throwable? = null, + ): LogMessage + + /** + * After acquiring a log message via [obtain], call this method to signal to the buffer that + * data fields have been filled. + */ + fun commit(message: LogMessage) +} diff --git a/systemUIPlugin/Android.bp b/systemUIPlugin/Android.bp new file mode 100644 index 0000000000..bb47a2f472 --- /dev/null +++ b/systemUIPlugin/Android.bp @@ -0,0 +1,62 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +java_library { + + name: "SystemUIPluginLib", + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + "bcsmartspace/src/**/*.java", + "bcsmartspace/src/**/*.kt", + ], + + // If you add a static lib here, you may need to also add the package to the ClassLoaderFilter + // in PluginInstance. That will ensure that loaded plugins have access to the related classes. + // You should also add it to proguard_common.flags so that proguard does not remove the portions + // of the library which are used by the plugins but not by systemui itself. + static_libs: [ + "androidx.annotation_annotation", + "PluginCoreLib", + "SystemUIAnimationLib", + "SystemUICommon", + "SystemUILogLib", + ], + +} + +android_app { + + // Dummy to generate .toc files. + name: "PluginDummyLib", + platform_apis: true, + srcs: ["src/**/*.java"], + + libs: ["SystemUIPluginLib"], + + optimize: { + enabled: false, + }, + +} diff --git a/systemUIPlugin/AndroidManifest.xml b/systemUIPlugin/AndroidManifest.xml new file mode 100644 index 0000000000..811595ade9 --- /dev/null +++ b/systemUIPlugin/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/systemUIPlugin/ExamplePlugin/Android.bp b/systemUIPlugin/ExamplePlugin/Android.bp new file mode 100644 index 0000000000..f6fa6a5998 --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/Android.bp @@ -0,0 +1,26 @@ +package { + default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_", + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_app { + + name: "ExamplePlugin", + use_resource_processor: true, + + libs: ["SystemUIPluginLib"], + + certificate: "platform", + optimize: { + enabled: false, + }, + + srcs: ["src/**/*.java"], + + platform_apis: true, +} diff --git a/systemUIPlugin/ExamplePlugin/AndroidManifest.xml b/systemUIPlugin/ExamplePlugin/AndroidManifest.xml new file mode 100644 index 0000000000..e9e844124e --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/systemUIPlugin/ExamplePlugin/res/layout/colored_overlay.xml b/systemUIPlugin/ExamplePlugin/res/layout/colored_overlay.xml new file mode 100644 index 0000000000..b2910cb19b --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/res/layout/colored_overlay.xml @@ -0,0 +1,22 @@ + + + + diff --git a/systemUIPlugin/ExamplePlugin/res/layout/plugin_settings.xml b/systemUIPlugin/ExamplePlugin/res/layout/plugin_settings.xml new file mode 100644 index 0000000000..eb90283f08 --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/res/layout/plugin_settings.xml @@ -0,0 +1,24 @@ + + + + diff --git a/systemUIPlugin/ExamplePlugin/res/values/strings.xml b/systemUIPlugin/ExamplePlugin/res/values/strings.xml new file mode 100644 index 0000000000..a0bfe849e5 --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/res/values/strings.xml @@ -0,0 +1,24 @@ + + + + + Plugin settings go here + Overlay Plugin + + diff --git a/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java b/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java new file mode 100644 index 0000000000..5fdbbf989b --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugin.testoverlayplugin; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +/** + * View with some logging to show that its being run. + */ +public class CustomView extends View { + + private static final String TAG = "CustomView"; + + public CustomView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + Log.d(TAG, "new instance"); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + Log.d(TAG, "onAttachedToWindow"); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + Log.d(TAG, "onDetachedFromWindow"); + } +} diff --git a/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/PluginSettings.java b/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/PluginSettings.java new file mode 100644 index 0000000000..cf39075d95 --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/PluginSettings.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugin.testoverlayplugin; + +import android.annotation.Nullable; +import android.app.Activity; +import android.os.Bundle; + +/** + * DO NOT Reference Plugin interfaces here, this runs in the plugin APK's process + * and is only for modifying settings. + */ +public class PluginSettings extends Activity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.plugin_settings); + } +} diff --git a/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java b/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java new file mode 100644 index 0000000000..4a0626c383 --- /dev/null +++ b/systemUIPlugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugin.testoverlayplugin; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.InternalInsetsInfo; +import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; + +import com.android.systemui.plugins.OverlayPlugin; +import com.android.systemui.plugins.annotations.Requires; + +@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION) +public class SampleOverlayPlugin implements OverlayPlugin { + private static final String TAG = "SampleOverlayPlugin"; + private Context mPluginContext; + + private View mStatusBarView; + private View mNavBarView; + private boolean mInputSetup; + private boolean mCollapseDesired; + private float mStatusBarHeight; + + @Override + public void onCreate(Context sysuiContext, Context pluginContext) { + Log.d(TAG, "onCreate"); + mPluginContext = pluginContext; + } + + @Override + public void onDestroy() { + if (mInputSetup) { + mStatusBarView.getViewTreeObserver().removeOnComputeInternalInsetsListener( + onComputeInternalInsetsListener); + } + Log.d(TAG, "onDestroy"); + if (mStatusBarView != null) { + mStatusBarView.post( + () -> ((ViewGroup) mStatusBarView.getParent()).removeView(mStatusBarView)); + } + if (mNavBarView != null) { + mNavBarView.post(() -> ((ViewGroup) mNavBarView.getParent()).removeView(mNavBarView)); + } + } + + @Override + public void setup(View statusBar, View navBar) { + Log.d(TAG, "Setup"); + + int id = mPluginContext.getResources().getIdentifier("status_bar_height", "dimen", + "android"); + mStatusBarHeight = mPluginContext.getResources().getDimension(id); + if (statusBar instanceof ViewGroup) { + mStatusBarView = LayoutInflater.from(mPluginContext) + .inflate(R.layout.colored_overlay, (ViewGroup) statusBar, false); + ((ViewGroup) statusBar).addView(mStatusBarView); + } + if (navBar instanceof ViewGroup) { + mNavBarView = LayoutInflater.from(mPluginContext) + .inflate(R.layout.colored_overlay, (ViewGroup) navBar, false); + ((ViewGroup) navBar).addView(mNavBarView); + } + } + + @Override + public void setCollapseDesired(boolean collapseDesired) { + mCollapseDesired = collapseDesired; + } + + @Override + public boolean holdStatusBarOpen() { + if (!mInputSetup) { + mInputSetup = true; + mStatusBarView.getViewTreeObserver().addOnComputeInternalInsetsListener( + onComputeInternalInsetsListener); + } + return true; + } + + final OnComputeInternalInsetsListener onComputeInternalInsetsListener = inoutInfo -> { + inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + if (mCollapseDesired) { + inoutInfo.touchableRegion.set(new Rect(0, 0, 50000, (int) mStatusBarHeight)); + } else { + inoutInfo.touchableRegion.set(new Rect(0, 0, 50000, 50000)); + } + }; +} diff --git a/systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt b/systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt new file mode 100644 index 0000000000..509f022310 --- /dev/null +++ b/systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt @@ -0,0 +1,24 @@ +/* + * 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.systemui.plugins + +// TODO(b/265360975): Evaluate this plugin approach. +/** Plugin to provide BC smartspace configuration */ +interface BcSmartspaceConfigPlugin { + /** Gets default date/weather disabled status. */ + val isDefaultDateWeatherDisabled: Boolean +} diff --git a/systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java new file mode 100644 index 0000000000..9ad4012cfd --- /dev/null +++ b/systemUIPlugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java @@ -0,0 +1,246 @@ +/* + * 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.systemui.plugins; + +import android.app.PendingIntent; +import android.app.smartspace.SmartspaceAction; +import android.app.smartspace.SmartspaceTarget; +import android.app.smartspace.SmartspaceTargetEvent; +import android.app.smartspace.uitemplatedata.TapAction; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Parcelable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +import java.util.List; + +/** + * Interface to provide SmartspaceTargets to BcSmartspace. + */ +@ProvidesInterface(action = BcSmartspaceDataPlugin.ACTION, version = BcSmartspaceDataPlugin.VERSION) +public interface BcSmartspaceDataPlugin extends Plugin { + String UI_SURFACE_LOCK_SCREEN_AOD = "lockscreen"; + String UI_SURFACE_HOME_SCREEN = "home"; + String UI_SURFACE_MEDIA = "media_data_manager"; + String UI_SURFACE_DREAM = "dream"; + String UI_SURFACE_GLANCEABLE_HUB = "glanceable_hub"; + + String ACTION = "com.android.systemui.action.PLUGIN_BC_SMARTSPACE_DATA"; + int VERSION = 1; + String TAG = "BcSmartspaceDataPlugin"; + + /** Register a listener to get Smartspace data. */ + default void registerListener(SmartspaceTargetListener listener) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** Unregister a listener. */ + default void unregisterListener(SmartspaceTargetListener listener) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** Register a SmartspaceEventNotifier. */ + default void registerSmartspaceEventNotifier(SmartspaceEventNotifier notifier) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** Push a SmartspaceTargetEvent to the SmartspaceEventNotifier. */ + default void notifySmartspaceEvent(SmartspaceTargetEvent event) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** Allows for notifying the SmartspaceSession of SmartspaceTargetEvents. */ + interface SmartspaceEventNotifier { + /** Pushes a given SmartspaceTargetEvent to the SmartspaceSession. */ + void notifySmartspaceEvent(SmartspaceTargetEvent event); + } + + /** + * Create a view to be shown within the parent. Do not add the view, as the parent + * will be responsible for correctly setting the LayoutParams + */ + default SmartspaceView getView(ViewGroup parent) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** + * As the smartspace view becomes available, allow listeners to receive an event. + */ + default void addOnAttachStateChangeListener(View.OnAttachStateChangeListener listener) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** Updates Smartspace data and propagates it to any listeners. */ + default void onTargetsAvailable(List targets) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** Provides Smartspace data to registered listeners. */ + interface SmartspaceTargetListener { + /** Each Parcelable is a SmartspaceTarget that represents a card. */ + void onSmartspaceTargetsUpdated(List targets); + } + + /** View to which this plugin can be registered, in order to get updates. */ + interface SmartspaceView { + void registerDataProvider(BcSmartspaceDataPlugin plugin); + + /** + * Sets {@link BcSmartspaceConfigPlugin}. + */ + default void registerConfigProvider(BcSmartspaceConfigPlugin configProvider) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** + * Primary color for unprotected text + */ + void setPrimaryTextColor(int color); + + /** + * Set the UI surface for the cards. Should be called immediately after the view is created. + */ + void setUiSurface(String uiSurface); + + /** + * Range [0.0 - 1.0] when transitioning from Lockscreen to/from AOD + */ + void setDozeAmount(float amount); + + /** + * Set if the screen is on. + */ + default void setScreenOn(boolean screenOn) {} + + /** + * Sets a delegate to handle clock event registration. Should be called immediately after + * the view is created. + */ + default void setTimeChangedDelegate(TimeChangedDelegate delegate) {} + + /** + * Set if dozing is true or false + */ + default void setDozing(boolean dozing) {} + + /** + * Set if split shade enabled + */ + default void setSplitShadeEnabled(boolean enabled) {} + + /** + * Set the current keyguard bypass enabled status. + */ + default void setKeyguardBypassEnabled(boolean enabled) {} + + /** + * Overrides how Intents/PendingIntents gets launched. Mostly to support auth from + * the lockscreen. + */ + void setIntentStarter(IntentStarter intentStarter); + + /** + * When on the lockscreen, use the FalsingManager to help detect errant touches + */ + void setFalsingManager(com.android.systemui.plugins.FalsingManager falsingManager); + + /** + * Set or clear Do Not Disturb information. + */ + default void setDnd(@Nullable Drawable image, @Nullable String description) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** + * Set or clear next alarm information + */ + default void setNextAlarm(@Nullable Drawable image, @Nullable String description) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** + * Set or clear device media playing + */ + default void setMediaTarget(@Nullable SmartspaceTarget target) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** + * Get the index of the currently selected page. + */ + default int getSelectedPage() { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** + * Return the top padding value from the currently visible card, or 0 if there is no current + * card. + */ + default int getCurrentCardTopPadding() { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + } + + /** Interface for launching Intents, which can differ on the lockscreen */ + interface IntentStarter { + default void startFromAction(SmartspaceAction action, View v, boolean showOnLockscreen) { + try { + if (action.getIntent() != null) { + startIntent(v, action.getIntent(), showOnLockscreen); + } else if (action.getPendingIntent() != null) { + startPendingIntent(v, action.getPendingIntent(), showOnLockscreen); + } + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Could not launch intent for action: " + action, e); + } + } + + default void startFromAction(TapAction action, View v, boolean showOnLockscreen) { + try { + if (action.getIntent() != null) { + startIntent(v, action.getIntent(), showOnLockscreen); + } else if (action.getPendingIntent() != null) { + startPendingIntent(v, action.getPendingIntent(), showOnLockscreen); + } + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Could not launch intent for action: " + action, e); + } + } + + /** Start the intent */ + void startIntent(View v, Intent i, boolean showOnLockscreen); + + /** Start the PendingIntent */ + void startPendingIntent(View v, PendingIntent pi, boolean showOnLockscreen); + } + + /** Interface for delegating time updates */ + interface TimeChangedDelegate { + /** Register the callback to be called when time is updated **/ + void register(Runnable callback); + + /** Unegister the callback **/ + void unregister(); + } +} diff --git a/systemUIPlugin/build.gradle b/systemUIPlugin/build.gradle new file mode 100644 index 0000000000..5b6b034dee --- /dev/null +++ b/systemUIPlugin/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace "com.android.systemui.plugins" + buildFeatures { + aidl true + } + sourceSets { + main { + java.srcDirs = ['src'] + aidl.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + res.srcDirs = ['res'] + } + } +} + +addFrameworkJar('framework-15.jar') +compileOnlyCommonJars() + +dependencies { + compileOnly projects.systemUIPluginCore + compileOnly projects.systemUILog + compileOnly projects.systemUIAnim +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/ActivityStarter.java b/systemUIPlugin/src/com/android/systemui/plugins/ActivityStarter.java new file mode 100644 index 0000000000..7cf56aa5c4 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/ActivityStarter.java @@ -0,0 +1,185 @@ +/* + * 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.systemui.plugins; + +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.view.View; + +import com.android.systemui.animation.ActivityTransitionAnimator; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * An interface to start activities. This is used as a callback from the views to + * {@link PhoneStatusBar} to allow custom handling for starting the activity, i.e. dismissing the + * Keyguard. + */ +@ProvidesInterface(version = ActivityStarter.VERSION) +public interface ActivityStarter { + int VERSION = 2; + + void startPendingIntentDismissingKeyguard(PendingIntent intent); + + /** + * Similar to {@link #startPendingIntentDismissingKeyguard(PendingIntent)}, but allows + * you to specify the callback that is executed on the UI thread after the intent is sent. + */ + void startPendingIntentDismissingKeyguard(PendingIntent intent, + Runnable intentSentUiThreadCallback); + + /** + * Similar to {@link #startPendingIntentDismissingKeyguard(PendingIntent, Runnable)}, but also + * specifies an associated view that should be used for the activity launch animation. + */ + void startPendingIntentDismissingKeyguard(PendingIntent intent, + Runnable intentSentUiThreadCallback, @Nullable View associatedView); + + /** + * Similar to {@link #startPendingIntentDismissingKeyguard(PendingIntent, Runnable)}, but also + * specifies an animation controller that should be used for the activity launch animation. + */ + void startPendingIntentDismissingKeyguard(PendingIntent intent, + Runnable intentSentUiThreadCallback, + @Nullable ActivityTransitionAnimator.Controller animationController); + + /** + * Similar to {@link #startPendingIntentMaybeDismissingKeyguard(PendingIntent, Runnable, + * ActivityTransitionAnimator.Controller)} but will always not dismiss the keyguard when + * launching activities. This should be avoided and other alternatives should be used. + */ + void startPendingIntentWithoutDismissing( + PendingIntent intent, + boolean dismissShade, + Runnable intentSentUiThreadCallback, + @Nullable ActivityTransitionAnimator.Controller animationController, + @Nullable Intent fillInIntent, + @Nullable Bundle extraOptions); + + /** + * Similar to {@link #startPendingIntentDismissingKeyguard}, except that it supports launching + * activities on top of the keyguard. If the activity supports {@code showOverLockscreen}, it + * will show over keyguard without first dimissing it. If it doesn't support it, calling this + * method is exactly the same as calling {@link #startPendingIntentDismissingKeyguard}. + */ + void startPendingIntentMaybeDismissingKeyguard(PendingIntent intent, + @Nullable Runnable intentSentUiThreadCallback, + @Nullable ActivityTransitionAnimator.Controller animationController); + + /** + * Similar to {@link #startPendingIntentMaybeDismissingKeyguard(PendingIntent, Runnable, + * ActivityTransitionAnimator.Controller)}, but also specifies a fill-in intent and extra + * option that could be used to populate the pending intent and launch the activity. This also + * allows the caller to avoid dismissing the shade. + */ + void startPendingIntentMaybeDismissingKeyguard(PendingIntent intent, + boolean dismissShade, + @Nullable Runnable intentSentUiThreadCallback, + @Nullable ActivityTransitionAnimator.Controller animationController, + @Nullable Intent fillInIntent, + @Nullable Bundle extraOptions); + + /** + * The intent flag can be specified in startActivity(). + */ + void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags); + void startActivity(Intent intent, boolean dismissShade); + default void startActivity(Intent intent, boolean dismissShade, + @Nullable ActivityTransitionAnimator.Controller animationController) { + startActivity(intent, dismissShade, animationController, + false /* showOverLockscreenWhenLocked */); + } + + void startActivity(Intent intent, boolean dismissShade, + @Nullable ActivityTransitionAnimator.Controller animationController, + boolean showOverLockscreenWhenLocked); + void startActivity(Intent intent, boolean dismissShade, + @Nullable ActivityTransitionAnimator.Controller animationController, + boolean showOverLockscreenWhenLocked, UserHandle userHandle); + void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade); + void startActivity(Intent intent, boolean dismissShade, Callback callback); + void postStartActivityDismissingKeyguard(Intent intent, int delay); + void postStartActivityDismissingKeyguard(Intent intent, int delay, + @Nullable ActivityTransitionAnimator.Controller animationController); + + /** Posts a start activity intent that dismisses keyguard. */ + void postStartActivityDismissingKeyguard(Intent intent, int delay, + @Nullable ActivityTransitionAnimator.Controller animationController, + @Nullable String customMessage); + void postStartActivityDismissingKeyguard(PendingIntent intent); + + /** + * Similar to {@link #postStartActivityDismissingKeyguard(PendingIntent)}, but also specifies an + * animation controller that should be used for the activity launch animation. + */ + void postStartActivityDismissingKeyguard(PendingIntent intent, + @Nullable ActivityTransitionAnimator.Controller animationController); + + void postQSRunnableDismissingKeyguard(Runnable runnable); + + void dismissKeyguardThenExecute(OnDismissAction action, @Nullable Runnable cancel, + boolean afterKeyguardGone); + + /** Authenticates if needed and dismisses keyguard to execute an action. */ + void dismissKeyguardThenExecute(OnDismissAction action, @Nullable Runnable cancel, + boolean afterKeyguardGone, @Nullable String customMessage); + + /** Starts an activity and dismisses keyguard. */ + void startActivityDismissingKeyguard(Intent intent, + boolean onlyProvisioned, + boolean dismissShade); + + /** Starts an activity and dismisses keyguard. */ + void startActivityDismissingKeyguard(Intent intent, + boolean onlyProvisioned, + boolean dismissShade, + boolean disallowEnterPictureInPictureWhileLaunching, + Callback callback, + int flags, + @Nullable ActivityTransitionAnimator.Controller animationController, + UserHandle userHandle); + + /** Execute a runnable after dismissing keyguard. */ + void executeRunnableDismissingKeyguard(Runnable runnable, + Runnable cancelAction, + boolean dismissShade, + boolean afterKeyguardGone, + boolean deferred); + + /** Whether we should animate an activity launch. */ + boolean shouldAnimateLaunch(boolean isActivityIntent); + + interface Callback { + void onActivityStarted(int resultCode); + } + + interface OnDismissAction { + /** + * @return {@code true} if the dismiss should be deferred. When returning true, make sure to + * call {@link com.android.keyguard.ViewMediatorCallback#readyForKeyguardDone()} + * *after* returning to start hiding the keyguard. + */ + boolean onDismiss(); + + /** + * Whether running this action when we are locked will start an animation on the keyguard. + */ + default boolean willRunAnimationOnKeyguard() { + return false; + } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/DarkIconDispatcher.java b/systemUIPlugin/src/com/android/systemui/plugins/DarkIconDispatcher.java new file mode 100644 index 0000000000..403c7c5004 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/DarkIconDispatcher.java @@ -0,0 +1,204 @@ +/* + * 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.systemui.plugins; + +import android.graphics.Color; +import android.graphics.Rect; +import android.view.View; + +import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area and dark + * intensity. Accessible through {@link PluginDependency} + */ +@ProvidesInterface(version = DarkIconDispatcher.VERSION) +@DependsOn(target = DarkReceiver.class) +public interface DarkIconDispatcher { + int VERSION = 2; + + /** + * Sets the dark area so {@link #applyDark} only affects the icons in the specified area. + * + * @param r the areas in which icons should change its tint, in logical screen + * coordinates + */ + void setIconsDarkArea(ArrayList r); + + /** + * Adds a receiver to receive callbacks onDarkChanged + */ + void addDarkReceiver(DarkReceiver receiver); + + /** + * Must have been previously been added through one of the addDarkReceive methods above. + */ + void removeDarkReceiver(DarkReceiver object); + + /** + * Used to reapply darkness on an object, must have previously been added through + * addDarkReceiver. + */ + void applyDark(DarkReceiver object); + + /** The default tint (applicable for dark backgrounds) is white */ + int DEFAULT_ICON_TINT = Color.WHITE; + /** To support an icon which wants to create contrast, the default tint is black-on-white. */ + int DEFAULT_INVERSE_ICON_TINT = Color.BLACK; + + Rect sTmpRect = new Rect(); + int[] sTmpInt2 = new int[2]; + + /** + * @return the tint to apply to view depending on the desired tint color and + * the screen tintArea in which to apply that tint + */ + static int getTint(Collection tintAreas, View view, int color) { + if (isInAreas(tintAreas, view)) { + return color; + } else { + return DEFAULT_ICON_TINT; + } + } + + /** + * @return the tint to apply to a foreground, given that the background is tinted + * per {@link #getTint} + */ + static int getInverseTint(Collection tintAreas, View view, int inverseColor) { + if (isInAreas(tintAreas, view)) { + return inverseColor; + } else { + return DEFAULT_INVERSE_ICON_TINT; + } + } + + /** + * @return true if more than half of the view's area is in any of the given area Rects, false + * otherwise + */ + static boolean isInAreas(Collection areas, View view) { + if (areas.isEmpty()) { + return true; + } + for (Rect area : areas) { + if (isInArea(area, view)) { + return true; + } + } + return false; + } + + /** + * @return true if more than half of the viewBounds are in any of the given area Rects, false + * otherwise + */ + static boolean isInAreas(Collection areas, Rect viewBounds) { + if (areas.isEmpty()) { + return true; + } + for (Rect area : areas) { + if (isInArea(area, viewBounds)) { + return true; + } + } + return false; + } + + /** @return true if more than half of the viewBounds are in the area Rect, false otherwise */ + static boolean isInArea(Rect area, Rect viewBounds) { + if (area.isEmpty()) { + return true; + } + sTmpRect.set(area); + int left = viewBounds.left; + int width = viewBounds.width(); + + int intersectStart = Math.max(left, area.left); + int intersectEnd = Math.min(left + width, area.right); + int intersectAmount = Math.max(0, intersectEnd - intersectStart); + + boolean coversFullStatusBar = area.top <= 0; + boolean majorityOfWidth = 2 * intersectAmount > width; + return majorityOfWidth && coversFullStatusBar; + } + + /** @return true if more than half of the view's area is in the area Rect, false otherwise */ + static boolean isInArea(Rect area, View view) { + if (area.isEmpty()) { + return true; + } + sTmpRect.set(area); + view.getLocationOnScreen(sTmpInt2); + int left = sTmpInt2[0]; + + int intersectStart = Math.max(left, area.left); + int intersectEnd = Math.min(left + view.getWidth(), area.right); + int intersectAmount = Math.max(0, intersectEnd - intersectStart); + + boolean coversFullStatusBar = area.top <= 0; + boolean majorityOfWidth = 2 * intersectAmount > view.getWidth(); + return majorityOfWidth && coversFullStatusBar; + } + + /** + * Receives a callback on darkness changes + */ + @ProvidesInterface(version = DarkReceiver.VERSION) + interface DarkReceiver { + int VERSION = 3; + + /** + * @param areas list of regions on screen where the tint applies + * @param darkIntensity float representing the level of tint. In the range [0,1] + * @param tint the tint applicable as a foreground contrast to the dark regions. This value + * is interpolated between a default light and dark tone, and is therefore + * usable as-is, as long as the view is in one of the areas defined in + * {@code areas}. + * + * @see DarkIconDispatcher#isInArea(Rect, View) for utilizing {@code areas} + * + * Note: only one of {@link #onDarkChanged(ArrayList, float, int)} or + * {@link #onDarkChangedWithContrast(ArrayList, int, int)} need to be implemented, as both + * will be called in the same circumstances. + */ + void onDarkChanged(ArrayList areas, float darkIntensity, int tint); + + /** + * New version of onDarkChanged, which describes a tint plus an optional contrastTint + * that can be used if the tint is applied to the background of an icon. + * + * We use the 2 here to avoid the case where an existing override of onDarkChanged + * might pass in parameters as bare numbers (e.g. 0 instead of 0f) which might get + * mistakenly cast to (int) and therefore trigger this method. + * + * @param areas list of areas where dark tint applies + * @param tint int describing the tint color to use + * @param contrastTint if desired, a contrasting color that can be used for a foreground + * + * Note: only one of {@link #onDarkChanged(ArrayList, float, int)} or + * {@link #onDarkChangedWithContrast(ArrayList, int, int)} need to be implemented, as both + * will be called in the same circumstances. + */ + default void onDarkChangedWithContrast(ArrayList areas, int tint, int contrastTint) {} + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/DozeServicePlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/DozeServicePlugin.java new file mode 100644 index 0000000000..3ca5690af4 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/DozeServicePlugin.java @@ -0,0 +1,21 @@ +package com.android.systemui.plugins; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +@ProvidesInterface(action = DozeServicePlugin.ACTION, version = DozeServicePlugin.VERSION) +public interface DozeServicePlugin extends Plugin { + String ACTION = "com.android.systemui.action.PLUGIN_DOZE"; + int VERSION = 1; + + public interface RequestDoze { + void onRequestShowDoze(); + + void onRequestHideDoze(); + } + + void onDreamingStarted(); + + void onDreamingStopped(); + + void setDozeRequester(RequestDoze requester); +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/FalsingManager.java b/systemUIPlugin/src/com/android/systemui/plugins/FalsingManager.java new file mode 100644 index 0000000000..e52a57f761 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/FalsingManager.java @@ -0,0 +1,176 @@ +/* + * 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.systemui.plugins; + +import android.annotation.IntDef; +import android.net.Uri; +import android.view.MotionEvent; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Interface that decides whether a touch on the phone was accidental. i.e. Pocket Dialing. + * + * {@see com.android.systemui.classifier.BrightLineFalsingManager} + */ +@ProvidesInterface(version = FalsingManager.VERSION) +public interface FalsingManager { + int VERSION = 6; + + int NO_PENALTY = 0; + int LOW_PENALTY = 1; + int MODERATE_PENALTY = 2; + int HIGH_PENALTY = 3; + + @IntDef({ + NO_PENALTY, + LOW_PENALTY, + MODERATE_PENALTY, + HIGH_PENALTY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Penalty {} + + void onSuccessfulUnlock(); + + boolean isUnlockingDisabled(); + + /** Returns true if the gesture should be rejected. */ + boolean isFalseTouch(int interactionType); + + /** + * Does basic checking to see if gesture looks like a tap. + * + * Only does the most basic of checks. No penalty is applied if this method returns false. + * + * For more robust analysis, use {@link #isFalseTap(int)}. + */ + boolean isSimpleTap(); + + /** + * Returns true if the FalsingManager thinks the last gesture was not a valid tap. + * + * This method runs a more thorough analysis than the similar {@link #isSimpleTap()}, + * that can include historical interactions and other contextual cues to see + * if the tap looks accidental. + * + * Use this method to validate a tap for launching an action, like opening + * a notification. + * + * The only parameter, penalty, indicates how much this should affect future gesture + * classifications if this tap looks like a false. As single taps are hard to confirm as false + * or otherwise, a low penalty value is encouraged unless context indicates otherwise. + */ + boolean isFalseTap(@Penalty int penalty); + + /** + * Returns true if the FalsingManager thinks the last gesture was not a valid long tap. + * + * Use this method to validate a long tap for launching an action, like long press on a UMO + * + * The only parameter, penalty, indicates how much this should affect future gesture + * classifications if this long tap looks like a false. + * As long taps are hard to confirm as false or otherwise, + * a low penalty value is encouraged unless context indicates otherwise. + */ + boolean isFalseLongTap(@Penalty int penalty); + + /** + * Returns true if the last two gestures do not look like a double tap. + * + * Only works on data that has already been reported to the FalsingManager. Be sure that + * {@link com.android.systemui.classifier.FalsingCollector#onTouchEvent(MotionEvent)} + * has already been called for all of the taps you want considered. + * + * This looks at the last two gestures on the screen, ensuring that they meet the following + * criteria: + * + * a) There are at least two gestures. + * b) The last two gestures look like taps. + * c) The last two gestures look like a double tap taken together. + * + * This method is _not_ context aware. That is to say, if two taps occur on two neighboring + * views, but are otherwise close to one another, this will report a successful double tap. + * It is up to the caller to decide + * @return + */ + boolean isFalseDoubleTap(); + + /** + * Whether the last proximity event reported NEAR. May be used to short circuit motion events + * that require the proximity sensor is not covered. + */ + boolean isProximityNear(); + + boolean isClassifierEnabled(); + + boolean shouldEnforceBouncer(); + + Uri reportRejectedTouch(); + + boolean isReportingEnabled(); + + /** From com.android.systemui.Dumpable. */ + void dump(PrintWriter pw, String[] args); + + /** + * Don't call this. It's meant for internal use to allow switching between implementations. + * + * Tests may also call it. + **/ + void cleanupInternal(); + + /** Call to report a ProximityEvent to the FalsingManager. */ + void onProximityEvent(ProximityEvent proximityEvent); + + /** Adds a {@link FalsingBeliefListener}. */ + void addFalsingBeliefListener(FalsingBeliefListener listener); + + /** Removes a {@link FalsingBeliefListener}. */ + void removeFalsingBeliefListener(FalsingBeliefListener listener); + + /** Adds a {@link FalsingTapListener}. */ + void addTapListener(FalsingTapListener falsingTapListener); + + /** Removes a {@link FalsingTapListener}. */ + void removeTapListener(FalsingTapListener falsingTapListener); + + /** Listener that is alerted when falsing belief level crosses a predfined threshold. */ + interface FalsingBeliefListener { + void onFalse(); + } + + /** + * Listener that is alerted when an additional tap is required to confirm a single tap. + **/ + interface FalsingTapListener { + void onAdditionalTapRequired(); + } + + /** Passed to {@link FalsingManager#onProximityEvent}. */ + interface ProximityEvent { + /** Returns true when the proximity sensor was covered. */ + boolean getCovered(); + + /** Returns when the proximity sensor was covered in nanoseconds. */ + long getTimestampNs(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/FalsingPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/FalsingPlugin.java new file mode 100644 index 0000000000..53d708d3b2 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/FalsingPlugin.java @@ -0,0 +1,52 @@ +/* + * 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.systemui.plugins; + +import android.content.Context; + +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Used to capture Falsing data (related to unlocking the screen). + * + * The intent is that the data can later be analyzed to validate the quality of the + * {@link FalsingManager}. + */ +@ProvidesInterface(action = FalsingPlugin.ACTION, version = FalsingPlugin.VERSION) +@DependsOn(target = FalsingManager.class) +public interface FalsingPlugin extends Plugin { + String ACTION = "com.android.systemui.action.FALSING_PLUGIN"; + int VERSION = 2; + + /** + * Called when there is data to be recorded. + * + * @param success Indicates whether the action is considered a success. + * @param data The raw data to be recorded for analysis. + */ + default void dataCollected(boolean success, byte[] data) { } + + /** + * Return a {@link FalsingManager} to be used in place of the system's default. + * + * @param context + */ + default FalsingManager getFalsingManager(Context context) { + return null; + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/FragmentBase.java b/systemUIPlugin/src/com/android/systemui/plugins/FragmentBase.java new file mode 100644 index 0000000000..af55e8b830 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/FragmentBase.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.content.Context; +import android.view.View; + +/** + * Interface to deal with lack of multiple inheritance + * + * This interface is designed to be used as a base class for plugin interfaces + * that need fragment methods. Plugins should not extend Fragment directly, so + * plugins that are fragments should be extending PluginFragment, but in SysUI + * these same versions should extend Fragment directly. + * + * Only methods that are on Fragment should be included here. + */ +public interface FragmentBase { + View getView(); + Context getContext(); +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/GlobalActions.java b/systemUIPlugin/src/com/android/systemui/plugins/GlobalActions.java new file mode 100644 index 0000000000..95ff13b45f --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/GlobalActions.java @@ -0,0 +1,45 @@ +/* + * 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.systemui.plugins; + +import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +@ProvidesInterface(action = GlobalActions.ACTION, version = GlobalActions.VERSION) +@DependsOn(target = GlobalActionsManager.class) +public interface GlobalActions extends Plugin { + + String ACTION = "com.android.systemui.action.PLUGIN_GLOBAL_ACTIONS"; + int VERSION = 1; + + void showGlobalActions(GlobalActionsManager manager); + default void showShutdownUi(boolean isReboot, String reason) { + } + + default void destroy() { + } + + @ProvidesInterface(version = GlobalActionsManager.VERSION) + public interface GlobalActionsManager { + int VERSION = 1; + + void onGlobalActionsShown(); + void onGlobalActionsHidden(); + + void shutdown(); + void reboot(boolean safeMode); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/GlobalActionsPanelPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/GlobalActionsPanelPlugin.java new file mode 100644 index 0000000000..429458fe07 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/GlobalActionsPanelPlugin.java @@ -0,0 +1,115 @@ +/* + * 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.systemui.plugins; + +import android.annotation.Nullable; +import android.app.BroadcastOptions; +import android.app.PendingIntent; +import android.graphics.drawable.Drawable; +import android.view.View; + +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Plugin which provides a "Panel" {@link View} to be rendered inside of the GlobalActions menu. + * + * Implementations should construct a new {@link PanelViewController} with the given + * {@link Callbacks} instance inside of {@link #onPanelShown(Callbacks, boolean)}, and should not + * hold onto a reference, instead allowing Global Actions to manage the lifetime of the object. + * + * Under this assumption, {@link PanelViewController} represents the lifetime of a single invocation + * of the Global Actions menu. The {@link View} for the Panel is generated when the + * {@link PanelViewController} is constructed, and {@link PanelViewController#getPanelContent()} + * serves as a simple getter. When Global Actions is dismissed, + * {@link PanelViewController#onDismissed()} can be used to cleanup any resources allocated when + * constructed. Global Actions will then release the reference, and the {@link PanelViewController} + * will be garbage-collected. + */ +@ProvidesInterface( + action = GlobalActionsPanelPlugin.ACTION, version = GlobalActionsPanelPlugin.VERSION) +@DependsOn(target = GlobalActionsPanelPlugin.Callbacks.class) +@DependsOn(target = GlobalActionsPanelPlugin.PanelViewController.class) +public interface GlobalActionsPanelPlugin extends Plugin { + String ACTION = "com.android.systemui.action.PLUGIN_GLOBAL_ACTIONS_PANEL"; + int VERSION = 0; + + /** + * Invoked when the GlobalActions menu is shown. + * + * @param callbacks {@link Callbacks} instance that can be used by the Panel to interact with + * the Global Actions menu. + * @param deviceLocked Indicates whether or not the device is currently locked. + * @return A {@link PanelViewController} instance used to receive Global Actions events. + */ + PanelViewController onPanelShown(Callbacks callbacks, boolean deviceLocked); + + /** + * Provides methods to interact with the Global Actions menu. + */ + @ProvidesInterface(version = Callbacks.VERSION) + interface Callbacks { + int VERSION = 0; + + /** Dismisses the Global Actions menu. */ + void dismissGlobalActionsMenu(); + + /** Starts a PendingIntent, dismissing the keyguard if necessary. */ + default void startPendingIntentDismissingKeyguard(PendingIntent pendingIntent) { + try { + BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setInteractive(true); + pendingIntent.send(options.toBundle()); + } catch (PendingIntent.CanceledException e) { + // no-op + } + } + } + + /** + * Receives Global Actions events, and provides the Panel {@link View}. + */ + @ProvidesInterface(version = PanelViewController.VERSION) + interface PanelViewController { + int VERSION = 0; + + /** + * Returns the {@link View} for the Panel to be rendered in Global Actions. This View can be + * any size, and will be rendered above the Global Actions menu when z-ordered. + */ + View getPanelContent(); + + /** + * Invoked when the Global Actions menu (containing the View returned from + * {@link #getPanelContent()}) is dismissed. + */ + void onDismissed(); + + /** + * Invoked when the device is either locked or unlocked. + */ + void onDeviceLockStateChanged(boolean locked); + + /** + * Optionally returns a drawable to be used as the background for Global Actions. + */ + @Nullable + default Drawable getBackgroundDrawable() { + return null; + } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/IntentButtonProvider.java b/systemUIPlugin/src/com/android/systemui/plugins/IntentButtonProvider.java new file mode 100644 index 0000000000..63bb24fb1f --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/IntentButtonProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.content.Intent; +import android.graphics.drawable.Drawable; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * An Intent Button represents a triggerable element in SysUI that consists of an + * Icon and an intent to trigger when it is activated (clicked, swiped, etc.). + */ +@ProvidesInterface(version = IntentButtonProvider.VERSION) +public interface IntentButtonProvider extends Plugin { + + public static final int VERSION = 1; + + public IntentButton getIntentButton(); + + public interface IntentButton { + public static class IconState { + public boolean isVisible = true; + public CharSequence contentDescription = null; + public Drawable drawable; + public boolean tint = true; + } + + public IconState getIcon(); + + public Intent getIntent(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java new file mode 100644 index 0000000000..5f6f11c16d --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java @@ -0,0 +1,72 @@ +/* + * 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.systemui.plugins; + +import android.graphics.Point; +import android.view.MotionEvent; +import android.view.WindowManager; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +import java.io.PrintWriter; + +/** Plugin to handle navigation edge gestures for Back. */ +@ProvidesInterface( + action = NavigationEdgeBackPlugin.ACTION, + version = NavigationEdgeBackPlugin.VERSION) +public interface NavigationEdgeBackPlugin extends Plugin { + String ACTION = "com.android.systemui.action.PLUGIN_NAVIGATION_EDGE_BACK_ACTION"; + int VERSION = 1; + + + /** Specifies if the UI should be rendered on the left side of the screen. */ + void setIsLeftPanel(boolean isLeftPanel); + + /** Sets the insets for the gesture handling area. */ + void setInsets(int leftInset, int rightInset); + + /** Sets the display size. */ + void setDisplaySize(Point displaySize); + + /** Sets the callback that should be invoked when a Back gesture is detected. */ + void setBackCallback(BackCallback callback); + + /** Sets the base LayoutParams for the UI. */ + void setLayoutParams(WindowManager.LayoutParams layoutParams); + + /** Updates the UI based on the motion events passed in device coordinates. */ + void onMotionEvent(MotionEvent motionEvent); + + /** Dumps info about the back gesture plugin. */ + void dump(PrintWriter pw); + + /** Callback to let the system react to the detected back gestures. */ + interface BackCallback { + /** Indicates that a Back gesture was recognized and the system should go back. */ + void triggerBack(); + + /** Indicates that the gesture was cancelled and the system should not go back. */ + void cancelBack(); + + /** + * Indicates if back will be triggered if committed in current state. + * + * @param triggerBack if back will be triggered in current state. + */ + void setTriggerBack(boolean triggerBack); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/NotificationListenerController.java b/systemUIPlugin/src/com/android/systemui/plugins/NotificationListenerController.java new file mode 100644 index 0000000000..6799450079 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/NotificationListenerController.java @@ -0,0 +1,83 @@ +/* + * 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.systemui.plugins; + +import android.app.NotificationChannel; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService.RankingMap; +import android.service.notification.StatusBarNotification; + +import com.android.systemui.plugins.NotificationListenerController.NotificationProvider; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +@ProvidesInterface(action = NotificationListenerController.ACTION, + version = NotificationListenerController.VERSION) +@DependsOn(target = NotificationProvider.class) +public interface NotificationListenerController extends Plugin { + String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_ASSISTANT"; + int VERSION = 1; + + void onListenerConnected(NotificationProvider provider); + + /** + * @return whether plugin wants to skip the default callbacks. + */ + default boolean onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { + return false; + } + + /** + * @return whether plugin wants to skip the default callbacks. + */ + default boolean onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { + return false; + } + + /** + * Called when a notification channel is modified. + * @param modificationType One of {@link #NOTIFICATION_CHANNEL_OR_GROUP_ADDED}, + * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED}, + * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}. + * @return whether a plugin wants to skip the default callbacks. + */ + default boolean onNotificationChannelModified( + String pkgName, UserHandle user, NotificationChannel channel, int modificationType) { + return false; + } + + default StatusBarNotification[] getActiveNotifications( + StatusBarNotification[] activeNotifications) { + return activeNotifications; + } + + default RankingMap getCurrentRanking(RankingMap currentRanking) { + return currentRanking; + } + + @ProvidesInterface(version = NotificationProvider.VERSION) + interface NotificationProvider { + int VERSION = 1; + + // Methods to get info about current notifications + StatusBarNotification[] getActiveNotifications(); + RankingMap getRankingMap(); + + // Methods to notify sysui of changes to notification list. + void addNotification(StatusBarNotification sbn); + void removeNotification(StatusBarNotification sbn); + void updateRanking(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/NotificationPersonExtractorPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/NotificationPersonExtractorPlugin.java new file mode 100644 index 0000000000..4113c04e76 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/NotificationPersonExtractorPlugin.java @@ -0,0 +1,80 @@ +/* + * 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.systemui.plugins; + +import android.annotation.Nullable; +import android.graphics.drawable.Drawable; +import android.service.notification.StatusBarNotification; + +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** Custom logic that can extract a PeopleHub "person" from a notification. */ +@ProvidesInterface( + action = NotificationPersonExtractorPlugin.ACTION, + version = NotificationPersonExtractorPlugin.VERSION) +@DependsOn(target = NotificationPersonExtractorPlugin.PersonData.class) +public interface NotificationPersonExtractorPlugin extends Plugin { + + String ACTION = "com.android.systemui.action.PEOPLE_HUB_PERSON_EXTRACTOR"; + int VERSION = 1; + + /** + * Attempts to extract a person from a notification. Returns {@code null} if one is not found. + */ + @Nullable + PersonData extractPerson(StatusBarNotification sbn); + + /** + * Attempts to extract a person id from a notification. Returns {@code null} if one is not + * found. + * + * This method can be overridden in order to provide a faster implementation. + */ + @Nullable + default String extractPersonKey(StatusBarNotification sbn) { + return extractPerson(sbn).key; + } + + /** + * Determines whether or not a notification should be treated as having a person. Used for + * appropriate positioning in the notification shade. + */ + default boolean isPersonNotification(StatusBarNotification sbn) { + return extractPersonKey(sbn) != null; + } + + /** A person to be surfaced in PeopleHub. */ + @ProvidesInterface(version = PersonData.VERSION) + final class PersonData { + + public static final int VERSION = 0; + + public final String key; + public final CharSequence name; + public final Drawable avatar; + public final Runnable clickRunnable; + + public PersonData(String key, CharSequence name, Drawable avatar, + Runnable clickRunnable) { + this.key = key; + this.name = name; + this.avatar = avatar; + this.clickRunnable = clickRunnable; + } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/OverlayPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/OverlayPlugin.java new file mode 100644 index 0000000000..075df75f93 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/OverlayPlugin.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.android.systemui.plugins; + +import android.view.View; + +import com.android.systemui.plugins.annotations.ProvidesInterface; +import com.android.systemui.plugins.statusbar.DozeParameters; + +@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION) +public interface OverlayPlugin extends Plugin { + + String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY"; + int VERSION = 4; + + /** + * Setup overlay plugin + */ + void setup(View statusBar, View navBar); + + /** + * Setup overlay plugin with callback and DozeParameters + */ + default void setup(View statusBar, View navBar, Callback callback, + DozeParameters dozeParameters) { + setup(statusBar, navBar); + } + + default boolean holdStatusBarOpen() { + return false; + } + + /** + * Only called if the plugin has returned true to holdStatusBarOpen(). + */ + default void setCollapseDesired(boolean collapseDesired) { + } + + /** + * Used to update system ui whether to hold status bar open + */ + interface Callback { + void onHoldStatusBarOpenChange(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/PluginDependency.java b/systemUIPlugin/src/com/android/systemui/plugins/PluginDependency.java new file mode 100644 index 0000000000..25ce3ddf81 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/PluginDependency.java @@ -0,0 +1,31 @@ +/* + * 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.systemui.plugins; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +@ProvidesInterface(version = PluginDependency.VERSION) +public class PluginDependency { + public static final int VERSION = 1; + static DependencyProvider sProvider; + + public static T get(Plugin p, Class cls) { + return sProvider.get(p, cls); + } + + static abstract class DependencyProvider { + abstract T get(Plugin p, Class cls); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/PluginUtils.java b/systemUIPlugin/src/com/android/systemui/plugins/PluginUtils.java new file mode 100644 index 0000000000..acb6f115c7 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/PluginUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.content.Context; +import android.view.View; + +public class PluginUtils { + + public static void setId(Context sysuiContext, View view, String id) { + int i = sysuiContext.getResources().getIdentifier(id, "id", sysuiContext.getPackageName()); + view.setId(i); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/SensorManagerPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/SensorManagerPlugin.java new file mode 100644 index 0000000000..d62c1d411c --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/SensorManagerPlugin.java @@ -0,0 +1,119 @@ +/* + * 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.systemui.plugins; + +import android.hardware.SensorListener; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Allows for additional sensors to be retrieved from + * {@link com.android.systemui.util.sensors.AsyncSensorManager}. + */ +@ProvidesInterface(action = SensorManagerPlugin.ACTION, version = SensorManagerPlugin.VERSION) +public interface SensorManagerPlugin extends Plugin { + String ACTION = "com.android.systemui.action.PLUGIN_SENSOR_MANAGER"; + int VERSION = 1; + + /** + * Registers for sensor events. Events will be sent until the listener is unregistered. + * @param sensor + * @param listener + * @see android.hardware.SensorManager#registerListener(SensorListener, int) + */ + void registerListener(Sensor sensor, SensorEventListener listener); + + /** + * Unregisters events from the sensor. + * @param sensor + * @param listener + */ + void unregisterListener(Sensor sensor, SensorEventListener listener); + + /** + * Listener triggered whenever the Sensor has new data. + */ + interface SensorEventListener { + void onSensorChanged(SensorEvent event); + } + + /** + * Sensor that can be defined in a plugin. + */ + class Sensor { + public static final int TYPE_WAKE_LOCK_SCREEN = 1; + public static final int TYPE_WAKE_DISPLAY = 2; + public static final int TYPE_SWIPE = 3; + public static final int TYPE_SKIP_STATUS = 4; + + private int mType; + + public Sensor(int type) { + mType = type; + } + public int getType() { + return mType; + } + public String toString() { + return "{PluginSensor type=\"" + mType + "\"}"; + } + } + + /** + * Event sent by a {@link Sensor}. + */ + class SensorEvent { + Sensor mSensor; + int mVendorType; + float[] mValues; + + /** + * Creates a sensor event. + * @param sensor The type of sensor, e.g. TYPE_WAKE_LOCK_SCREEN + * @param vendorType The vendor type, which should be unique for each type of sensor, + * e.g. SINGLE_TAP = 1, DOUBLE_TAP = 2, etc. + */ + public SensorEvent(Sensor sensor, int vendorType) { + this(sensor, vendorType, null); + } + + /** + * Creates a sensor event. + * @param sensor The type of sensor, e.g. TYPE_WAKE_LOCK_SCREEN + * @param vendorType The vendor type, which should be unique for each type of sensor, + * e.g. SINGLE_TAP = 1, DOUBLE_TAP = 2, etc. + * @param values Values captured by the sensor. + */ + public SensorEvent(Sensor sensor, int vendorType, float[] values) { + mSensor = sensor; + mVendorType = vendorType; + mValues = values; + } + + public Sensor getSensor() { + return mSensor; + } + + public float[] getValues() { + return mValues; + } + + public int getVendorType() { + return mVendorType; + } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/ToastPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/ToastPlugin.java new file mode 100644 index 0000000000..da079cf044 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/ToastPlugin.java @@ -0,0 +1,112 @@ +/* + * 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.systemui.plugins; + +import android.animation.Animator; +import android.annotation.NonNull; +import android.view.View; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Customize toasts displayed by SystemUI (via Toast#makeText) + */ +@ProvidesInterface(action = ToastPlugin.ACTION, version = ToastPlugin.VERSION) +public interface ToastPlugin extends Plugin { + + String ACTION = "com.android.systemui.action.PLUGIN_TOAST"; + int VERSION = 1; + + /** + * Creates a CustomPluginToast. + */ + @NonNull Toast createToast(CharSequence text, String packageName, int userId); + + /** + * Custom Toast with the ability to change toast positioning, styling and animations. + */ + interface Toast { + /** + * Retrieve the Toast view's gravity. + * If no changes, returns null. + */ + default Integer getGravity() { + return null; + } + + /** + * Retrieve the Toast view's X-offset. + * If no changes, returns null. + */ + default Integer getXOffset() { + return null; + } + + /** + * Retrieve the Toast view's Y-offset. + * If no changes, returns null. + */ + default Integer getYOffset() { + return null; + } + + /** + * Retrieve the Toast view's horizontal margin. + * If no changes, returns null. + */ + default Integer getHorizontalMargin() { + return null; + } + + /** + * Retrieve the Toast view's vertical margin. + * If no changes, returns null. + */ + default Integer getVerticalMargin() { + return null; + } + + /** + * Retrieve the Toast view to show. + * If no changes, returns null. + */ + default View getView() { + return null; + } + + /** + * Retrieve the Toast's animate in. + * If no changes, returns null. + */ + default Animator getInAnimation() { + return null; + } + + /** + * Retrieve the Toast's animate out. + * If no changes, returns null. + */ + default Animator getOutAnimation() { + return null; + } + + /** + * Called on orientation changes. + */ + default void onOrientationChange(int orientation) { } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/ViewProvider.java b/systemUIPlugin/src/com/android/systemui/plugins/ViewProvider.java new file mode 100644 index 0000000000..18ca8e6e26 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/ViewProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.view.View; + +public interface ViewProvider extends Plugin { + View getView(); +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/VolumeDialog.java b/systemUIPlugin/src/com/android/systemui/plugins/VolumeDialog.java new file mode 100644 index 0000000000..9e5db73cf8 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/VolumeDialog.java @@ -0,0 +1,41 @@ +/* + * 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.systemui.plugins; + +import com.android.systemui.plugins.VolumeDialog.Callback; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * This interface is really just a stub for initialization/teardown, actual handling of + * when to show will be done through {@link VolumeDialogController} + */ +@ProvidesInterface(action = VolumeDialog.ACTION, version = VolumeDialog.VERSION) +@DependsOn(target = Callback.class) +public interface VolumeDialog extends Plugin { + String ACTION = "com.android.systemui.action.PLUGIN_VOLUME"; + int VERSION = 1; + + void init(int windowType, Callback callback); + void destroy(); + + @ProvidesInterface(version = VERSION) + public interface Callback { + int VERSION = 1; + + void onZenSettingsClicked(); + void onZenPrioritySettingsClicked(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/VolumeDialogController.java b/systemUIPlugin/src/com/android/systemui/plugins/VolumeDialogController.java new file mode 100644 index 0000000000..b1736b1687 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/VolumeDialogController.java @@ -0,0 +1,236 @@ +/* + * 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.systemui.plugins; + +import android.annotation.IntegerRes; +import android.content.ComponentName; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.os.Handler; +import android.os.VibrationEffect; +import android.util.SparseArray; + +import com.android.systemui.plugins.VolumeDialogController.Callbacks; +import com.android.systemui.plugins.VolumeDialogController.State; +import com.android.systemui.plugins.VolumeDialogController.StreamState; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Manages the VolumeDialog. + * + * Accessible through {@link PluginDependency} + */ +@ProvidesInterface(version = VolumeDialogController.VERSION) +@DependsOn(target = StreamState.class) +@DependsOn(target = State.class) +@DependsOn(target = Callbacks.class) +public interface VolumeDialogController { + int VERSION = 1; + + void setActiveStream(int stream); + void setStreamVolume(int stream, int userLevel); + void setRingerMode(int ringerModeNormal, boolean external); + + boolean hasVibrator(); + void vibrate(VibrationEffect effect); + void scheduleTouchFeedback(); + + AudioManager getAudioManager(); + + void notifyVisible(boolean visible); + + void addCallback(Callbacks callbacks, Handler handler); + void removeCallback(Callbacks callbacks); + + void userActivity(); + void getState(); + + /** + * Get Captions enabled state + * + * @param checkForSwitchState set true when we'd like to switch captions enabled state after + * getting the latest captions state. + */ + void getCaptionsEnabledState(boolean checkForSwitchState); + + /** + * Set Captions enabled state + * + * @param enabled the captions enabled state we'd like to update. + */ + void setCaptionsEnabledState(boolean enabled); + + /** + * Get Captions component state + * + * @param fromTooltip if it's triggered from tooltip. + */ + void getCaptionsComponentState(boolean fromTooltip); + + @ProvidesInterface(version = StreamState.VERSION) + public static final class StreamState { + public static final int VERSION = 1; + + public boolean dynamic; + public int level; + public int levelMin; + public int levelMax; + public boolean muted; + public boolean muteSupported; + public @IntegerRes int name; + public String remoteLabel; + public boolean routedToBluetooth; + + public StreamState copy() { + final StreamState rt = new StreamState(); + rt.dynamic = dynamic; + rt.level = level; + rt.levelMin = levelMin; + rt.levelMax = levelMax; + rt.muted = muted; + rt.muteSupported = muteSupported; + rt.name = name; + rt.remoteLabel = remoteLabel; + rt.routedToBluetooth = routedToBluetooth; + return rt; + } + } + + @ProvidesInterface(version = State.VERSION) + public static final class State { + public static final int VERSION = 1; + + public static int NO_ACTIVE_STREAM = -1; + + public final SparseArray states = new SparseArray<>(); + + public int ringerModeInternal; + public int ringerModeExternal; + public int zenMode; + public ComponentName effectsSuppressor; + public String effectsSuppressorName; + public int activeStream = NO_ACTIVE_STREAM; + public boolean disallowAlarms; + public boolean disallowMedia; + public boolean disallowSystem; + public boolean disallowRinger; + + public State copy() { + final State rt = new State(); + for (int i = 0; i < states.size(); i++) { + rt.states.put(states.keyAt(i), states.valueAt(i).copy()); + } + rt.ringerModeExternal = ringerModeExternal; + rt.ringerModeInternal = ringerModeInternal; + rt.zenMode = zenMode; + if (effectsSuppressor != null) { + rt.effectsSuppressor = effectsSuppressor.clone(); + } + rt.effectsSuppressorName = effectsSuppressorName; + rt.activeStream = activeStream; + rt.disallowAlarms = disallowAlarms; + rt.disallowMedia = disallowMedia; + rt.disallowSystem = disallowSystem; + rt.disallowRinger = disallowRinger; + return rt; + } + + @Override + public String toString() { + return toString(0); + } + + public String toString(int indent) { + final StringBuilder sb = new StringBuilder("{"); + if (indent > 0) sep(sb, indent); + for (int i = 0; i < states.size(); i++) { + if (i > 0) { + sep(sb, indent); + } + final int stream = states.keyAt(i); + final StreamState ss = states.valueAt(i); + sb.append(AudioSystem.streamToString(stream)).append(":").append(ss.level) + .append('[').append(ss.levelMin).append("..").append(ss.levelMax) + .append(']'); + if (ss.muted) sb.append(" [MUTED]"); + if (ss.dynamic) sb.append(" [DYNAMIC]"); + } + sep(sb, indent); sb.append("ringerModeExternal:").append(ringerModeExternal); + sep(sb, indent); sb.append("ringerModeInternal:").append(ringerModeInternal); + sep(sb, indent); sb.append("zenMode:").append(zenMode); + sep(sb, indent); sb.append("effectsSuppressor:").append(effectsSuppressor); + sep(sb, indent); sb.append("effectsSuppressorName:").append(effectsSuppressorName); + sep(sb, indent); sb.append("activeStream:").append(activeStream); + sep(sb, indent); sb.append("disallowAlarms:").append(disallowAlarms); + sep(sb, indent); sb.append("disallowMedia:").append(disallowMedia); + sep(sb, indent); sb.append("disallowSystem:").append(disallowSystem); + sep(sb, indent); sb.append("disallowRinger:").append(disallowRinger); + if (indent > 0) sep(sb, indent); + return sb.append('}').toString(); + } + + private static void sep(StringBuilder sb, int indent) { + if (indent > 0) { + sb.append('\n'); + for (int i = 0; i < indent; i++) { + sb.append(' '); + } + } else { + sb.append(','); + } + } + } + + @ProvidesInterface(version = Callbacks.VERSION) + public interface Callbacks { + int VERSION = 2; + + // requires version 1 + void onShowRequested(int reason, boolean keyguardLocked, int lockTaskModeState); + void onDismissRequested(int reason); + void onStateChanged(State state); + void onLayoutDirectionChanged(int layoutDirection); + void onConfigurationChanged(); + void onShowVibrateHint(); + void onShowSilentHint(); + void onScreenOff(); + void onShowSafetyWarning(int flags); + void onAccessibilityModeChanged(Boolean showA11yStream); + + /** + * Callback function for captions component state changed event + * + * @param isComponentEnabled the lateset captions component state. + * @param fromTooltip if it's triggered from tooltip. + */ + void onCaptionComponentStateChanged(Boolean isComponentEnabled, Boolean fromTooltip); + + /** + * Callback function for captions enabled state changed event + * + * @param isEnabled the lateset captions enabled state. + * @param checkBeforeSwitch intend to switch captions enabled state after the callback. + */ + void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch); + // requires version 2 + void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs); + + /** + * Callback function for when the volume changed due to a physical key press. + */ + void onVolumeChangedFromKey(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/clocks/AlarmData.kt b/systemUIPlugin/src/com/android/systemui/plugins/clocks/AlarmData.kt new file mode 100644 index 0000000000..837857bfa3 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/clocks/AlarmData.kt @@ -0,0 +1,6 @@ +package com.android.systemui.plugins.clocks + +data class AlarmData( + val nextAlarmMillis: Long?, + val descriptionId: String?, +) diff --git a/systemUIPlugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/systemUIPlugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt new file mode 100644 index 0000000000..8ba8e40768 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt @@ -0,0 +1,122 @@ +/* + * 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.systemui.plugins.clocks + +import com.android.internal.annotations.Keep +import org.json.JSONObject + +/** Identifies a clock design */ +typealias ClockId = String + +data class AodClockBurnInModel( + val scale: Float, + val translationX: Float, + val translationY: Float, +) + +/** Tick rates for clocks */ +enum class ClockTickRate(val value: Int) { + PER_MINUTE(2), // Update the clock once per minute. + PER_SECOND(1), // Update the clock once per second. + PER_FRAME(0), // Update the clock every second. +} + +/** Some data about a clock design */ +data class ClockMetadata( + val clockId: ClockId, +) + +/** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */ +data class ClockConfig( + val id: String, + + /** Localized name of the clock */ + val name: String, + + /** Localized accessibility description for the clock */ + val description: String, + + /** Transition to AOD should move smartspace like large clock instead of small clock */ + val useAlternateSmartspaceAODTransition: Boolean = false, + + /** True if the clock will react to tone changes in the seed color. */ + val isReactiveToTone: Boolean = true, + + /** True if the clock is large frame clock, which will use weather in compose. */ + val useCustomClockScene: Boolean = false, +) + +/** Render configuration options for a clock face. Modifies the way SystemUI behaves. */ +data class ClockFaceConfig( + /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */ + val tickRate: ClockTickRate = ClockTickRate.PER_MINUTE, + + /** Call to check whether the clock consumes weather data */ + val hasCustomWeatherDataDisplay: Boolean = false, + + /** + * Whether this clock has a custom position update animation. If true, the keyguard will call + * `onPositionUpdated` to notify the clock of a position update animation. If false, a default + * animation will be used (e.g. a simple translation). + */ + val hasCustomPositionUpdatedAnimation: Boolean = false, + + /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */ + val useCustomClockScene: Boolean = false, +) + +/** Structure for keeping clock-specific settings */ +@Keep +data class ClockSettings( + val clockId: ClockId? = null, + val seedColor: Int? = null, +) { + // Exclude metadata from equality checks + var metadata: JSONObject = JSONObject() + + companion object { + private val KEY_CLOCK_ID = "clockId" + private val KEY_SEED_COLOR = "seedColor" + private val KEY_METADATA = "metadata" + + fun serialize(setting: ClockSettings?): String { + if (setting == null) { + return "" + } + + return JSONObject() + .put(KEY_CLOCK_ID, setting.clockId) + .put(KEY_SEED_COLOR, setting.seedColor) + .put(KEY_METADATA, setting.metadata) + .toString() + } + + fun deserialize(jsonStr: String?): ClockSettings? { + if (jsonStr.isNullOrEmpty()) { + return null + } + + val json = JSONObject(jsonStr) + val result = + ClockSettings( + if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null, + if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null + ) + if (!json.isNull(KEY_METADATA)) { + result.metadata = json.getJSONObject(KEY_METADATA) + } + return result + } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/clocks/WeatherData.kt b/systemUIPlugin/src/com/android/systemui/plugins/clocks/WeatherData.kt new file mode 100644 index 0000000000..789a47304e --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/clocks/WeatherData.kt @@ -0,0 +1,123 @@ +package com.android.systemui.plugins.clocks + +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.annotation.VisibleForTesting + +typealias WeatherTouchAction = (View) -> Unit + +data class WeatherData( + val description: String, + val state: WeatherStateIcon, + val useCelsius: Boolean, + val temperature: Int, + val touchAction: WeatherTouchAction? = null, +) { + companion object { + const val DEBUG = true + private const val TAG = "WeatherData" + @VisibleForTesting const val DESCRIPTION_KEY = "description" + @VisibleForTesting const val STATE_KEY = "state" + @VisibleForTesting const val USE_CELSIUS_KEY = "use_celsius" + @VisibleForTesting const val TEMPERATURE_KEY = "temperature" + private const val INVALID_WEATHER_ICON_STATE = -1 + + fun fromBundle(extras: Bundle, touchAction: WeatherTouchAction? = null): WeatherData? { + val description = extras.getString(DESCRIPTION_KEY) + val state = + WeatherStateIcon.fromInt(extras.getInt(STATE_KEY, INVALID_WEATHER_ICON_STATE)) + val temperature = readIntFromBundle(extras, TEMPERATURE_KEY) + if ( + description == null || + state == null || + !extras.containsKey(USE_CELSIUS_KEY) || + temperature == null + ) { + if (DEBUG) { + Log.w(TAG, "Weather data did not parse from $extras") + } + return null + } else { + val result = + WeatherData( + description = description, + state = state, + useCelsius = extras.getBoolean(USE_CELSIUS_KEY), + temperature = temperature, + touchAction = touchAction + ) + if (DEBUG) { + Log.i(TAG, "Weather data parsed $result from $extras") + } + return result + } + } + + private fun readIntFromBundle(extras: Bundle, key: String): Int? = + try { + extras.getString(key)?.toInt() + } catch (e: Exception) { + null + } + } + + // Values for WeatherStateIcon must stay in sync with go/g3-WeatherStateIcon + enum class WeatherStateIcon(val id: Int) { + UNKNOWN_ICON(0), + + // Clear, day & night. + SUNNY(1), + CLEAR_NIGHT(2), + + // Mostly clear, day & night. + MOSTLY_SUNNY(3), + MOSTLY_CLEAR_NIGHT(4), + + // Partly cloudy, day & night. + PARTLY_CLOUDY(5), + PARTLY_CLOUDY_NIGHT(6), + + // Mostly cloudy, day & night. + MOSTLY_CLOUDY_DAY(7), + MOSTLY_CLOUDY_NIGHT(8), + CLOUDY(9), + HAZE_FOG_DUST_SMOKE(10), + DRIZZLE(11), + HEAVY_RAIN(12), + SHOWERS_RAIN(13), + + // Scattered showers, day & night. + SCATTERED_SHOWERS_DAY(14), + SCATTERED_SHOWERS_NIGHT(15), + + // Isolated scattered thunderstorms, day & night. + ISOLATED_SCATTERED_TSTORMS_DAY(16), + ISOLATED_SCATTERED_TSTORMS_NIGHT(17), + STRONG_TSTORMS(18), + BLIZZARD(19), + BLOWING_SNOW(20), + FLURRIES(21), + HEAVY_SNOW(22), + + // Scattered snow showers, day & night. + SCATTERED_SNOW_SHOWERS_DAY(23), + SCATTERED_SNOW_SHOWERS_NIGHT(24), + SNOW_SHOWERS_SNOW(25), + MIXED_RAIN_HAIL_RAIN_SLEET(26), + SLEET_HAIL(27), + TORNADO(28), + TROPICAL_STORM_HURRICANE(29), + WINDY_BREEZY(30), + WINTRY_MIX_RAIN_SNOW(31); + + companion object { + fun fromInt(value: Int) = values().firstOrNull { it.id == value } + } + } + + override fun toString(): String { + val unit = if (useCelsius) "C" else "F" + return "$state (\"$description\") $temperature°$unit" + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/clocks/ZenData.kt b/systemUIPlugin/src/com/android/systemui/plugins/clocks/ZenData.kt new file mode 100644 index 0000000000..e927ec3c85 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/clocks/ZenData.kt @@ -0,0 +1,22 @@ +package com.android.systemui.plugins.clocks + +import android.provider.Settings.Global.ZEN_MODE_ALARMS +import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS +import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS +import android.provider.Settings.Global.ZEN_MODE_OFF + +data class ZenData( + val zenMode: ZenMode, + val descriptionId: String?, +) { + enum class ZenMode(val zenMode: Int) { + OFF(ZEN_MODE_OFF), + IMPORTANT_INTERRUPTIONS(ZEN_MODE_IMPORTANT_INTERRUPTIONS), + NO_INTERRUPTIONS(ZEN_MODE_NO_INTERRUPTIONS), + ALARMS(ZEN_MODE_ALARMS); + + companion object { + fun fromInt(zenMode: Int) = values().firstOrNull { it.zenMode == zenMode } + } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt b/systemUIPlugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt new file mode 100644 index 0000000000..50b3f78a49 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt @@ -0,0 +1,58 @@ +/* + * 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.systemui.plugins.log + +/** + * Base interface for a logger that logs changes in table format. + * + * This is a plugin interface for classes outside of SystemUI core. + */ +interface TableLogBufferBase { + /** + * Logs a String? change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: String?) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs a String? change. */ + fun logChange(prefix: String, columnName: String, value: String?, isInitial: Boolean) + + /** + * Logs a Boolean change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: Boolean) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs a Boolean change. */ + fun logChange(prefix: String, columnName: String, value: Boolean, isInitial: Boolean) + + /** + * Logs an Int? change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: Int?) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs an Int? change. */ + fun logChange(prefix: String, columnName: String, value: Int?, isInitial: Boolean) +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/qs/QS.java b/systemUIPlugin/src/com/android/systemui/plugins/qs/QS.java new file mode 100644 index 0000000000..bf58eee9a9 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/qs/QS.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins.qs; + +import android.view.View; + +import androidx.annotation.FloatRange; + +import com.android.systemui.plugins.FragmentBase; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; +import com.android.systemui.plugins.qs.QS.HeightListener; + +import java.util.function.Consumer; + +/** + * Fragment that contains QS in the notification shade. Most of the interface is for + * handling the expand/collapsing of the view interaction. + */ +@ProvidesInterface(action = QS.ACTION, version = QS.VERSION) +@DependsOn(target = HeightListener.class) +public interface QS extends FragmentBase { + + String ACTION = "com.android.systemui.action.PLUGIN_QS"; + + int VERSION = 15; + + String TAG = "QS"; + + void setPanelView(HeightListener notificationPanelView); + + void hideImmediately(); + int getQsMinExpansionHeight(); + int getDesiredHeight(); + void setHeightOverride(int desiredHeight); + void setHeaderClickable(boolean qsExpansionEnabled); + boolean isCustomizing(); + /** Close the QS customizer, if it is open. */ + void closeCustomizer(); + void setOverscrolling(boolean overscrolling); + void setExpanded(boolean qsExpanded); + void setListening(boolean listening); + + /** + * Set whether QQS/QS is visible or not. + * + * This is different from setExpanded, as it will be true when QQS is visible. In particular, + * it should be false when device is locked and only notifications (in lockscreen) are visible. + */ + void setQsVisible(boolean qsVisible); + boolean isShowingDetail(); + void closeDetail(); + void animateHeaderSlidingOut(); + + /** + * Asks QS to update its presentation, according to {@code NotificationPanelViewController}. + * @param qsExpansionFraction How much each UI element in QS should be expanded (QQS to QS.) + * @param panelExpansionFraction Whats the expansion of the whole shade. + * @param headerTranslation How much we should vertically translate QS. + * @param squishinessFraction Fraction that affects tile height. 0 when collapsed, + * 1 when expanded. + */ + void setQsExpansion(float qsExpansionFraction, float panelExpansionFraction, + float headerTranslation, float squishinessFraction); + void setHeaderListening(boolean listening); + void notifyCustomizeChanged(); + void setContainerController(QSContainerController controller); + + /** + * Provide an action to collapse if expanded or expand if collapsed. + * @param action + */ + void setCollapseExpandAction(Runnable action); + + /** + * Returns the height difference between the QSPanel container and the QuickQSPanel container + */ + int getHeightDiff(); + + View getHeader(); + + default void setHasNotifications(boolean hasNotifications) { + } + + /** Sets whether the squishiness fraction should be updated on the media host. */ + default void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) {} + + /** + * Should touches from the notification panel be disallowed? + * The notification panel might grab any touches rom QS at any time to collapse the shade. + * We should disallow that in case we are showing the detail panel. + */ + default boolean disallowPanelTouches() { + return isShowingDetail(); + } + + /** + * If QS should translate as we pull it down, or if it should be static. + */ + void setInSplitShade(boolean shouldTranslate); + + /** + * Sets the progress of the transition to full shade on the lockscreen. + * + * @param isTransitioningToFullShade + * whether the transition to full shade is in progress. This might be {@code true}, even + * though {@code qsTransitionFraction} is still 0. + * The reason for that is that on some device configurations, the QS transition has a + * start delay compared to the overall transition. + * + * @param qsTransitionFraction + * the fraction of the QS transition progress, from 0 to 1. + * + * @param qsSquishinessFraction + * the fraction of the QS "squish" transition progress, from 0 to 1. + */ + default void setTransitionToFullShadeProgress( + boolean isTransitioningToFullShade, + @FloatRange(from = 0.0, to = 1.0) float qsTransitionFraction, + @FloatRange(from = 0.0, to = 1.0) float qsSquishinessFraction) {} + + /** + * A rounded corner clipping that makes QS feel as if it were behind everything. + */ + void setFancyClipping(int leftInset, int top, int rightInset, int bottom, int cornerRadius, + boolean visible, boolean fullWidth); + + /** + * @return if quick settings is fully collapsed currently + */ + default boolean isFullyCollapsed() { + return true; + } + + /** + * Add a listener for when the collapsed media visibility changes. + */ + void setCollapsedMediaVisibilityChangedListener(Consumer listener); + + /** + * Set a scroll listener for the QSPanel container + */ + default void setScrollListener(ScrollListener scrollListener) {} + + /** + * Sets the amount of vertical over scroll that should be performed on QS. + */ + default void setOverScrollAmount(int overScrollAmount) {} + + /** + * Sets whether the notification panel is using the full width of the screen. Typically true on + * small screens and false on large screens. + */ + void setIsNotificationPanelFullWidth(boolean isFullWidth); + + /** + * Callback for when QSPanel container is scrolled + */ + @ProvidesInterface(version = ScrollListener.VERSION) + interface ScrollListener { + int VERSION = 1; + void onQsPanelScrollChanged(int scrollY); + } + + @ProvidesInterface(version = HeightListener.VERSION) + interface HeightListener { + int VERSION = 1; + void onQsHeightChanged(); + } + +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/qs/QSContainerController.kt b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSContainerController.kt new file mode 100644 index 0000000000..9c7fbe8842 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSContainerController.kt @@ -0,0 +1,11 @@ +package com.android.systemui.plugins.qs + +interface QSContainerController { + fun setCustomizerAnimating(animating: Boolean) + + fun setCustomizerShowing(showing: Boolean) = setCustomizerShowing(showing, 0L) + + fun setCustomizerShowing(showing: Boolean, animationDuration: Long) + + fun setDetailShowing(showing: Boolean) +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/qs/QSFactory.java b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSFactory.java new file mode 100644 index 0000000000..3ac6422c03 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSFactory.java @@ -0,0 +1,34 @@ +/* + * 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.systemui.plugins.qs; + +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Plugin that has the ability to create or override any part of + * QS tiles. + */ +@ProvidesInterface(action = QSFactory.ACTION, version = QSFactory.VERSION) +@DependsOn(target = QSTile.class) +@DependsOn(target = QSTileView.class) +public interface QSFactory extends Plugin { + + String ACTION = "com.android.systemui.action.PLUGIN_QS_FACTORY"; + int VERSION = 3; + + QSTile createTile(String tileSpec); +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/qs/QSIconView.java b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSIconView.java new file mode 100644 index 0000000000..0cdb509a52 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSIconView.java @@ -0,0 +1,35 @@ +/* + * 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.systemui.plugins.qs; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.plugins.annotations.ProvidesInterface; +import com.android.systemui.plugins.qs.QSTile.State; + +@ProvidesInterface(version = QSIconView.VERSION) +public abstract class QSIconView extends ViewGroup { + public static final int VERSION = 1; + + public QSIconView(Context context) { + super(context); + } + + public abstract void setIcon(State state, boolean allowAnimations); + public abstract void disableAnimation(); + public abstract View getIconView(); +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/qs/QSTile.java b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSTile.java new file mode 100644 index 0000000000..d13c750827 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSTile.java @@ -0,0 +1,314 @@ +/* + * 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.systemui.plugins.qs; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.metrics.LogMaker; +import android.service.quicksettings.Tile; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.InstanceId; +import com.android.systemui.animation.Expandable; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; +import com.android.systemui.plugins.qs.QSTile.Callback; +import com.android.systemui.plugins.qs.QSTile.Icon; +import com.android.systemui.plugins.qs.QSTile.State; + +import java.util.Objects; +import java.util.function.Supplier; + +@ProvidesInterface(version = QSTile.VERSION) +@DependsOn(target = QSIconView.class) +@DependsOn(target = Callback.class) +@DependsOn(target = Icon.class) +@DependsOn(target = State.class) +public interface QSTile { + int VERSION = 4; + + String getTileSpec(); + + boolean isAvailable(); + void setTileSpec(String tileSpec); + + @Deprecated default void clearState() {} + void refreshState(); + + void addCallback(Callback callback); + void removeCallback(Callback callback); + void removeCallbacks(); + + /** + * The tile was clicked. + * + * @param expandable {@link Expandable} that was clicked. + */ + void click(@Nullable Expandable expandable); + + /** + * The tile secondary click was triggered. + * + * @param expandable {@link Expandable} that was clicked. + */ + void secondaryClick(@Nullable Expandable expandable); + + /** + * The tile was long clicked. + * + * @param expandable {@link Expandable} that was clicked. + */ + void longClick(@Nullable Expandable expandable); + + void userSwitch(int currentUser); + + /** + * @deprecated not needed as {@link com.android.internal.logging.UiEvent} will use + * {@link #getMetricsSpec} + */ + @Deprecated + int getMetricsCategory(); + + void setListening(Object client, boolean listening); + void setDetailListening(boolean show); + + void destroy(); + + CharSequence getTileLabel(); + + State getState(); + + default LogMaker populate(LogMaker logMaker) { + return logMaker; + } + + /** + * Return a string to be used to identify the tile in UiEvents. + */ + default String getMetricsSpec() { + return getClass().getSimpleName(); + } + + /** + * Return an {@link InstanceId} to be used to identify the tile in UiEvents. + */ + InstanceId getInstanceId(); + + default boolean isTileReady() { + return false; + } + + /** + * Return whether the tile is set to its listening state and therefore receiving updates and + * refreshes from controllers + */ + boolean isListening(); + + @ProvidesInterface(version = Callback.VERSION) + interface Callback { + static final int VERSION = 2; + void onStateChanged(State state); + } + + @ProvidesInterface(version = Icon.VERSION) + public static abstract class Icon { + public static final int VERSION = 1; + abstract public Drawable getDrawable(Context context); + + public Drawable getInvisibleDrawable(Context context) { + return getDrawable(context); + } + + @Override + public int hashCode() { + return Icon.class.hashCode(); + } + + public int getPadding() { + return 0; + } + + @Override + @NonNull + public String toString() { + return "Icon"; + } + } + + @ProvidesInterface(version = State.VERSION) + public static class State { + public static final int VERSION = 1; + public static final int DEFAULT_STATE = Tile.STATE_ACTIVE; + + public Icon icon; + public Supplier iconSupplier; + public int state = DEFAULT_STATE; + public CharSequence label; + @Nullable public CharSequence secondaryLabel; + public CharSequence contentDescription; + @Nullable public CharSequence stateDescription; + public CharSequence dualLabelContentDescription; + public boolean disabledByPolicy; + public boolean dualTarget = false; + public boolean isTransient = false; + public String expandedAccessibilityClassName; + public boolean handlesLongClick = true; + @Nullable + public Drawable sideViewCustomDrawable; + public String spec; + + /** Get the state text. */ + public CharSequence getStateText(int arrayResId, Resources resources) { + if (state == Tile.STATE_UNAVAILABLE || this instanceof QSTile.BooleanState) { + String[] array = resources.getStringArray(arrayResId); + return array[state]; + } else { + return ""; + } + } + + /** Get the text for secondaryLabel. */ + public CharSequence getSecondaryLabel(CharSequence stateText) { + // Use a local reference as the value might change from other threads + CharSequence localSecondaryLabel = secondaryLabel; + if (TextUtils.isEmpty(localSecondaryLabel)) { + return stateText; + } + return localSecondaryLabel; + } + + public boolean copyTo(State other) { + if (other == null) throw new IllegalArgumentException(); + if (!other.getClass().equals(getClass())) throw new IllegalArgumentException(); + final boolean changed = !Objects.equals(other.spec, spec) + || !Objects.equals(other.icon, icon) + || !Objects.equals(other.iconSupplier, iconSupplier) + || !Objects.equals(other.label, label) + || !Objects.equals(other.secondaryLabel, secondaryLabel) + || !Objects.equals(other.contentDescription, contentDescription) + || !Objects.equals(other.stateDescription, stateDescription) + || !Objects.equals(other.dualLabelContentDescription, + dualLabelContentDescription) + || !Objects.equals(other.expandedAccessibilityClassName, + expandedAccessibilityClassName) + || !Objects.equals(other.disabledByPolicy, disabledByPolicy) + || !Objects.equals(other.state, state) + || !Objects.equals(other.isTransient, isTransient) + || !Objects.equals(other.dualTarget, dualTarget) + || !Objects.equals(other.handlesLongClick, handlesLongClick) + || !Objects.equals(other.sideViewCustomDrawable, sideViewCustomDrawable); + other.spec = spec; + other.icon = icon; + other.iconSupplier = iconSupplier; + other.label = label; + other.secondaryLabel = secondaryLabel; + other.contentDescription = contentDescription; + other.stateDescription = stateDescription; + other.dualLabelContentDescription = dualLabelContentDescription; + other.expandedAccessibilityClassName = expandedAccessibilityClassName; + other.disabledByPolicy = disabledByPolicy; + other.state = state; + other.dualTarget = dualTarget; + other.isTransient = isTransient; + other.handlesLongClick = handlesLongClick; + other.sideViewCustomDrawable = sideViewCustomDrawable; + return changed; + } + + @Override + public String toString() { + return toStringBuilder().toString(); + } + + // Used in dumps to determine current state of a tile. + // This string may be used for CTS testing of tiles, so removing elements is discouraged. + protected StringBuilder toStringBuilder() { + final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('['); + sb.append("spec=").append(spec); + sb.append(",icon=").append(icon); + sb.append(",iconSupplier=").append(iconSupplier); + sb.append(",label=").append(label); + sb.append(",secondaryLabel=").append(secondaryLabel); + sb.append(",contentDescription=").append(contentDescription); + sb.append(",stateDescription=").append(stateDescription); + sb.append(",dualLabelContentDescription=").append(dualLabelContentDescription); + sb.append(",expandedAccessibilityClassName=").append(expandedAccessibilityClassName); + sb.append(",disabledByPolicy=").append(disabledByPolicy); + sb.append(",dualTarget=").append(dualTarget); + sb.append(",isTransient=").append(isTransient); + sb.append(",state=").append(state); + sb.append(",sideViewCustomDrawable=").append(sideViewCustomDrawable); + return sb.append(']'); + } + + public State copy() { + State state = new State(); + copyTo(state); + return state; + } + } + + /** + * Distinguished from [BooleanState] for use-case purposes such as allowing null secondary label + */ + @ProvidesInterface(version = AdapterState.VERSION) + class AdapterState extends State { + public static final int VERSION = 1; + public boolean value; + public boolean forceExpandIcon; + + @Override + public boolean copyTo(State other) { + final AdapterState o = (AdapterState) other; + final boolean changed = super.copyTo(other) + || o.value != value + || o.forceExpandIcon != forceExpandIcon; + o.value = value; + o.forceExpandIcon = forceExpandIcon; + return changed; + } + + @Override + protected StringBuilder toStringBuilder() { + final StringBuilder rt = super.toStringBuilder(); + rt.insert(rt.length() - 1, ",value=" + value); + rt.insert(rt.length() - 1, ",forceExpandIcon=" + forceExpandIcon); + return rt; + } + + @Override + public State copy() { + AdapterState state = new AdapterState(); + copyTo(state); + return state; + } + } + + @ProvidesInterface(version = BooleanState.VERSION) + class BooleanState extends AdapterState { + public static final int VERSION = 1; + + @Override + public State copy() { + BooleanState state = new BooleanState(); + copyTo(state); + return state; + } + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/qs/QSTileView.java b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSTileView.java new file mode 100644 index 0000000000..6c8949e510 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/qs/QSTileView.java @@ -0,0 +1,83 @@ +/* + * 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.systemui.plugins.qs; + +import android.content.Context; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.LinearLayout; + +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; +import com.android.systemui.plugins.qs.QSTile.State; + +@ProvidesInterface(version = QSTileView.VERSION) +@DependsOn(target = QSIconView.class) +@DependsOn(target = QSTile.class) +public abstract class QSTileView extends LinearLayout { + public static final int VERSION = 3; + + public QSTileView(Context context) { + super(context); + } + + public abstract View updateAccessibilityOrder(View previousView); + + /** + * Returns a {@link QSIconView} containing only the icon for this tile. Use + * {@link #getIconWithBackground()} to retrieve the entire tile (background & peripherals + * included). + */ + public abstract QSIconView getIcon(); + + /** + * Returns a {@link View} containing the icon for this tile along with the accompanying + * background circle/peripherals. To retrieve only the inner icon, use {@link #getIcon()}. + */ + public abstract View getIconWithBackground(); + + /** + * Returns the {@link View} containing the icon on the right + * + * @see com.android.systemui.qs.tileimpl.QSTileViewHorizontal#sideView + */ + public View getSecondaryIcon() { + return null; + } + public abstract void init(QSTile tile); + public abstract void onStateChanged(State state); + + public abstract int getDetailY(); + + public View getLabel() { + return null; + } + + public View getLabelContainer() { + return null; + } + + public View getSecondaryLabel() { + return null; + } + + /** Sets the index of this tile in its layout */ + public abstract void setPosition(int position); + + /** Get the duration of a visuo-haptic long-press effect */ + public int getLongPressEffectDuration() { + return ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/statusbar/DozeParameters.java b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/DozeParameters.java new file mode 100644 index 0000000000..678eb31304 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/DozeParameters.java @@ -0,0 +1,32 @@ +/* + * 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.systemui.plugins.statusbar; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Retrieve doze information + */ +@ProvidesInterface(version = DozeParameters.VERSION) +public interface DozeParameters { + int VERSION = 1; + + /** + * Whether to doze when the screen turns off + */ + boolean shouldControlScreenOff(); +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java new file mode 100644 index 0000000000..94fdbae832 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java @@ -0,0 +1,255 @@ +/* + * 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.systemui.plugins.statusbar; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Point; +import android.service.notification.StatusBarNotification; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; +import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem; +import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; +import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; + +import java.util.ArrayList; + +@ProvidesInterface(action = NotificationMenuRowPlugin.ACTION, + version = NotificationMenuRowPlugin.VERSION) +@DependsOn(target = OnMenuEventListener.class) +@DependsOn(target = MenuItem.class) +@DependsOn(target = NotificationSwipeActionHelper.class) +@DependsOn(target = SnoozeOption.class) +public interface NotificationMenuRowPlugin extends Plugin { + + public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_MENU_ROW"; + public static final int VERSION = 5; + + @ProvidesInterface(version = OnMenuEventListener.VERSION) + public interface OnMenuEventListener { + public static final int VERSION = 1; + + public void onMenuClicked(View row, int x, int y, MenuItem menu); + + public void onMenuReset(View row); + + public void onMenuShown(View row); + } + + @ProvidesInterface(version = MenuItem.VERSION) + public interface MenuItem { + public static final int VERSION = 1; + + public View getMenuView(); + + public View getGutsView(); + + public String getContentDescription(); + } + + /** + * @return a list of items to populate the menu 'behind' a notification. + */ + public ArrayList getMenuItems(Context context); + + /** + * @return the {@link MenuItem} to display when a notification is long pressed. + */ + public MenuItem getLongpressMenuItem(Context context); + + /** + * @return the {@link MenuItem} to display when feedback icon is pressed. + */ + public MenuItem getFeedbackMenuItem(Context context); + + /** + * @return the {@link MenuItem} to display when snooze item is pressed. + */ + public MenuItem getSnoozeMenuItem(Context context); + + public void setMenuItems(ArrayList items); + + /** + * If this returns {@code true}, then the menu row will bind and fade in the notification guts + * view for the menu item it holds. + * + * @see #menuItemToExposeOnSnap() + * @return whether or not to immediately expose the notification guts + */ + default boolean shouldShowGutsOnSnapOpen() { + return false; + } + + /** + * When #shouldShowGutsOnExpose is true, this method must return the menu item to expose on + * #onSnapOpen. Otherwise we will fall back to the default behavior of fading in the menu row + * + * @return the {@link MenuItem} containing the NotificationGuts that should be exposed + */ + @Nullable + default MenuItem menuItemToExposeOnSnap() { + return null; + } + + /** + * Get the origin for the circular reveal animation when expanding the notification guts. Only + * used when #shouldShowGutsOnSnapOpen is true + * @return the x,y coordinates for the start of the animation + */ + @Nullable + default Point getRevealAnimationOrigin() { + return new Point(0, 0); + } + + public void setMenuClickListener(OnMenuEventListener listener); + + public void setAppName(String appName); + + public void createMenu(ViewGroup parent, StatusBarNotification sbn); + + public void resetMenu(); + + public View getMenuView(); + + /** + * Get the target position that a notification row should be snapped open to in order to reveal + * the menu. This is generally determined by the number of icons in the notification menu and the + * size of each icon. This method accounts for whether the menu appears on the left or ride side + * of the parent notification row. + * + + * @return an int representing the x-offset in pixels that the notification should snap open to. + * Positive values imply that the notification should be offset to the right to reveal the menu, + * and negative alues imply that the notification should be offset to the right. + */ + public int getMenuSnapTarget(); + + /** + * Determines whether or not the menu should be shown in response to user input. + * @return true if the menu should be shown, false otherwise. + */ + public boolean shouldShowMenu(); + + /** + * Determines whether the menu is currently visible. + * @return true if the menu is visible, false otherwise. + */ + public boolean isMenuVisible(); + + /** + * Determines whether a given movement is towards or away from the current location of the menu. + * @param movement + * @return true if the movement is towards the menu, false otherwise. + */ + public boolean isTowardsMenu(float movement); + + /** + * Determines whether the menu should snap closed instead of dismissing the + * parent notification, as a function of its current state. + * + * @return true if the menu should snap closed, false otherwise. + */ + public boolean shouldSnapBack(); + + /** + * Determines whether the menu was previously snapped open to the same side that it is currently + * being shown on. + * @return true if the menu is snapped open to the same side on which it currently appears, + * false otherwise. + */ + public boolean isSnappedAndOnSameSide(); + + /** + * Determines whether the notification the menu is attached to is able to be dismissed. + * @return true if the menu's parent notification is dismissable, false otherwise. + */ + public boolean canBeDismissed(); + + /** + * Determines whether the menu should remain open given its current state, or snap closed. + * @return true if the menu should remain open, false otherwise. + */ + public boolean isWithinSnapMenuThreshold(); + + /** + * Determines whether the menu has been swiped far enough to snap open. + * @return true if the menu has been swiped far enough to open, false otherwise. + */ + public boolean isSwipedEnoughToShowMenu(); + + public default boolean onInterceptTouchEvent(View view, MotionEvent ev) { + return false; + } + + public default boolean shouldUseDefaultMenuItems() { + return false; + } + + /** + * Callback used to signal the menu that its parent's translation has changed. + * @param translation The new x-translation of the menu as a position (not an offset). + */ + public void onParentTranslationUpdate(float translation); + + /** + * Callback used to signal the menu that its parent's height has changed. + */ + public void onParentHeightUpdate(); + + /** + * Callback used to signal the menu that its parent notification has been updated. + * @param sbn + */ + public void onNotificationUpdated(StatusBarNotification sbn); + + /** + * Callback used to signal the menu that a user is moving the parent notification. + * @param delta The change in the parent notification's position. + */ + public void onTouchMove(float delta); + + /** + * Callback used to signal the menu that a user has begun touching its parent notification. + */ + public void onTouchStart(); + + /** + * Callback used to signal the menu that a user has finished touching its parent notification. + */ + public void onTouchEnd(); + + /** + * Callback used to signal the menu that it has been snapped closed. + */ + public void onSnapClosed(); + + /** + * Callback used to signal the menu that it has been snapped open. + */ + public void onSnapOpen(); + + /** + * Callback used to signal the menu that its parent notification has been dismissed. + */ + public void onDismiss(); + + public default void onConfigurationChanged() { } + +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java new file mode 100644 index 0000000000..59911b233e --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java @@ -0,0 +1,70 @@ +/* + * 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.systemui.plugins.statusbar; + +import android.service.notification.SnoozeCriterion; +import android.service.notification.StatusBarNotification; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; +import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; + +@ProvidesInterface(version = NotificationSwipeActionHelper.VERSION) +@DependsOn(target = SnoozeOption.class) +public interface NotificationSwipeActionHelper { + public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_SWIPE_ACTION"; + + public static final int VERSION = 1; + + /** + * Call this to dismiss a notification. + */ + public void dismiss(View animView, float velocity); + + /** + * Call this to snap a notification to provided {@code targetLeft}. + */ + public void snapOpen(View animView, int targetLeft, float velocity); + + /** + * Call this to snooze a notification based on the provided {@link SnoozeOption}. + */ + public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption); + + public float getMinDismissVelocity(); + + public boolean isDismissGesture(MotionEvent ev); + + /** Returns true if the gesture should be rejected. */ + boolean isFalseGesture(); + + @ProvidesInterface(version = SnoozeOption.VERSION) + public interface SnoozeOption { + public static final int VERSION = 2; + + public SnoozeCriterion getSnoozeCriterion(); + + public CharSequence getDescription(); + + public CharSequence getConfirmation(); + + public int getMinutesToSnoozeFor(); + + public AccessibilityAction getAccessibilityAction(); + } +} diff --git a/systemUIPlugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java new file mode 100644 index 0000000000..4c5ca2fd90 --- /dev/null +++ b/systemUIPlugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java @@ -0,0 +1,135 @@ +/* + * 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.systemui.plugins.statusbar; + +import com.android.systemui.plugins.annotations.DependsOn; +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Sends updates to {@link StateListener}s about changes to the status bar state and dozing state + */ +@ProvidesInterface(version = StatusBarStateController.VERSION) +@DependsOn(target = StatusBarStateController.StateListener.class) +public interface StatusBarStateController { + int VERSION = 1; + + /** + * Current status bar state + */ + int getState(); + + /** + * Is device dozing. Dozing is when the screen is in AOD or asleep given that + * {@link com.android.systemui.doze.DozeService} is configured. + */ + boolean isDozing(); + + /** + * Is the status bar panel expanded. + */ + boolean isExpanded(); + + /** + * Is device pulsing. + */ + boolean isPulsing(); + + /** + * Is device dreaming. This method is more inclusive than + * {@link android.service.dreams.IDreamManager.isDreaming}, as it will return true during the + * dream's wake-up phase. + */ + boolean isDreaming(); + + /** + * Adds a state listener + */ + void addCallback(StateListener listener); + + /** + * Removes callback from listeners + */ + void removeCallback(StateListener listener); + + /** + * Get amount of doze + */ + float getDozeAmount(); + + /** + * Listener for StatusBarState updates + */ + @ProvidesInterface(version = StateListener.VERSION) + public interface StateListener { + int VERSION = 1; + + /** + * Callback before the new state is applied, for those who need to preempt the change. + */ + default void onStatePreChange(int oldState, int newState) { + } + + /** + * Callback after all listeners have had a chance to update based on the state change + */ + default void onStatePostChange() { + } + + /** + * Required callback. Get the new state and do what you will with it. Keep in mind that + * other listeners are typically unordered and don't rely on your work being done before + * other peers. + * + * Only called if the state is actually different. + */ + default void onStateChanged(int newState) { + } + + /** + * Callback to be notified about upcoming state changes. Typically, is immediately followed + * by #onStateChanged, unless there was an intentional delay in updating the state changed. + */ + default void onUpcomingStateChanged(int upcomingState) {} + + /** + * Callback to be notified when Dozing changes. Dozing is stored separately from state. + */ + default void onDozingChanged(boolean isDozing) {} + + /** + * Callback to be notified when Dreaming changes. Dreaming is stored separately from state. + */ + default void onDreamingChanged(boolean isDreaming) {} + + /** + * Callback to be notified when the doze amount changes. Useful for animations. + * Note: this will be called for each animation frame. Please be careful to avoid + * performance regressions. + */ + default void onDozeAmountChanged(float linear, float eased) {} + + /** + * Callback to be notified when the pulsing state changes + */ + default void onPulsingChanged(boolean pulsing) {} + + /** + * Callback to be notified when the expanded state of the status bar changes + */ + default void onExpandedChanged(boolean isExpanded) {} + } +} diff --git a/systemUIPlugin/update_plugin_lib.sh b/systemUIPlugin/update_plugin_lib.sh new file mode 100644 index 0000000000..34f4895de3 --- /dev/null +++ b/systemUIPlugin/update_plugin_lib.sh @@ -0,0 +1,15 @@ +#!/bin/bash +cd $ANDROID_BUILD_TOP/frameworks/base/packages/SystemUI/plugin +# Clear out anything old. +rm -rf /tmp/plugin_classes/ +mkdir /tmp/plugin_classes + +# Compile the jar +javac -cp $ANDROID_BUILD_TOP/out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar:$ANDROID_BUILD_TOP/out/target/common/obj/JAVA_LIBRARIES/core-all_intermediates/classes.jar `find ../plugin*/src -name *.java` -d /tmp/plugin_classes/ +echo "" >> /tmp/plugin_classes/manifest.txt +jar cvfm SystemUIPluginLib.jar /tmp/plugin_classes/manifest.txt -C /tmp/plugin_classes . + +# Place the jar and update the latest +mv SystemUIPluginLib.jar ./SystemUIPluginLib-`date +%m-%d-%Y`.jar +rm SystemUIPluginLib-latest.jar +ln -s SystemUIPluginLib-`date +%m-%d-%Y`.jar SystemUIPluginLib-latest.jar diff --git a/systemUIPluginCore/Android.bp b/systemUIPluginCore/Android.bp new file mode 100644 index 0000000000..4e39f1ac95 --- /dev/null +++ b/systemUIPluginCore/Android.bp @@ -0,0 +1,35 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +java_library { + sdk_version: "current", + name: "PluginCoreLib", + srcs: ["src/**/*.java"], + optimize: { + proguard_flags_files: ["proguard.flags"], + }, + + // Enforce that the library is built against java 8 so that there are + // no compatibility issues with launcher + java_version: "1.8", +} diff --git a/systemUIPluginCore/AndroidManifest.xml b/systemUIPluginCore/AndroidManifest.xml new file mode 100644 index 0000000000..df835fd8e3 --- /dev/null +++ b/systemUIPluginCore/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/systemUIPluginCore/build.gradle b/systemUIPluginCore/build.gradle new file mode 100644 index 0000000000..06ca335af0 --- /dev/null +++ b/systemUIPluginCore/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace "com.android.systemui.plugin_core" + buildFeatures { + aidl true + } + sourceSets { + main { + java.srcDirs = ['src'] + aidl.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + } + } +} + +addFrameworkJar('framework-15.jar') +compileOnlyCommonJars() diff --git a/systemUIPluginCore/proguard.flags b/systemUIPluginCore/proguard.flags new file mode 100644 index 0000000000..6240898b3b --- /dev/null +++ b/systemUIPluginCore/proguard.flags @@ -0,0 +1,11 @@ +# R8's full mode is a bit more aggressive in stripping annotations, but the +# SystemUI plugin architecture requires these annotations at runtime. The +# following rules are the minimal set necessary to ensure compatibility. +# For more details, see: +# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode +-keepattributes RuntimeVisible*Annotation*,AnnotationDefault + +-keep interface com.android.systemui.plugins.annotations.** { + *; +} +-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification @com.android.systemui.plugins.annotations.** class * diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/Plugin.java b/systemUIPluginCore/src/com/android/systemui/plugins/Plugin.java new file mode 100644 index 0000000000..8ff6c114dd --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/Plugin.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.android.systemui.plugins; + +import android.content.Context; + +import com.android.systemui.plugins.annotations.Requires; + +/** + * Plugins are separate APKs that + * are expected to implement interfaces provided by SystemUI. Their + * code is dynamically loaded into the SysUI process which can allow + * for multiple prototypes to be created and run on a single android + * build. + * + * PluginLifecycle: + *

+ *
+ * plugin.onCreate(Context sysuiContext, Context pluginContext);
+ * --- This is always called before any other calls
+ *
+ * pluginListener.onPluginConnected(Plugin p);
+ * --- This lets the plugin hook know that a plugin is now connected.
+ *
+ * ** Any other calls back and forth between sysui/plugin **
+ *
+ * pluginListener.onPluginDisconnected(Plugin p);
+ * --- Lets the plugin hook know that it should stop interacting with
+ *     this plugin and drop all references to it.
+ *
+ * plugin.onDestroy();
+ * --- Finally the plugin can perform any cleanup to ensure that its not
+ *     leaking into the SysUI process.
+ *
+ * Any time a plugin APK is updated the plugin is destroyed and recreated
+ * to load the new code/resources.
+ *
+ * 
+ * + * Creating plugin hooks: + * + * To create a plugin hook, first create an interface in + * frameworks/base/packages/SystemUI/plugin that extends Plugin. + * Include in it any hooks you want to be able to call into from + * sysui and create callback interfaces for anything you need to + * pass through into the plugin. + * + * Then to attach to any plugins simply add a plugin listener and + * onPluginConnected will get called whenever new plugins are installed, + * updated, or enabled. Like this example from SystemUIApplication: + * + *
+ * {@literal
+ * PluginManager.getInstance(this).addPluginListener(OverlayPlugin.COMPONENT,
+ *        new PluginListener() {
+ *        @Override
+ *        public void onPluginConnected(OverlayPlugin plugin) {
+ *            StatusBar phoneStatusBar = getComponent(StatusBar.class);
+ *            if (phoneStatusBar != null) {
+ *                plugin.setup(phoneStatusBar.getStatusBarWindow(),
+ *                phoneStatusBar.getNavigationBarView());
+ *            }
+ *        }
+ * }, OverlayPlugin.VERSION, true /* Allow multiple plugins *\/);
+ * }
+ * 
+ * Note the VERSION included here. Any time incompatible changes in the + * interface are made, this version should be changed to ensure old plugins + * aren't accidentally loaded. Since the plugin library is provided by + * SystemUI, default implementations can be added for new methods to avoid + * version changes when possible. + * + * Implementing a Plugin: + * + * See the ExamplePlugin for an example Android.mk on how to compile + * a plugin. Note that SystemUILib is not static for plugins, its classes + * are provided by SystemUI. + * + * Plugin security is based around a signature permission, so plugins must + * hold the following permission in their manifest. + * + *
+ * {@literal
+ * 
+ * }
+ * 
+ * + * A plugin is found through a querying for services, so to let SysUI know + * about it, create a service with a name that points at your implementation + * of the plugin interface with the action accompanying it: + * + *
+ * {@literal
+ * 
+ *    
+ *        
+ *    
+ * 
+ * }
+ * 
+ */ +public interface Plugin { + + /** + * @deprecated + * @see Requires + */ + default int getVersion() { + // Default of -1 indicates the plugin supports the new Requires model. + return -1; + } + + default void onCreate(Context sysuiContext, Context pluginContext) { + } + + default void onDestroy() { + } +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/PluginFragment.java b/systemUIPluginCore/src/com/android/systemui/plugins/PluginFragment.java new file mode 100644 index 0000000000..1bfa567b66 --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/PluginFragment.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; + +public abstract class PluginFragment extends Fragment implements Plugin { + + private Context mPluginContext; + + @Override + public void onCreate(Context sysuiContext, Context pluginContext) { + mPluginContext = pluginContext; + } + + @Override + public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) { + return super.onGetLayoutInflater(savedInstanceState).cloneInContext(getContext()); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + } + + @Override + public Context getContext() { + return mPluginContext; + } +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/PluginLifecycleManager.java b/systemUIPluginCore/src/com/android/systemui/plugins/PluginLifecycleManager.java new file mode 100644 index 0000000000..f0ce460430 --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/PluginLifecycleManager.java @@ -0,0 +1,61 @@ +/* + * 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.systemui.plugins; + +import android.content.ComponentName; + +import java.util.function.BiConsumer; + +/** + * Provides the ability for consumers to control plugin lifecycle. + * + * @param is the target plugin type + */ +public interface PluginLifecycleManager { + /** Returns the ComponentName of the target plugin. Maybe be called when not loaded. */ + ComponentName getComponentName(); + + /** Returns the package name of the target plugin. May be called when not loaded. */ + String getPackage(); + + /** Returns the currently loaded plugin instance (if plugin is loaded) */ + T getPlugin(); + + /** Log tag and messages will be sent to the provided Consumer */ + void setLogFunc(BiConsumer logConsumer); + + /** returns true if the plugin is currently loaded */ + default boolean isLoaded() { + return getPlugin() != null; + } + + /** + * Loads and creates the plugin instance if it does not exist. + * + * This will trigger {@link PluginListener#onPluginLoaded} with the new instance if it did not + * already exist. + */ + void loadPlugin(); + + /** + * Unloads and destroys the plugin instance if it exists. + * + * This will trigger {@link PluginListener#onPluginUnloaded} if a concrete plugin instance + * existed when this call was made. + */ + void unloadPlugin(); +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/PluginListener.java b/systemUIPluginCore/src/com/android/systemui/plugins/PluginListener.java new file mode 100644 index 0000000000..bd0bd8942d --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/PluginListener.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.content.Context; + +/** + * Interface for listening to plugins being connected and disconnected. + * + * The call order for a plugin is + * 1) {@link #onPluginAttached} + * Called when a new plugin is added to the device, or an existing plugin was replaced by + * the package manager. Will only be called once per package manager event. If multiple + * non-conflicting packages which have the same plugin interface are installed on the + * device, then this method can be called multiple times with different instances of + * {@link PluginLifecycleManager} (as long as `allowMultiple` was set to true when the + * listener was registered with {@link PluginManager#addPluginListener}). + * 2) {@link #onPluginLoaded} + * Called whenever a new instance of the plugin object is created and ready for use. Can be + * called multiple times per {@link PluginLifecycleManager}, but will always pass a newly + * created plugin object. {@link #onPluginUnloaded} with the previous plugin object will + * be called before another call to {@link #onPluginLoaded} is made. This method will be + * called once automatically after {@link #onPluginAttached}. Besides the initial call, + * {@link #onPluginLoaded} will occur due to {@link PluginLifecycleManager#loadPlugin}. + * 3) {@link #onPluginUnloaded} + * Called when a request to unload the plugin has been received. This can be triggered from + * a related call to {@link PluginLifecycleManager#unloadPlugin} or for any reason that + * {@link #onPluginDetached} would be triggered. + * 4) {@link #onPluginDetached} + * Called when the package is removed from the device, disabled, or replaced due to an + * external trigger. These are events from the android package manager. + * + * @param is the target plugin type + */ +public interface PluginListener { + /** + * Called when the plugin has been loaded and is ready to be used. + * This may be called multiple times if multiple plugins are allowed. + * It may also be called in the future if the plugin package changes + * and needs to be reloaded. + * + * @deprecated Migrate to {@link #onPluginLoaded} or {@link #onPluginAttached} + */ + @Deprecated + default void onPluginConnected(T plugin, Context pluginContext) { + // Optional + } + + /** + * Called when the plugin is first attached to the host application. {@link #onPluginLoaded} + * will be automatically called as well when first attached if true is returned. This may be + * called multiple times if multiple plugins are allowed. It may also be called in the future + * if the plugin package changes and needs to be reloaded. Each call to + * {@link #onPluginAttached} will provide a new or different {@link PluginLifecycleManager}. + * + * @return returning true will immediately load the plugin and call onPluginLoaded with the + * created object. false will skip loading, but the listener can load it at any time using the + * provided PluginLifecycleManager. Loading plugins immediately is the default behavior. + */ + default boolean onPluginAttached(PluginLifecycleManager manager) { + // Optional + return true; + } + + /** + * Called when a plugin has been uninstalled/updated and should be removed + * from use. + * + * @deprecated Migrate to {@link #onPluginDetached} or {@link #onPluginUnloaded} + */ + @Deprecated + default void onPluginDisconnected(T plugin) { + // Optional. + } + + /** + * Called when the plugin has been detached from the host application. Implementers should no + * longer attempt to reload it via this {@link PluginLifecycleManager}. If the package was + * updated and not removed, then {@link #onPluginAttached} will be called again when the updated + * package is available. + */ + default void onPluginDetached(PluginLifecycleManager manager) { + // Optional. + } + + /** + * Called when the plugin is loaded into the host's process and is available for use. This can + * happen several times if clients are using {@link PluginLifecycleManager} to manipulate a + * plugin's load state. Each call to {@link #onPluginLoaded} will have a matched call to + * {@link #onPluginUnloaded} when that plugin object should no longer be used. + */ + default void onPluginLoaded( + T plugin, + Context pluginContext, + PluginLifecycleManager manager + ) { + // Optional, default to deprecated version + onPluginConnected(plugin, pluginContext); + } + + /** + * Called when the plugin should no longer be used. Listeners should clean up all references to + * the relevant plugin so that it can be garbage collected. If the plugin object is required in + * the future a call can be made to {@link PluginLifecycleManager#loadPlugin} to create a new + * plugin object and trigger {@link #onPluginLoaded}. + */ + default void onPluginUnloaded(T plugin, PluginLifecycleManager manager) { + // Optional, default to deprecated version + onPluginDisconnected(plugin); + } +} \ No newline at end of file diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/PluginManager.java b/systemUIPluginCore/src/com/android/systemui/plugins/PluginManager.java new file mode 100644 index 0000000000..80c64cd416 --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/PluginManager.java @@ -0,0 +1,58 @@ +/* + * 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.systemui.plugins; + +import android.text.TextUtils; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +public interface PluginManager { + + String PLUGIN_CHANGED = "com.android.systemui.action.PLUGIN_CHANGED"; + + // must be one of the channels created in NotificationChannels.java + String NOTIFICATION_CHANNEL_ID = "ALR"; + + /** Returns plugins that don't get disabled when an exceptoin occurs. */ + String[] getPrivilegedPlugins(); + + /** */ + void addPluginListener(PluginListener listener, Class cls); + /** */ + void addPluginListener(PluginListener listener, Class cls, + boolean allowMultiple); + void addPluginListener(String action, PluginListener listener, + Class cls); + void addPluginListener(String action, PluginListener listener, + Class cls, boolean allowMultiple); + + void removePluginListener(PluginListener listener); + + boolean dependsOn(Plugin p, Class cls); + + class Helper { + public static

String getAction(Class

cls) { + ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class); + if (info == null) { + throw new RuntimeException(cls + " doesn't provide an interface"); + } + if (TextUtils.isEmpty(info.action())) { + throw new RuntimeException(cls + " doesn't provide an action"); + } + return info.action(); + } + } + +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Dependencies.java b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Dependencies.java new file mode 100644 index 0000000000..dbbf047519 --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Dependencies.java @@ -0,0 +1,27 @@ +/* + * 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.systemui.plugins.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used for repeated @DependsOn internally, not for plugin + * use. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Dependencies { + DependsOn[] value(); +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/annotations/DependsOn.java b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/DependsOn.java new file mode 100644 index 0000000000..b81d673063 --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/DependsOn.java @@ -0,0 +1,32 @@ +/* + * 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.systemui.plugins.annotations; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used to indicate that an interface in the plugin library needs another + * interface to function properly. When this is added, it will be enforced + * that all plugins that @Requires the annotated interface also @Requires + * the specified class as well. + */ +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(value = Dependencies.class) +public @interface DependsOn { + Class target(); + +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/annotations/ProvidesInterface.java b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/ProvidesInterface.java new file mode 100644 index 0000000000..d0e14b8657 --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/ProvidesInterface.java @@ -0,0 +1,30 @@ +/* + * 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.systemui.plugins.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Should be added to all interfaces in plugin lib to specify their + * current version and optionally their action to implement the plugin. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface ProvidesInterface { + int version(); + + String action() default ""; + +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requirements.java b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requirements.java new file mode 100644 index 0000000000..9cfa279b9c --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requirements.java @@ -0,0 +1,27 @@ +/* + * 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.systemui.plugins.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used for repeated @Requires internally, not for plugin + * use. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Requirements { + Requires[] value(); +} diff --git a/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requires.java b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requires.java new file mode 100644 index 0000000000..e1b1303b8c --- /dev/null +++ b/systemUIPluginCore/src/com/android/systemui/plugins/annotations/Requires.java @@ -0,0 +1,33 @@ +/* + * 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.systemui.plugins.annotations; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used to annotate which interfaces a given plugin depends on. + * + * At minimum all plugins should have at least one @Requires annotation + * for the plugin interface that they are implementing. They will also + * need an @Requires for each class that the plugin interface @DependsOn. + */ +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(value = Requirements.class) +public @interface Requires { + Class target(); + int version(); +} diff --git a/systemUIShared/build.gradle b/systemUIShared/build.gradle index 81b397c40e..6ed3c402fb 100644 --- a/systemUIShared/build.gradle +++ b/systemUIShared/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' + id 'com.google.devtools.ksp' } android { @@ -16,6 +17,9 @@ android { res.srcDirs = ['res'] } } + ksp { + arg("dagger.hilt.disableModulesHaveInstallInCheck", "true") + } } addFrameworkJar('framework-15.jar') @@ -24,9 +28,12 @@ compileOnlyCommonJars() dependencies { compileOnly projects.hiddenApi compileOnly projects.systemUnFold + compileOnly projects.systemUIPlugin + compileOnly projects.systemUIPluginCore + compileOnly projects.flags implementation "com.google.dagger:hilt-android:$daggerVersion" - annotationProcessor "com.google.dagger:hilt-compiler:$daggerVersion" + ksp "com.google.dagger:hilt-compiler:$daggerVersion" implementation "androidx.concurrent:concurrent-futures:1.2.0" } diff --git a/systemUIViewCapture/.gitignore b/systemUIViewCapture/.gitignore new file mode 100644 index 0000000000..6213826ab5 --- /dev/null +++ b/systemUIViewCapture/.gitignore @@ -0,0 +1,13 @@ +*.iml +.project +.classpath +.project.properties +gen/ +bin/ +.idea/ +.gradle/ +local.properties +gradle/ +build/ +gradlew* +.DS_Store diff --git a/systemUIViewCapture/Android.bp b/systemUIViewCapture/Android.bp new file mode 100644 index 0000000000..33da2dd36d --- /dev/null +++ b/systemUIViewCapture/Android.bp @@ -0,0 +1,73 @@ +// 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: ["Android-Apache-2.0"], +} + +java_library { + name: "view_capture_proto", + srcs: ["src/com/android/app/viewcapture/proto/*.proto"], + proto: { + type: "lite", + local_include_dirs:[ + "src/com/android/app/viewcapture/proto" + ], + }, + static_libs: ["libprotobuf-java-lite"], + java_version: "1.8", +} + +android_library { + name: "view_capture", + manifest: "AndroidManifest.xml", + platform_apis: true, + min_sdk_version: "26", + + static_libs: [ + "androidx.core_core", + "view_capture_proto", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt" + ], +} + +android_test { + name: "view_capture_tests", + manifest: "tests/AndroidManifest.xml", + platform_apis: true, + min_sdk_version: "26", + + static_libs: [ + "androidx.core_core", + "view_capture", + "androidx.test.ext.junit", + "androidx.test.rules", + "testables", + "mockito-target-extended-minus-junit4", + ], + srcs: [ + "**/*.java", + "**/*.kt" + ], + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + ], + test_suites: ["device-tests"], +} diff --git a/systemUIViewCapture/AndroidManifest.xml b/systemUIViewCapture/AndroidManifest.xml new file mode 100644 index 0000000000..1da812903b --- /dev/null +++ b/systemUIViewCapture/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/systemUIViewCapture/OWNERS b/systemUIViewCapture/OWNERS new file mode 100644 index 0000000000..30bdc84a4d --- /dev/null +++ b/systemUIViewCapture/OWNERS @@ -0,0 +1,2 @@ +sunnygoyal@google.com +andonian@google.com diff --git a/systemUIViewCapture/README.md b/systemUIViewCapture/README.md new file mode 100644 index 0000000000..e2834cb22b --- /dev/null +++ b/systemUIViewCapture/README.md @@ -0,0 +1,11 @@ +# ViewCapture Library + +> [!CAUTION] +> `ViewCapture.java` is **extremely performance sensitive**. +> **Any changes should be carried out with great caution** not to hurt performance. + +The following measurements should serve as a performance baseline (as of 02.10.2022): + +The onDraw() function invocation time in WindowListener within ViewCapture is measured with System.nanoTime(). The following scenario was measured: + +1. Capturing the notification shade window root view on a freshly rebooted bluejay device (2 notifications present) -> avg. time = 204237ns (0.2ms) diff --git a/systemUIViewCapture/TEST_MAPPING b/systemUIViewCapture/TEST_MAPPING new file mode 100644 index 0000000000..ecd3e96c5f --- /dev/null +++ b/systemUIViewCapture/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "presubmit": [ + { + "name": "view_capture_tests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} diff --git a/systemUIViewCapture/build.gradle b/systemUIViewCapture/build.gradle new file mode 100644 index 0000000000..860aaa5f18 --- /dev/null +++ b/systemUIViewCapture/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'com.google.protobuf' +} + +android { + namespace "com.android.app.viewcapture" + sourceSets { + main { + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + proto.srcDirs = ['src'] + } + androidTest { + java.srcDirs = ["tests"] + manifest.srcFile "tests/AndroidManifest.xml" + } + } +} + +addFrameworkJar('framework-15.jar') +compileOnlyCommonJars() diff --git a/systemUIViewCapture/src/com/android/app/viewcapture/LooperExecutor.java b/systemUIViewCapture/src/com/android/app/viewcapture/LooperExecutor.java new file mode 100644 index 0000000000..e3450f6bcc --- /dev/null +++ b/systemUIViewCapture/src/com/android/app/viewcapture/LooperExecutor.java @@ -0,0 +1,59 @@ +/* + * 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.app.viewcapture; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RunnableFuture; + +/** + * Implementation of {@link Executor} which executes on a provided looper. + */ +public class LooperExecutor implements Executor { + + private final Handler mHandler; + + public LooperExecutor(Looper looper) { + mHandler = new Handler(looper); + } + + @Override + public void execute(Runnable runnable) { + if (mHandler.getLooper() == Looper.myLooper()) { + runnable.run(); + } else { + mHandler.post(runnable); + } + } + + /** + * @throws RejectedExecutionException {@inheritDoc} + * @throws NullPointerException {@inheritDoc} + */ + public Future submit(Callable task) { + if (task == null) throw new NullPointerException(); + RunnableFuture ftask = new FutureTask(task); + execute(ftask); + return ftask; + } + +} diff --git a/systemUIViewCapture/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt b/systemUIViewCapture/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt new file mode 100644 index 0000000000..f75166b73e --- /dev/null +++ b/systemUIViewCapture/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt @@ -0,0 +1,107 @@ +/* + * 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.app.viewcapture + +import android.content.Context +import android.content.pm.LauncherApps +import android.database.ContentObserver +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.ParcelFileDescriptor +import android.os.Process +import android.provider.Settings +import android.util.Log +import android.window.IDumpCallback +import androidx.annotation.AnyThread +import androidx.annotation.VisibleForTesting +import java.util.concurrent.Executor + +private val TAG = SettingsAwareViewCapture::class.java.simpleName + +/** + * ViewCapture that listens to system updates and enables / disables attached ViewCapture + * WindowListeners accordingly. The Settings toggle is currently controlled by the Winscope + * developer tile in the System developer options. + */ +class SettingsAwareViewCapture +@VisibleForTesting +internal constructor(private val context: Context, executor: Executor) + : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE, executor) { + /** Dumps all the active view captures to the wm trace directory via LauncherAppService */ + private val mDumpCallback: IDumpCallback.Stub? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) object : IDumpCallback.Stub() { + override fun onDump(out: ParcelFileDescriptor) { + try { + ParcelFileDescriptor.AutoCloseOutputStream(out).use { os -> dumpTo(os, context) } + } catch (e: Exception) { + Log.e(TAG, "failed to dump data to wm trace", e) + } + } + } else null + + init { + enableOrDisableWindowListeners() + context.contentResolver.registerContentObserver( + Settings.Global.getUriFor(VIEW_CAPTURE_ENABLED), + false, + object : ContentObserver(Handler()) { + override fun onChange(selfChange: Boolean) { + enableOrDisableWindowListeners() + } + }) + } + + @AnyThread + private fun enableOrDisableWindowListeners() { + mBgExecutor.execute { + val isEnabled = Settings.Global.getInt(context.contentResolver, VIEW_CAPTURE_ENABLED, + 0) != 0 + MAIN_EXECUTOR.execute { + enableOrDisableWindowListeners(isEnabled) + } + val launcherApps = context.getSystemService(LauncherApps::class.java) + if (mDumpCallback != null) { + if (isEnabled) { + launcherApps?.registerDumpCallback(mDumpCallback) + } else { + launcherApps?.unRegisterDumpCallback(mDumpCallback) + } + } + } + } + + companion object { + @VisibleForTesting internal const val VIEW_CAPTURE_ENABLED = "view_capture_enabled" + + private var INSTANCE: ViewCapture? = null + + @JvmStatic + fun getInstance(context: Context): ViewCapture = when { + INSTANCE != null -> INSTANCE!! + Looper.myLooper() == Looper.getMainLooper() -> SettingsAwareViewCapture( + context.applicationContext, + createAndStartNewLooperExecutor("SAViewCapture", + Process.THREAD_PRIORITY_FOREGROUND)).also { INSTANCE = it } + else -> try { + MAIN_EXECUTOR.submit { getInstance(context) }.get() + } catch (e: Exception) { + throw e + } + } + + } +} \ No newline at end of file diff --git a/systemUIViewCapture/src/com/android/app/viewcapture/SimpleViewCapture.kt b/systemUIViewCapture/src/com/android/app/viewcapture/SimpleViewCapture.kt new file mode 100644 index 0000000000..420faca9fe --- /dev/null +++ b/systemUIViewCapture/src/com/android/app/viewcapture/SimpleViewCapture.kt @@ -0,0 +1,6 @@ +package com.android.app.viewcapture + +import android.os.Process + +open class SimpleViewCapture(threadName: String) : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE, + createAndStartNewLooperExecutor(threadName, Process.THREAD_PRIORITY_FOREGROUND)) \ No newline at end of file diff --git a/systemUIViewCapture/src/com/android/app/viewcapture/ViewCapture.java b/systemUIViewCapture/src/com/android/app/viewcapture/ViewCapture.java new file mode 100644 index 0000000000..bbd797efbf --- /dev/null +++ b/systemUIViewCapture/src/com/android/app/viewcapture/ViewCapture.java @@ -0,0 +1,623 @@ +/* + * 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.app.viewcapture; + +import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_H; +import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_L; + +import android.content.Context; +import android.content.res.Resources; +import android.media.permission.SafeCloseable; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.SystemClock; +import android.os.Trace; +import android.text.TextUtils; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import com.android.app.viewcapture.data.ExportedData; +import com.android.app.viewcapture.data.FrameData; +import com.android.app.viewcapture.data.MotionWindowData; +import com.android.app.viewcapture.data.ViewNode; +import com.android.app.viewcapture.data.WindowData; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Utility class for capturing view data every frame + */ +public abstract class ViewCapture { + + private static final String TAG = "ViewCapture"; + + // These flags are copies of two private flags in the View class. + private static final int PFLAG_INVALIDATED = 0x80000000; + private static final int PFLAG_DIRTY_MASK = 0x00200000; + + private static final long MAGIC_NUMBER_FOR_WINSCOPE = + ((long) MAGIC_NUMBER_H.getNumber() << 32) | MAGIC_NUMBER_L.getNumber(); + + // Number of frames to keep in memory + private final int mMemorySize; + protected static final int DEFAULT_MEMORY_SIZE = 2000; + // Initial size of the reference pool. This is at least be 5 * total number of views in + // Launcher. This allows the first free frames avoid object allocation during view capture. + protected static final int DEFAULT_INIT_POOL_SIZE = 300; + + public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper()); + + private final List mListeners = new ArrayList<>(); + + protected final Executor mBgExecutor; + + // Pool used for capturing view tree on the UI thread. + private ViewRef mPool = new ViewRef(); + private boolean mIsEnabled = true; + + protected ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor) { + mMemorySize = memorySize; + mBgExecutor = bgExecutor; + mBgExecutor.execute(() -> initPool(initPoolSize)); + } + + public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) { + HandlerThread thread = new HandlerThread(name, priority); + thread.start(); + return new LooperExecutor(thread.getLooper()); + } + + @UiThread + private void addToPool(ViewRef start, ViewRef end) { + end.next = mPool; + mPool = start; + } + + @WorkerThread + private void initPool(int initPoolSize) { + ViewRef start = new ViewRef(); + ViewRef current = start; + + for (int i = 0; i < initPoolSize; i++) { + current.next = new ViewRef(); + current = current.next; + } + + ViewRef finalCurrent = current; + MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent)); + } + + /** + * Attaches the ViewCapture to the provided window and returns a handle to detach the listener + */ + @NonNull + public SafeCloseable startCapture(Window window) { + String title = window.getAttributes().getTitle().toString(); + String name = TextUtils.isEmpty(title) ? window.toString() : title; + return startCapture(window.getDecorView(), name); + } + + /** + * Attaches the ViewCapture to the provided window and returns a handle to detach the listener. + * Verifies that ViewCapture is enabled before actually attaching an onDrawListener. + */ + @NonNull + public SafeCloseable startCapture(View view, String name) { + WindowListener listener = new WindowListener(view, name); + if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot); + mListeners.add(listener); + return () -> { + mListeners.remove(listener); + listener.detachFromRoot(); + }; + } + + /** + * Launcher checks for leaks in many spots during its instrumented tests. The WindowListeners + * appear to have leaks because they store mRoot views. In reality, attached views close their + * respective window listeners when they are destroyed. + *

+ * This method deletes detaches and deletes mRoot views from windowListeners. This makes the + * WindowListeners unusable for anything except dumping previously captured information. They + * are still technically enabled to allow for dumping. + */ + @VisibleForTesting + public void stopCapture(@NonNull View rootView) { + mListeners.forEach(it -> { + if (rootView == it.mRoot) { + it.mRoot.getViewTreeObserver().removeOnDrawListener(it); + it.mRoot = null; + } + }); + } + + @UiThread + protected void enableOrDisableWindowListeners(boolean isEnabled) { + mIsEnabled = isEnabled; + mListeners.forEach(WindowListener::detachFromRoot); + if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot); + } + + @AnyThread + public void dumpTo(OutputStream os, Context context) + throws InterruptedException, ExecutionException, IOException { + if (mIsEnabled) getExportedData(context).writeTo(os); + } + + @VisibleForTesting + public ExportedData getExportedData(Context context) + throws InterruptedException, ExecutionException { + ArrayList classList = new ArrayList<>(); + return ExportedData.newBuilder() + .setMagicNumber(MAGIC_NUMBER_FOR_WINSCOPE) + .setPackage(context.getPackageName()) + .addAllWindowData(getWindowData(context, classList, l -> l.mIsActive).get()) + .addAllClassname(toStringList(classList)) + .setRealToElapsedTimeOffsetNanos(TimeUnit.MILLISECONDS + .toNanos(System.currentTimeMillis()) - SystemClock.elapsedRealtimeNanos()) + .build(); + } + + private static List toStringList(List classList) { + return classList.stream().map(Class::getName).toList(); + } + + public CompletableFuture> getDumpTask(View view) { + ArrayList classList = new ArrayList<>(); + return getWindowData(view.getContext().getApplicationContext(), classList, + l -> l.mRoot.equals(view)).thenApply(list -> list.stream().findFirst().map(w -> + MotionWindowData.newBuilder() + .addAllFrameData(w.getFrameDataList()) + .addAllClassname(toStringList(classList)) + .build())); + } + + @AnyThread + private CompletableFuture> getWindowData(Context context, + ArrayList outClassList, Predicate filter) { + ViewIdProvider idProvider = new ViewIdProvider(context.getResources()); + return CompletableFuture.supplyAsync(() -> + mListeners.stream().filter(filter).toList(), MAIN_EXECUTOR).thenApplyAsync(it -> + it.stream().map(l -> l.dumpToProto(idProvider, outClassList)).toList(), + mBgExecutor); + } + + + /** + * Once this window listener is attached to a window's root view, it traverses the entire + * view tree on the main thread every time onDraw is called. It then saves the state of the view + * tree traversed in a local list of nodes, so that this list of nodes can be processed on a + * background thread, and prepared for being dumped into a bugreport. + * + * Since some of the work needs to be done on the main thread after every draw, this piece of + * code needs to be hyper optimized. That is why we are recycling ViewRef and ViewPropertyRef + * objects and storing the list of nodes as a flat LinkedList, rather than as a tree. This data + * structure allows recycling to happen in O(1) time via pointer assignment. Without this + * optimization, a lot of time is wasted creating ViewRef objects, or finding ViewRef objects to + * recycle. + * + * Another optimization is to only traverse view nodes on the main thread that have potentially + * changed since the last frame was drawn. This can be determined via a combination of private + * flags inside the View class. + * + * Another optimization is to not store or manipulate any string objects on the main thread. + * While this might seem trivial, using Strings in any form causes the ViewCapture to hog the + * main thread for up to an additional 6-7ms. It must be avoided at all costs. + * + * Another optimization is to only store the class names of the Views in the view hierarchy one + * time. They are then referenced via a classNameIndex value stored in each ViewPropertyRef. + * + * TODO: b/262585897: If further memory optimization is required, an effective one would be to + * only store the changes between frames, rather than the entire node tree for each frame. + * The go/web-hv UX already does this, and has reaped significant memory improves because of it. + * + * TODO: b/262585897: Another memory optimization could be to store all integer, float, and + * boolean information via single integer values via the Chinese remainder theorem, or a similar + * algorithm, which enables multiple numerical values to be stored inside 1 number. Doing this + * would allow each ViewProperty / ViewRef to slim down its memory footprint significantly. + * + * One important thing to remember is that bugs related to recycling will usually only appear + * after at least 2000 frames have been rendered. If that code is changed, the tester can + * use hard-coded logs to verify that recycling is happening, and test view capturing at least + * ~8000 frames or so to verify the recycling functionality is working properly. + */ + private class WindowListener implements ViewTreeObserver.OnDrawListener { + + @Nullable // Nullable in tests only + public View mRoot; + public final String name; + + private final ViewRef mViewRef = new ViewRef(); + + private int mFrameIndexBg = -1; + private boolean mIsFirstFrame = true; + private final long[] mFrameTimesNanosBg = new long[mMemorySize]; + private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize]; + + private boolean mIsActive = true; + private final Consumer mCaptureCallback = this::captureViewPropertiesBg; + + WindowListener(View view, String name) { + mRoot = view; + this.name = name; + } + + /** + * Every time onDraw is called, it does the minimal set of work required on the main thread, + * i.e. capturing potentially dirty / invalidated views, and then immediately offloads the + * rest of the processing work (extracting the captured view properties) to a background + * thread via mExecutor. + */ + @Override + public void onDraw() { + Trace.beginSection("view_capture"); + captureViewTree(mRoot, mViewRef); + ViewRef captured = mViewRef.next; + if (captured != null) { + captured.callback = mCaptureCallback; + captured.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos(); + mBgExecutor.execute(captured); + } + mIsFirstFrame = false; + Trace.endSection(); + } + + /** + * Captures the View property on the background thread, and transfer all the ViewRef objects + * back to the pool + */ + @WorkerThread + private void captureViewPropertiesBg(ViewRef viewRefStart) { + long elapsedRealtimeNanos = viewRefStart.elapsedRealtimeNanos; + mFrameIndexBg++; + if (mFrameIndexBg >= mMemorySize) { + mFrameIndexBg = 0; + } + mFrameTimesNanosBg[mFrameIndexBg] = elapsedRealtimeNanos; + + ViewPropertyRef recycle = mNodesBg[mFrameIndexBg]; + + ViewPropertyRef resultStart = null; + ViewPropertyRef resultEnd = null; + + ViewRef viewRefEnd = viewRefStart; + while (viewRefEnd != null) { + ViewPropertyRef propertyRef = recycle; + if (propertyRef == null) { + propertyRef = new ViewPropertyRef(); + } else { + recycle = recycle.next; + propertyRef.next = null; + } + + ViewPropertyRef copy = null; + if (viewRefEnd.childCount < 0) { + copy = findInLastFrame(viewRefEnd.view.hashCode()); + viewRefEnd.childCount = (copy != null) ? copy.childCount : 0; + } + viewRefEnd.transferTo(propertyRef); + + if (resultStart == null) { + resultStart = propertyRef; + resultEnd = resultStart; + } else { + resultEnd.next = propertyRef; + resultEnd = resultEnd.next; + } + + if (copy != null) { + int pending = copy.childCount; + while (pending > 0) { + copy = copy.next; + pending = pending - 1 + copy.childCount; + + propertyRef = recycle; + if (propertyRef == null) { + propertyRef = new ViewPropertyRef(); + } else { + recycle = recycle.next; + propertyRef.next = null; + } + + copy.transferTo(propertyRef); + + resultEnd.next = propertyRef; + resultEnd = resultEnd.next; + } + } + + if (viewRefEnd.next == null) { + // The compiler will complain about using a non-final variable from + // an outer class in a lambda if we pass in viewRefEnd directly. + final ViewRef finalViewRefEnd = viewRefEnd; + MAIN_EXECUTOR.execute(() -> addToPool(viewRefStart, finalViewRefEnd)); + break; + } + viewRefEnd = viewRefEnd.next; + } + mNodesBg[mFrameIndexBg] = resultStart; + } + + private @Nullable ViewPropertyRef findInLastFrame(int hashCode) { + int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1; + ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex]; + while (viewPropertyRef != null && viewPropertyRef.hashCode != hashCode) { + viewPropertyRef = viewPropertyRef.next; + } + return viewPropertyRef; + } + + void attachToRoot() { + mIsActive = true; + if (mRoot.isAttachedToWindow()) { + safelyEnableOnDrawListener(); + } else { + mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + if (mIsActive) { + safelyEnableOnDrawListener(); + } + mRoot.removeOnAttachStateChangeListener(this); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + } + } + + void detachFromRoot() { + mIsActive = false; + if (mRoot != null) { + mRoot.getViewTreeObserver().removeOnDrawListener(this); + } + } + + private void safelyEnableOnDrawListener() { + mRoot.getViewTreeObserver().removeOnDrawListener(this); + mRoot.getViewTreeObserver().addOnDrawListener(this); + } + + @WorkerThread + private WindowData dumpToProto(ViewIdProvider idProvider, ArrayList classList) { + WindowData.Builder builder = WindowData.newBuilder().setTitle(name); + int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize; + for (int i = size - 1; i >= 0; i--) { + int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize; + ViewNode.Builder nodeBuilder = ViewNode.newBuilder(); + mNodesBg[index].toProto(idProvider, classList, nodeBuilder); + FrameData.Builder frameDataBuilder = FrameData.newBuilder() + .setNode(nodeBuilder) + .setTimestamp(mFrameTimesNanosBg[index]); + builder.addFrameData(frameDataBuilder); + } + return builder.build(); + } + + private ViewRef captureViewTree(View view, ViewRef start) { + ViewRef ref; + if (mPool != null) { + ref = mPool; + mPool = mPool.next; + ref.next = null; + } else { + ref = new ViewRef(); + } + ref.view = view; + start.next = ref; + if (view instanceof ViewGroup) { + ViewGroup parent = (ViewGroup) view; + // If a view has not changed since the last frame, we will copy + // its children from the last processed frame's data. + if ((view.mPrivateFlags & (PFLAG_INVALIDATED | PFLAG_DIRTY_MASK)) == 0 + && !mIsFirstFrame) { + // A negative child count is the signal to copy this view from the last frame. + ref.childCount = -parent.getChildCount(); + return ref; + } + ViewRef result = ref; + int childCount = ref.childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + result = captureViewTree(parent.getChildAt(i), result); + } + return result; + } else { + ref.childCount = 0; + return ref; + } + } + } + + private static class ViewPropertyRef { + // We store reference in memory to avoid generating and storing too many strings + public Class clazz; + public int hashCode; + public int childCount = 0; + + public int id; + public int left, top, right, bottom; + public int scrollX, scrollY; + + public float translateX, translateY; + public float scaleX, scaleY; + public float alpha; + public float elevation; + + public int visibility; + public boolean willNotDraw; + public boolean clipChildren; + + public ViewPropertyRef next; + + public void transferTo(ViewPropertyRef out) { + out.clazz = this.clazz; + out.hashCode = this.hashCode; + out.childCount = this.childCount; + out.id = this.id; + out.left = this.left; + out.top = this.top; + out.right = this.right; + out.bottom = this.bottom; + out.scrollX = this.scrollX; + out.scrollY = this.scrollY; + out.scaleX = this.scaleX; + out.scaleY = this.scaleY; + out.translateX = this.translateX; + out.translateY = this.translateY; + out.alpha = this.alpha; + out.visibility = this.visibility; + out.willNotDraw = this.willNotDraw; + out.clipChildren = this.clipChildren; + out.elevation = this.elevation; + } + + /** + * Converts the data to the proto representation and returns the next property ref + * at the end of the iteration. + */ + public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList classList, + ViewNode.Builder viewNode) { + int classnameIndex = classList.indexOf(clazz); + if (classnameIndex < 0) { + classnameIndex = classList.size(); + classList.add(clazz); + } + + viewNode.setClassnameIndex(classnameIndex) + .setHashcode(hashCode) + .setId(idProvider.getName(id)) + .setLeft(left) + .setTop(top) + .setWidth(right - left) + .setHeight(bottom - top) + .setTranslationX(translateX) + .setTranslationY(translateY) + .setScrollX(scrollX) + .setScrollY(scrollY) + .setScaleX(scaleX) + .setScaleY(scaleY) + .setAlpha(alpha) + .setVisibility(visibility) + .setWillNotDraw(willNotDraw) + .setElevation(elevation) + .setClipChildren(clipChildren); + + ViewPropertyRef result = next; + for (int i = 0; (i < childCount) && (result != null); i++) { + ViewNode.Builder childViewNode = ViewNode.newBuilder(); + result = result.toProto(idProvider, classList, childViewNode); + viewNode.addChildren(childViewNode); + } + return result; + } + } + + + private static class ViewRef implements Runnable { + public View view; + public int childCount = 0; + public ViewRef next; + + public Consumer callback = null; + public long elapsedRealtimeNanos = 0; + + public void transferTo(ViewPropertyRef out) { + out.childCount = this.childCount; + + View view = this.view; + this.view = null; + + out.clazz = view.getClass(); + out.hashCode = view.hashCode(); + out.id = view.getId(); + out.left = view.getLeft(); + out.top = view.getTop(); + out.right = view.getRight(); + out.bottom = view.getBottom(); + out.scrollX = view.getScrollX(); + out.scrollY = view.getScrollY(); + + out.translateX = view.getTranslationX(); + out.translateY = view.getTranslationY(); + out.scaleX = view.getScaleX(); + out.scaleY = view.getScaleY(); + out.alpha = view.getAlpha(); + out.elevation = view.getElevation(); + + out.visibility = view.getVisibility(); + out.willNotDraw = view.willNotDraw(); + } + + @Override + public void run() { + Consumer oldCallback = callback; + callback = null; + if (oldCallback != null) { + oldCallback.accept(this); + } + } + } + + private static final class ViewIdProvider { + + private final SparseArray mNames = new SparseArray<>(); + private final Resources mRes; + + ViewIdProvider(Resources res) { + mRes = res; + } + + String getName(int id) { + String name = mNames.get(id); + if (name == null) { + if (id >= 0) { + try { + name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id); + } catch (Resources.NotFoundException e) { + name = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); + } + } else { + name = "NO_ID"; + } + mNames.put(id, name); + } + return name; + } + } +} diff --git a/systemUIViewCapture/src/com/android/app/viewcapture/proto/view_capture.proto b/systemUIViewCapture/src/com/android/app/viewcapture/proto/view_capture.proto new file mode 100644 index 0000000000..c73ce8bcf8 --- /dev/null +++ b/systemUIViewCapture/src/com/android/app/viewcapture/proto/view_capture.proto @@ -0,0 +1,83 @@ +/* + * 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. + */ + +syntax = "proto2"; + +package com.android.app.viewcapture.data; + +option java_multiple_files = true; + +message ExportedData { + /* 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 = 0x65906578; /* AZAN (ASCII) */ + MAGIC_NUMBER_H = 0x68658273; /* DARI (ASCII) */ + } + + optional fixed64 magic_number = 1; /* Must be the first field, set to value in MagicNumber */ + repeated WindowData windowData = 2; + optional string package = 3; + repeated string classname = 4; + + /* 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 = 5; +} + +message WindowData { + repeated FrameData frameData = 1; + optional string title = 2; +} + +message MotionWindowData { + repeated FrameData frameData = 1; + repeated string classname = 2; +} + +message FrameData { + optional int64 timestamp = 1; // unit is elapsed realtime nanos + optional ViewNode node = 2; +} + +message ViewNode { + optional int32 classname_index = 1; + optional int32 hashcode = 2; + + repeated ViewNode children = 3; + + optional string id = 4; + optional int32 left = 5; + optional int32 top = 6; + optional int32 width = 7; + optional int32 height = 8; + optional int32 scrollX = 9; + optional int32 scrollY = 10; + + optional float translationX = 11; + optional float translationY = 12; + optional float scaleX = 13 [default = 1]; + optional float scaleY = 14 [default = 1]; + optional float alpha = 15 [default = 1]; + + optional bool willNotDraw = 16; + optional bool clipChildren = 17; + optional int32 visibility = 18; + + optional float elevation = 19; +} diff --git a/systemUIViewCapture/tests/AndroidManifest.xml b/systemUIViewCapture/tests/AndroidManifest.xml new file mode 100644 index 0000000000..8d31c0eb5d --- /dev/null +++ b/systemUIViewCapture/tests/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/systemUIViewCapture/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt b/systemUIViewCapture/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt new file mode 100644 index 0000000000..15352aa760 --- /dev/null +++ b/systemUIViewCapture/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt @@ -0,0 +1,108 @@ +/* + * 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.app.viewcapture + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.media.permission.SafeCloseable +import android.provider.Settings +import android.testing.AndroidTestingRunner +import android.view.Choreographer +import android.view.View +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import com.android.app.viewcapture.SettingsAwareViewCapture.Companion.VIEW_CAPTURE_ENABLED +import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR +import junit.framework.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SettingsAwareViewCaptureTest { + private val context: Context = InstrumentationRegistry.getInstrumentation().context + private val activityIntent = Intent(context, TestActivity::class.java) + + @get:Rule val activityScenarioRule = ActivityScenarioRule(activityIntent) + @get:Rule val grantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.WRITE_SECURE_SETTINGS) + + @Test + fun do_not_capture_view_hierarchies_if_setting_is_disabled() { + Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 0) + + activityScenarioRule.scenario.onActivity { activity -> + val viewCapture: ViewCapture = SettingsAwareViewCapture(context, MAIN_EXECUTOR) + val rootView: View = activity.requireViewById(android.R.id.content) + + val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId") + Choreographer.getInstance().postFrameCallback { + rootView.viewTreeObserver.dispatchOnDraw() + + assertEquals( + 0, + viewCapture + .getDumpTask(activity.requireViewById(android.R.id.content)) + .get() + .get() + .frameDataList + .size + ) + closeable.close() + } + } + } + + @Test + fun capture_view_hierarchies_if_setting_is_enabled() { + Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 1) + + activityScenarioRule.scenario.onActivity { activity -> + val viewCapture: ViewCapture = SettingsAwareViewCapture(context, MAIN_EXECUTOR) + val rootView: View = activity.requireViewById(android.R.id.content) + + val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId") + Choreographer.getInstance().postFrameCallback { + rootView.viewTreeObserver.dispatchOnDraw() + + assertEquals( + 1, + viewCapture + .getDumpTask(activity.requireViewById(android.R.id.content)) + .get() + .get() + .frameDataList + .size + ) + + closeable.close() + } + } + } + + @Test + fun getInstance_calledTwiceInARow_returnsSameObject() { + assertEquals( + SettingsAwareViewCapture.getInstance(context).hashCode(), + SettingsAwareViewCapture.getInstance(context).hashCode() + ) + } +} diff --git a/systemUIViewCapture/tests/com/android/app/viewcapture/TestActivity.kt b/systemUIViewCapture/tests/com/android/app/viewcapture/TestActivity.kt new file mode 100644 index 0000000000..749327ebe3 --- /dev/null +++ b/systemUIViewCapture/tests/com/android/app/viewcapture/TestActivity.kt @@ -0,0 +1,45 @@ +/* + * 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.app.viewcapture + +import android.app.Activity +import android.os.Bundle +import android.widget.LinearLayout +import android.widget.TextView + +/** + * Activity with the content set to a [LinearLayout] with [TextView] children. + */ +class TestActivity : Activity() { + + companion object { + const val TEXT_VIEW_COUNT = 1000 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(createContentView()) + } + + private fun createContentView(): LinearLayout { + val root = LinearLayout(this) + for (i in 0 until TEXT_VIEW_COUNT) { + root.addView(TextView(this)) + } + return root + } +} \ No newline at end of file diff --git a/systemUIViewCapture/tests/com/android/app/viewcapture/ViewCaptureTest.kt b/systemUIViewCapture/tests/com/android/app/viewcapture/ViewCaptureTest.kt new file mode 100644 index 0000000000..e3272c422e --- /dev/null +++ b/systemUIViewCapture/tests/com/android/app/viewcapture/ViewCaptureTest.kt @@ -0,0 +1,111 @@ +/* + * 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.app.viewcapture + +import android.content.Intent +import android.media.permission.SafeCloseable +import android.testing.AndroidTestingRunner +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.app.viewcapture.TestActivity.Companion.TEXT_VIEW_COUNT +import com.android.app.viewcapture.data.MotionWindowData +import junit.framework.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ViewCaptureTest { + + private val memorySize = 100 + private val initPoolSize = 15 + private val viewCapture by lazy { + object : + ViewCapture(memorySize, initPoolSize, MAIN_EXECUTOR) {} + } + + private val activityIntent = + Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java) + + @get:Rule val activityScenarioRule = ActivityScenarioRule(activityIntent) + + @Test + fun testWindowListenerDumpsOneFrameAfterInvalidate() { + activityScenarioRule.scenario.onActivity { activity -> + val closeable = startViewCaptureAndInvalidateNTimes(1, activity) + val rootView = activity.requireViewById(android.R.id.content) + val data = viewCapture.getDumpTask(rootView).get().get() + + assertEquals(1, data.frameDataList.size) + verifyTestActivityViewHierarchy(data) + closeable.close() + } + } + + @Test + fun testWindowListenerDumpsCorrectlyAfterRecyclingStarted() { + activityScenarioRule.scenario.onActivity { activity -> + val closeable = startViewCaptureAndInvalidateNTimes(memorySize + 5, activity) + val rootView = activity.requireViewById(android.R.id.content) + val data = viewCapture.getDumpTask(rootView).get().get() + + // since ViewCapture MEMORY_SIZE is [viewCaptureMemorySize], only + // [viewCaptureMemorySize] frames are exported, although the view is invalidated + // [viewCaptureMemorySize + 5] times + assertEquals(memorySize, data.frameDataList.size) + verifyTestActivityViewHierarchy(data) + closeable.close() + } + } + + private fun startViewCaptureAndInvalidateNTimes(n: Int, activity: TestActivity): SafeCloseable { + val rootView: View = activity.requireViewById(android.R.id.content) + val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId") + dispatchOnDraw(rootView, times = n) + return closeable + } + + private fun dispatchOnDraw(view: View, times: Int) { + if (times > 0) { + view.viewTreeObserver.dispatchOnDraw() + dispatchOnDraw(view, times - 1) + } + } + + private fun verifyTestActivityViewHierarchy(exportedData: MotionWindowData) { + for (frame in exportedData.frameDataList) { + val testActivityRoot = + frame.node // FrameLayout (android.R.id.content) + .childrenList + .first() // LinearLayout (set by setContentView()) + assertEquals(TEXT_VIEW_COUNT, testActivityRoot.childrenList.size) + assertEquals( + LinearLayout::class.qualifiedName, + exportedData.getClassname(testActivityRoot.classnameIndex) + ) + assertEquals( + TextView::class.qualifiedName, + exportedData.getClassname(testActivityRoot.childrenList.first().classnameIndex) + ) + } + } +} diff --git a/wmshell/Android.bp b/wmshell/Android.bp new file mode 100644 index 0000000000..25d3067a34 --- /dev/null +++ b/wmshell/Android.bp @@ -0,0 +1,234 @@ +// 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 { + // 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"], + default_team: "trendy_team_multitasking_windowing", +} + +// Begin ProtoLog +java_library { + name: "wm_shell_protolog-groups", + srcs: [ + "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java", + ":protolog-common-src", + ], +} + +filegroup { + name: "wm_shell-sources", + srcs: [ + "src/**/*.java", + ], + path: "src", +} + +// Sources that have no dependencies that can be used directly downstream of this library +// TODO(b/322791067): move these sources to WindowManager-Shell-shared +filegroup { + name: "wm_shell_util-sources", + srcs: [ + "src/com/android/wm/shell/animation/Interpolators.java", + "src/com/android/wm/shell/common/bubbles/*.kt", + "src/com/android/wm/shell/common/bubbles/*.java", + "src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt", + "src/com/android/wm/shell/common/split/SplitScreenConstants.java", + "src/com/android/wm/shell/common/TransactionPool.java", + "src/com/android/wm/shell/common/TriangleShape.java", + "src/com/android/wm/shell/common/desktopmode/*.kt", + "src/com/android/wm/shell/draganddrop/DragAndDropConstants.java", + "src/com/android/wm/shell/pip/PipContentOverlay.java", + "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java", + "src/com/android/wm/shell/sysui/ShellSharedConstants.java", + "src/com/android/wm/shell/util/**/*.java", + ], + path: "src", +} + +// Aidls which can be used directly downstream of this library +filegroup { + name: "wm_shell-aidls", + srcs: [ + "src/**/*.aidl", + ], + path: "src", +} + +// TODO(b/168581922) protologtool do not support kotlin(*.kt) +filegroup { + name: "wm_shell-sources-kt", + srcs: [ + "src/**/*.kt", + ], + path: "src", +} + +genrule { + name: "wm_shell_protolog_src", + srcs: [ + ":protolog-impl", + ":wm_shell_protolog-groups", + ":wm_shell-sources", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) transform-protolog-calls " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + + "--loggroups-jar $(location :wm_shell_protolog-groups) " + + "--viewer-config-file-path /system_ext/etc/wmshell.protolog.pb " + + "--legacy-viewer-config-file-path /system_ext/etc/wmshell.protolog.json.gz " + + "--legacy-output-file-path /data/misc/wmtrace/shell_log.winscope " + + "--output-srcjar $(out) " + + "$(locations :wm_shell-sources)", + out: ["wm_shell_protolog.srcjar"], +} + +genrule { + name: "generate-wm_shell_protolog.json", + srcs: [ + ":wm_shell_protolog-groups", + ":wm_shell-sources", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) generate-viewer-config " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + + "--loggroups-jar $(location :wm_shell_protolog-groups) " + + "--viewer-config-type json " + + "--viewer-config $(out) " + + "$(locations :wm_shell-sources)", + out: ["wm_shell_protolog.json"], +} + +genrule { + name: "gen-wmshell.protolog.pb", + srcs: [ + ":wm_shell_protolog-groups", + ":wm_shell-sources", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) generate-viewer-config " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + + "--loggroups-jar $(location :wm_shell_protolog-groups) " + + "--viewer-config-type proto " + + "--viewer-config $(out) " + + "$(locations :wm_shell-sources)", + out: ["wmshell.protolog.pb"], +} + +genrule { + name: "protolog.json.gz", + srcs: [":generate-wm_shell_protolog.json"], + out: ["wmshell.protolog.json.gz"], + cmd: "gzip -c < $(in) > $(out)", +} + +prebuilt_etc { + name: "wmshell.protolog.json.gz", + system_ext_specific: true, + src: ":protolog.json.gz", + filename_from_src: true, +} + +prebuilt_etc { + name: "wmshell.protolog.pb", + system_ext_specific: true, + src: ":gen-wmshell.protolog.pb", + filename_from_src: true, +} + +// End ProtoLog + +java_library { + name: "WindowManager-Shell-proto", + + srcs: ["proto/*.proto"], + + proto: { + type: "nano", + }, +} + +filegroup { + name: "wm_shell-shared-aidls", + + srcs: [ + "shared/**/*.aidl", + ], + + path: "shared/src", +} + +java_library { + name: "WindowManager-Shell-shared", + + srcs: [ + "shared/**/*.java", + "shared/**/*.kt", + ":wm_shell-shared-aidls", + ], + static_libs: [ + "androidx.dynamicanimation_dynamicanimation", + "jsr330", + ], +} + +android_library { + name: "WindowManager-Shell", + srcs: [ + ":wm_shell_protolog_src", + // TODO(b/168581922) protologtool do not support kotlin(*.kt) + ":wm_shell-sources-kt", + ":wm_shell-aidls", + ], + resource_dirs: [ + "res", + ], + static_libs: [ + "androidx.appcompat_appcompat", + "androidx.core_core-animation", + "androidx.core_core-ktx", + "androidx.arch.core_core-runtime", + "androidx.compose.material3_material3", + "androidx-constraintlayout_constraintlayout", + "androidx.dynamicanimation_dynamicanimation", + "androidx.recyclerview_recyclerview", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", + "//frameworks/libs/systemui:iconloader_base", + "com_android_wm_shell_flags_lib", + "com.android.window.flags.window-aconfig-java", + "WindowManager-Shell-proto", + "WindowManager-Shell-shared", + "perfetto_trace_java_protos", + "dagger2", + "jsr330", + ], + libs: [ + // Soong fails to automatically add this dependency because all the + // *.kt sources are inside a filegroup. + "kotlin-annotations", + ], + kotlincflags: ["-Xjvm-default=all"], + manifest: "AndroidManifest.xml", + plugins: ["dagger2-compiler"], + use_resource_processor: true, +} diff --git a/wmshell/AndroidManifest.xml b/wmshell/AndroidManifest.xml new file mode 100644 index 0000000000..36d3313a9f --- /dev/null +++ b/wmshell/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/wmshell/OWNERS b/wmshell/OWNERS new file mode 100644 index 0000000000..ebebd8a52c --- /dev/null +++ b/wmshell/OWNERS @@ -0,0 +1,5 @@ +xutan@google.com + +# Give submodule owners in shell resource approval +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com +per-file res*/*/tv_*.xml = bronger@google.com diff --git a/wmshell/aconfig/Android.bp b/wmshell/aconfig/Android.bp new file mode 100644 index 0000000000..7f8f57b172 --- /dev/null +++ b/wmshell/aconfig/Android.bp @@ -0,0 +1,13 @@ +aconfig_declarations { + name: "com_android_wm_shell_flags", + package: "com.android.wm.shell", + container: "system", + srcs: [ + "multitasking.aconfig", + ], +} + +java_aconfig_library { + name: "com_android_wm_shell_flags_lib", + aconfig_declarations: "com_android_wm_shell_flags", +} \ No newline at end of file diff --git a/wmshell/aconfig/OWNERS b/wmshell/aconfig/OWNERS new file mode 100644 index 0000000000..9eba0f2dea --- /dev/null +++ b/wmshell/aconfig/OWNERS @@ -0,0 +1,3 @@ +# Owners for flag changes +madym@google.com +hwwang@google.com \ No newline at end of file diff --git a/wmshell/aconfig/multitasking.aconfig b/wmshell/aconfig/multitasking.aconfig new file mode 100644 index 0000000000..112eb617e7 --- /dev/null +++ b/wmshell/aconfig/multitasking.aconfig @@ -0,0 +1,123 @@ +# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto + +package: "com.android.wm.shell" +container: "system" + +flag { + name: "enable_app_pairs" + namespace: "multitasking" + description: "Enables the ability to create and save app pairs to the Home screen" + bug: "274835596" +} + +flag { + name: "enable_split_contextual" + namespace: "multitasking" + description: "Enables invoking split contextually" + bug: "276361926" +} + +flag { + name: "enable_taskbar_navbar_unification" + namespace: "multitasking" + description: "Enables taskbar / navbar unification" + bug: "309671494" +} + +flag { + name: "enable_pip2_implementation" + namespace: "multitasking" + description: "Enables the new implementation of PiP (PiP2)" + bug: "290220798" + is_fixed_read_only: true +} + +flag { + name: "enable_left_right_split_in_portrait" + namespace: "multitasking" + description: "Enables left/right split in portrait" + bug: "291018646" +} + +flag { + name: "enable_new_bubble_animations" + namespace: "multitasking" + description: "Enables new animations for expand and collapse for bubbles" + bug: "311450609" +} + +flag { + name: "enable_pip_umo_experience" + namespace: "multitasking" + description: "Enables new UMO experience for PiP menu" + bug: "307998712" +} + +flag { + name: "enable_bubble_bar" + namespace: "multitasking" + description: "Enables the new bubble bar UI for tablets" + bug: "286246694" +} + +flag { + name: "enable_bubbles_long_press_nav_handle" + namespace: "multitasking" + description: "Enables long-press action for nav handle when a bubble is expanded" + bug: "324910035" +} + +flag { + name: "enable_optional_bubble_overflow" + namespace: "multitasking" + description: "Hides the bubble overflow if there aren't any overflowed bubbles" + bug: "334175587" +} + +flag { + name: "enable_retrievable_bubbles" + namespace: "multitasking" + description: "Allow opening bubbles overflow UI without bubbles being visible" + bug: "340337839" +} + +flag { + name: "enable_bubble_stashing" + namespace: "multitasking" + description: "Allow the floating bubble stack to stash on the edge of the screen" + bug: "341361249" +} + +flag { + name: "enable_tiny_taskbar" + namespace: "multitasking" + description: "Enables Taskbar on phones" + bug: "341784466" +} + +flag { + name: "enable_bubble_anything" + namespace: "multitasking" + description: "Enable UI affordances to put other content into a bubble" + bug: "342245211" +} + +flag { + name: "only_reuse_bubbled_task_when_launched_from_bubble" + namespace: "multitasking" + description: "Allow reusing bubbled tasks for new activities only when launching from bubbles" + bug: "328229865" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "animate_bubble_size_change" + namespace: "multitasking" + description: "Turns on the animation for bubble bar icons size change" + bug: "335575529" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/wmshell/build.gradle b/wmshell/build.gradle new file mode 100644 index 0000000000..b9258cb57c --- /dev/null +++ b/wmshell/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'com.google.devtools.ksp' +} + +android { + namespace "com.android.wm.shell" + buildFeatures { + aidl true + } + sourceSets { + main { + java.srcDirs = ['shared/src'] + aidl.srcDirs = ['shared/src', 'wmshell/shared/aidl'] + manifest.srcFile 'AndroidManifest.xml' + res.srcDirs = ['shared'] + } + } +} + +addFrameworkJar('framework-15.jar') +compileOnlyCommonJars() + +dependencies { + implementation 'androidx.core:core-animation:1.0.0-alpha02' + implementation 'androidx.core:core-ktx:1.9.0' + implementation "com.google.dagger:hilt-android:$daggerVersion" + ksp "com.google.dagger:hilt-compiler:$daggerVersion" +} diff --git a/wmshell/multivalentTests/Android.bp b/wmshell/multivalentTests/Android.bp new file mode 100644 index 0000000000..1ad19c9f30 --- /dev/null +++ b/wmshell/multivalentTests/Android.bp @@ -0,0 +1,99 @@ +// 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 { + // 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"], + default_team: "trendy_team_multitasking_windowing", +} + +android_app { + name: "WindowManagerShellRobolectric", + platform_apis: true, + static_libs: [ + "WindowManager-Shell", + ], + manifest: "AndroidManifestRobolectric.xml", + use_resource_processor: true, +} + +android_robolectric_test { + name: "WMShellRobolectricTests", + instrumentation_for: "WindowManagerShellRobolectric", + upstream: true, + java_resource_dirs: [ + "robolectric/config", + ], + srcs: [ + "src/**/*.kt", + ], + // TODO(b/323188766): Include BubbleStackViewTest once the robolectric issue is fixed. + exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"], + static_libs: [ + "junit", + "androidx.core_core-animation-testing", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-robolectric-prebuilt", + "mockito-kotlin2", + "truth", + ], + auto_gen_config: true, +} + +android_test { + name: "WMShellMultivalentTestsOnDevice", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "WindowManager-Shell", + "junit", + "androidx.core_core-animation-testing", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "frameworks-base-testutils", + "mockito-kotlin2", + "mockito-target-extended-minus-junit4", + "truth", + "platform-test-annotations", + "platform-test-rules", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + kotlincflags: ["-Xjvm-default=all"], + optimize: { + enabled: false, + }, + test_suites: ["device-tests"], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--extra-packages", + "com.android.wm.shell", + ], + manifest: "AndroidManifest.xml", +} diff --git a/wmshell/multivalentTests/AndroidManifest.xml b/wmshell/multivalentTests/AndroidManifest.xml new file mode 100644 index 0000000000..f8f8338e5f --- /dev/null +++ b/wmshell/multivalentTests/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/wmshell/multivalentTests/AndroidManifestRobolectric.xml b/wmshell/multivalentTests/AndroidManifestRobolectric.xml new file mode 100644 index 0000000000..ffcd7d46fb --- /dev/null +++ b/wmshell/multivalentTests/AndroidManifestRobolectric.xml @@ -0,0 +1,3 @@ + + diff --git a/wmshell/multivalentTests/AndroidTest.xml b/wmshell/multivalentTests/AndroidTest.xml new file mode 100644 index 0000000000..36fe8ec337 --- /dev/null +++ b/wmshell/multivalentTests/AndroidTest.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/wmshell/multivalentTests/OWNERS b/wmshell/multivalentTests/OWNERS new file mode 100644 index 0000000000..24c1a3a6d4 --- /dev/null +++ b/wmshell/multivalentTests/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com + diff --git a/wmshell/multivalentTests/robolectric/config/robolectric.properties b/wmshell/multivalentTests/robolectric/config/robolectric.properties new file mode 100644 index 0000000000..7a0527ccaa --- /dev/null +++ b/wmshell/multivalentTests/robolectric/config/robolectric.properties @@ -0,0 +1,2 @@ +sdk=NEWEST_SDK + diff --git a/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt new file mode 100644 index 0000000000..9e1440d571 --- /dev/null +++ b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.os.UserHandle +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests operations and the resulting state managed by [BubblePositioner]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubblePositionerTest { + + private lateinit var positioner: BubblePositioner + private val context = ApplicationProvider.getApplicationContext() + private val resources: Resources + get() = context.resources + + private val defaultDeviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, 1000, 2000), + isLargeScreen = false, + isSmallTablet = false, + isLandscape = false, + isRtl = false, + insets = Insets.of(0, 0, 0, 0) + ) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + } + + @Test + fun testUpdate() { + val insets = Insets.of(10, 20, 5, 15) + val screenBounds = Rect(0, 0, 1000, 1200) + val availableRect = Rect(screenBounds) + availableRect.inset(insets) + positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds)) + assertThat(positioner.availableRect).isEqualTo(availableRect) + assertThat(positioner.isLandscape).isFalse() + assertThat(positioner.isLargeScreen).isFalse() + assertThat(positioner.insets).isEqualTo(insets) + } + + @Test + fun testShowBubblesVertically_phonePortrait() { + positioner.update(defaultDeviceConfig) + assertThat(positioner.showBubblesVertically()).isFalse() + } + + @Test + fun testShowBubblesVertically_phoneLandscape() { + positioner.update(defaultDeviceConfig.copy(isLandscape = true)) + assertThat(positioner.isLandscape).isTrue() + assertThat(positioner.showBubblesVertically()).isTrue() + } + + @Test + fun testShowBubblesVertically_tablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + assertThat(positioner.showBubblesVertically()).isTrue() + } + + /** If a resting position hasn't been set, calling it will return the default position. */ + @Test + fun testGetRestingPosition_returnsDefaultPosition() { + positioner.update(defaultDeviceConfig) + val restingPosition = positioner.getRestingPosition() + val defaultPosition = positioner.defaultStartPosition + assertThat(restingPosition).isEqualTo(defaultPosition) + } + + /** If a resting position has been set, it'll return that instead of the default position. */ + @Test + fun testGetRestingPosition_returnsRestingPosition() { + positioner.update(defaultDeviceConfig) + val restingPosition = PointF(100f, 100f) + positioner.restingPosition = restingPosition + assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition) + } + + /** Test that the default resting position on phone is in upper left. */ + @Test + fun testGetRestingPosition_bubble_onPhone() { + positioner.update(defaultDeviceConfig) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_bubble_onPhone_RTL() { + positioner.update(defaultDeviceConfig.copy(isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + /** Test that the default resting position on tablet is middle left. */ + @Test + fun testGetRestingPosition_chatBubble_onTablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_chatBubble_onTablet_RTL() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + /** Test that the default resting position on tablet is middle right. */ + @Test + fun testGetDefaultPosition_appBubble_onTablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + assertThat(startPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(startPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_appBubble_onTablet_RTL() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + assertThat(startPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(startPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_afterBoundsChange() { + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600)) + ) + + // Set the resting position to the right side + var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY()) + positioner.restingPosition = restingPosition + + // Now make the device smaller + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600)) + ) + + // Check the resting position is on the correct side + allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right) + } + + @Test + fun testHasUserModifiedDefaultPosition_false() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + positioner.restingPosition = positioner.defaultStartPosition + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + } + + @Test + fun testHasUserModifiedDefaultPosition_true() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + positioner.restingPosition = PointF(0f, 100f) + assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue() + } + + @Test + fun testBubbleBarExpandedViewHeightAndWidth() { + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680 + val expandedViewVerticalSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = + spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing + val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width) + + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test + fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() { + val screenWidth = 300 + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, screenWidth, 2600) + ) + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 180 + val expandedViewSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing + val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test + fun testGetExpandedViewHeight_max() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT) + } + + @Test + fun testGetExpandedViewHeight_customHeight_valid() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + val minHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) + val bubble = + Bubble( + "key", + ShortcutInfo.Builder(context, "id").build(), + minHeight + 100 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor() + ) {} + + // Ensure the height is the same as the desired value + assertThat(positioner.getExpandedViewHeight(bubble)) + .isEqualTo(bubble.getDesiredHeight(context)) + } + + @Test + fun testGetExpandedViewHeight_customHeight_tooSmall() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val bubble = + Bubble( + "key", + ShortcutInfo.Builder(context, "id").build(), + 10 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor() + ) {} + + // Ensure the height is the same as the desired value + val minHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) + assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight) + } + + @Test + fun testGetMaxExpandedViewHeight_onLargeTablet() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val manageButtonHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) + val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = + 1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2 + assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */)) + .isEqualTo(expectedHeight) + } + + @Test + fun testAreBubblesBottomAligned_largeScreen_true() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isTrue() + } + + @Test + fun testAreBubblesBottomAligned_largeScreen_landscape_false() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testAreBubblesBottomAligned_smallTablet_false() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isSmallTablet = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testAreBubblesBottomAligned_phone_false() { + val deviceConfig = + defaultDeviceConfig.copy( + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testExpandedViewY_phoneLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height so it'll always be top aligned + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_phonePortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // Always top aligned in phone portrait + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_smallTabletLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isSmallTablet = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on small tablets + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_smallTabletPortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + isSmallTablet = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on small tablets + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_largeScreenLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on landscape, large tablet + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_largeScreenPortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + val manageButtonHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) + val manageButtonPlusMargin = + manageButtonHeight + + 2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin) + val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) + + val expectedExpandedViewY = + positioner.availableRect.bottom - + manageButtonPlusMargin - + positioner.getExpandedViewHeightForLargeScreen() - + pointerWidth + + // Bubbles are bottom aligned on portrait, large tablet + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(expectedExpandedViewY) + } + + @Test + fun testGetTaskViewContentWidth_onLeft() { + positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) + val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */) + val paddings = + positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) + } + + @Test + fun testGetTaskViewContentWidth_onRight() { + positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) + val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */) + val paddings = + positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) + } + + @Test + fun testIsBubbleBarOnLeft_defaultsToRight() { + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() { + positioner.update(defaultDeviceConfig.copy(isRtl = true)) + + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true) + } + + private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) { + positioner.setShowingInBubbleBar(true) + val windowBounds = Rect(0, 0, 2000, 2600) + val insets = Insets.of(10, 20, 5, 15) + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = insets, + windowBounds = windowBounds + ) + positioner.update(deviceConfig) + + val bubbleBarHeight = 100 + positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight + + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + + val left: Int + val right: Int + if (onLeft) { + // Pin to the left, calculate right + left = deviceConfig.insets.left + expandedViewPadding + right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } else { + // Pin to the right, calculate left + right = + deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding + left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } + // Above the bubble bar + val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding + // Calculate right and top based on size + val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow) + val expectedBounds = Rect(left, top, right, bottom) + + val bounds = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds) + + assertThat(bounds).isEqualTo(expectedBounds) + } + + private val defaultYPosition: Float + /** + * Calculates the Y position bubbles should be placed based on the config. Based on the + * calculations in [BubblePositioner.getDefaultStartPosition] and + * [BubbleStackView.RelativeStackPosition]. + */ + get() { + val isTablet = positioner.isLargeScreen + + // On tablet the position is centered, on phone it is an offset from the top. + val desiredY = + if (isTablet) { + positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f + } else { + context.resources + .getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y) + .toFloat() + } + // Since we're visually centering the bubbles on tablet, use total screen height rather + // than the available height. + val height = + if (isTablet) { + positioner.screenRect.height() + } else { + positioner.availableRect.height() + } + val offsetPercent = (desiredY / height).coerceIn(0f, 1f) + val allowableStackRegion = + positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent + } +} diff --git a/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt new file mode 100644 index 0000000000..327e205955 --- /dev/null +++ b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.UserHandle +import android.platform.test.flag.junit.SetFlagsRule +import android.view.IWindowManager +import android.view.WindowManager +import android.view.WindowManagerGlobal +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.protolog.common.ProtoLog +import com.android.launcher3.icons.BubbleIconFactory +import com.android.wm.shell.Flags +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix +import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + +/** Unit tests for [BubbleStackView]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleStackViewTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var positioner: BubblePositioner + private lateinit var iconFactory: BubbleIconFactory + private lateinit var expandedViewManager: FakeBubbleExpandedViewManager + private lateinit var bubbleStackView: BubbleStackView + private lateinit var shellExecutor: ShellExecutor + private lateinit var windowManager: IWindowManager + private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory + private lateinit var bubbleData: BubbleData + private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager + private var sysuiProxy = mock() + + @Before + fun setUp() { + PhysicsAnimatorTestUtils.prepareForTest() + // Disable protolog tool when running the tests from studio + ProtoLog.REQUIRE_PROTOLOGTOOL = false + windowManager = WindowManagerGlobal.getWindowManagerService()!! + shellExecutor = TestShellExecutor() + val windowManager = context.getSystemService(WindowManager::class.java) + iconFactory = + BubbleIconFactory( + context, + context.resources.getDimensionPixelSize(R.dimen.bubble_size), + context.resources.getDimensionPixelSize(R.dimen.bubble_badge_size), + Color.BLACK, + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width + ) + ) + positioner = BubblePositioner(context, windowManager) + bubbleData = + BubbleData( + context, + BubbleLogger(UiEventLoggerFake()), + positioner, + BubbleEducationController(context), + shellExecutor + ) + bubbleStackViewManager = FakeBubbleStackViewManager() + expandedViewManager = FakeBubbleExpandedViewManager() + bubbleTaskViewFactory = FakeBubbleTaskViewFactory() + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + context + .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit() + .putBoolean(StackEducationView.PREF_STACK_EDUCATION, true) + .apply() + } + + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + + @Test + fun addBubble() { + val bubble = createAndInflateBubble() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + } + + @Test + fun tapBubbleToExpand() { + val bubble = createAndInflateBubble() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble.iconView!!.performClick() + // we're checking the expanded state in BubbleData because that's the source of truth. + // This will eventually propagate an update back to the stack view, but setting the + // entire pipeline is outside the scope of a unit test. + assertThat(bubbleData.isExpanded).isTrue() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate).isNotNull() + assertThat(lastUpdate!!.expandedChanged).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + } + + @Test + fun tapDifferentBubble_shouldReorder() { + val bubble1 = createAndInflateChatBubble(key = "bubble1") + val bubble2 = createAndInflateChatBubble(key = "bubble2") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble1) + bubbleStackView.addBubble(bubble2) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(bubbleStackView.bubbleCount).isEqualTo(2) + assertThat(bubbleData.bubbles).hasSize(2) + assertThat(bubbleData.selectedBubble).isEqualTo(bubble2) + assertThat(bubble2.iconView).isNotNull() + + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble2.iconView!!.performClick() + assertThat(bubbleData.isExpanded).isTrue() + + bubbleStackView.setSelectedBubble(bubble2) + bubbleStackView.isExpanded = true + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble2", "bubble1") + .inOrder() + + // wait for idle to allow the animation to start + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + // wait for the expansion animation to complete before interacting with the bubbles + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y) + + // tap on bubble1 to select it + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble1.iconView!!.performClick() + } + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + + // tap on bubble1 again to collapse the stack + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // we have to set the selected bubble in the stack view manually because we don't have a + // listener wired up. + bubbleStackView.setSelectedBubble(bubble1) + bubble1.iconView!!.performClick() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + assertThat(bubbleData.isExpanded).isFalse() + assertThat(lastUpdate!!.orderChanged).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble1", "bubble2") + .inOrder() + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_noOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + assertThat(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + // Overflow shouldn't be attached + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_hasOverflowContents_hasOverflow() { + // Add a bubble to the overflow + val bubble1 = createAndInflateChatBubble(key = "bubble1") + bubbleData.notificationEntryUpdated(bubble1, false, false) + bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE) + assertThat(bubbleData.overflowBubbles).isNotEmpty() + + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_hasOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + assertThat(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_true() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_false() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // The overflow should've been removed + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_ignored() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // showOverflow should've been ignored, so the overflow would be attached + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + private fun createAndInflateChatBubble(key: String): Bubble { + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() + val bubble = + Bubble( + key, + shortcutInfo, + /* desiredHeight= */ 6, + Resources.ID_NULL, + "title", + /* taskId= */ 0, + "locus", + /* isDismissable= */ true, + directExecutor() + ) {} + inflateBubble(bubble) + return bubble + } + + private fun createAndInflateBubble(): Bubble { + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor()) + inflateBubble(bubble) + return bubble + } + + private fun inflateBubble(bubble: Bubble) { + bubble.setInflateSynchronously(true) + bubbleData.notificationEntryUpdated(bubble, true, false) + + val semaphore = Semaphore(0) + val callback: BubbleViewInfoTask.Callback = + BubbleViewInfoTask.Callback { semaphore.release() } + bubble.inflate( + callback, + context, + expandedViewManager, + bubbleTaskViewFactory, + positioner, + bubbleStackView, + null, + iconFactory, + false + ) + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubble.isInflated).isTrue() + } + + private class FakeBubbleStackViewManager : BubbleStackViewManager { + + override fun onAllBubblesAnimatedOut() {} + + override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {} + + override fun checkNotificationPanelExpandedState(callback: Consumer) {} + + override fun hideCurrentInputMethod() {} + } + + private class TestShellExecutor : ShellExecutor { + + override fun execute(runnable: Runnable) { + runnable.run() + } + + override fun executeDelayed(r: Runnable, delayMillis: Long) { + r.run() + } + + override fun removeCallbacks(r: Runnable?) {} + + override fun hasCallback(r: Runnable): Boolean = false + } + + private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory { + override fun create(): BubbleTaskView { + val taskViewTaskController = mock() + val taskView = TaskView(context, taskViewTaskController) + return BubbleTaskView(taskView, shellExecutor) + } + } + + private inner class FakeBubbleExpandedViewManager : BubbleExpandedViewManager { + + override val overflowBubbles: List + get() = emptyList() + + override fun setOverflowListener(listener: BubbleData.Listener) {} + + override fun collapseStack() {} + + override fun updateWindowFlagsForBackpress(intercept: Boolean) {} + + override fun promoteBubbleFromOverflow(bubble: Bubble) {} + + override fun removeBubble(key: String, reason: Int) {} + + override fun dismissBubble(bubble: Bubble, reason: Int) {} + + override fun setAppBubbleTaskId(key: String, taskId: Int) {} + + override fun isStackExpanded(): Boolean = false + + override fun isShowingAsBubbleBar(): Boolean = false + + override fun hideCurrentInputMethod() {} + } +} diff --git a/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt new file mode 100644 index 0000000000..398fd554f0 --- /dev/null +++ b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.ComponentName +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.taskview.TaskView + +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleTaskViewTest { + + private lateinit var bubbleTaskView: BubbleTaskView + private val context = ApplicationProvider.getApplicationContext() + private lateinit var taskView: TaskView + + @Before + fun setUp() { + taskView = mock() + bubbleTaskView = BubbleTaskView(taskView, directExecutor()) + } + + @Test + fun onTaskCreated_updatesState() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(bubbleTaskView.taskId).isEqualTo(123) + assertThat(bubbleTaskView.componentName).isEqualTo(componentName) + assertThat(bubbleTaskView.isCreated).isTrue() + } + + @Test + fun onTaskCreated_callsDelegateListener() { + var actualTaskId = -1 + var actualComponentName: ComponentName? = null + val delegateListener = object : TaskView.Listener { + override fun onTaskCreated(taskId: Int, name: ComponentName) { + actualTaskId = taskId + actualComponentName = name + } + } + bubbleTaskView.delegateListener = delegateListener + + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(actualTaskId).isEqualTo(123) + assertThat(actualComponentName).isEqualTo(componentName) + } + + @Test + fun cleanup_invalidTaskId_doesNotRemoveTask() { + bubbleTaskView.cleanup() + verify(taskView, never()).removeTask() + } + + @Test + fun cleanup_validTaskId_removesTask() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + bubbleTaskView.cleanup() + verify(taskView).removeTask() + } +} diff --git a/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt new file mode 100644 index 0000000000..ace2c13105 --- /dev/null +++ b/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.common.bubbles.BaseBubblePinController +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [BubbleExpandedViewPinController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleExpandedViewPinControllerTest { + + companion object { + @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + + const val BUBBLE_BAR_HEIGHT = 50 + } + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var positioner: BubblePositioner + private lateinit var container: FrameLayout + + private lateinit var controller: BubbleExpandedViewPinController + private lateinit var testListener: TestLocationChangeListener + + private val dropTargetView: View? + get() = container.findViewById(R.id.bubble_bar_drop_target) + + private val pointOnLeft = PointF(100f, 100f) + private val pointOnRight = PointF(1900f, 500f) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + container = FrameLayout(context) + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = + SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT + controller = BubbleExpandedViewPinController(context, container, positioner) + testListener = TestLocationChangeListener() + controller.setListener(testListener) + } + + @After + fun tearDown() { + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnRightSide() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(RIGHT) + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnLeftSide() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_rightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_leftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_rightToLeftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_leftToRightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_rightToLeft_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_leftToRight_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f + 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_rightToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_leftToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_rightToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_leftToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_rightToLeftToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_leftToRightToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_rightToLeftRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_leftToRightRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).containsExactly(RIGHT) + } + + private fun getExpectedDropTargetBoundsOnLeft(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + true /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun getExpectedDropTargetBoundsOnRight(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + false /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) + } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) + } + } + + private fun View.bounds(): Rect { + return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect -> + rect.offsetTo(x.toInt(), y.toInt()) + } + } + + internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener { + val locationChanges = mutableListOf() + val locationReleases = mutableListOf() + override fun onChange(location: BubbleBarLocation) { + locationChanges.add(location) + } + + override fun onRelease(location: BubbleBarLocation) { + locationReleases.add(location) + } + } +} diff --git a/wmshell/multivalentTestsForDevice/Android.bp b/wmshell/multivalentTestsForDevice/Android.bp new file mode 100644 index 0000000000..1ad19c9f30 --- /dev/null +++ b/wmshell/multivalentTestsForDevice/Android.bp @@ -0,0 +1,99 @@ +// 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 { + // 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"], + default_team: "trendy_team_multitasking_windowing", +} + +android_app { + name: "WindowManagerShellRobolectric", + platform_apis: true, + static_libs: [ + "WindowManager-Shell", + ], + manifest: "AndroidManifestRobolectric.xml", + use_resource_processor: true, +} + +android_robolectric_test { + name: "WMShellRobolectricTests", + instrumentation_for: "WindowManagerShellRobolectric", + upstream: true, + java_resource_dirs: [ + "robolectric/config", + ], + srcs: [ + "src/**/*.kt", + ], + // TODO(b/323188766): Include BubbleStackViewTest once the robolectric issue is fixed. + exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"], + static_libs: [ + "junit", + "androidx.core_core-animation-testing", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-robolectric-prebuilt", + "mockito-kotlin2", + "truth", + ], + auto_gen_config: true, +} + +android_test { + name: "WMShellMultivalentTestsOnDevice", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "WindowManager-Shell", + "junit", + "androidx.core_core-animation-testing", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "frameworks-base-testutils", + "mockito-kotlin2", + "mockito-target-extended-minus-junit4", + "truth", + "platform-test-annotations", + "platform-test-rules", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + kotlincflags: ["-Xjvm-default=all"], + optimize: { + enabled: false, + }, + test_suites: ["device-tests"], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--extra-packages", + "com.android.wm.shell", + ], + manifest: "AndroidManifest.xml", +} diff --git a/wmshell/multivalentTestsForDevice/AndroidManifest.xml b/wmshell/multivalentTestsForDevice/AndroidManifest.xml new file mode 100644 index 0000000000..f8f8338e5f --- /dev/null +++ b/wmshell/multivalentTestsForDevice/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/wmshell/multivalentTestsForDevice/AndroidManifestRobolectric.xml b/wmshell/multivalentTestsForDevice/AndroidManifestRobolectric.xml new file mode 100644 index 0000000000..ffcd7d46fb --- /dev/null +++ b/wmshell/multivalentTestsForDevice/AndroidManifestRobolectric.xml @@ -0,0 +1,3 @@ + + diff --git a/wmshell/multivalentTestsForDevice/AndroidTest.xml b/wmshell/multivalentTestsForDevice/AndroidTest.xml new file mode 100644 index 0000000000..36fe8ec337 --- /dev/null +++ b/wmshell/multivalentTestsForDevice/AndroidTest.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/wmshell/multivalentTestsForDevice/OWNERS b/wmshell/multivalentTestsForDevice/OWNERS new file mode 100644 index 0000000000..24c1a3a6d4 --- /dev/null +++ b/wmshell/multivalentTestsForDevice/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com + diff --git a/wmshell/multivalentTestsForDevice/robolectric/config/robolectric.properties b/wmshell/multivalentTestsForDevice/robolectric/config/robolectric.properties new file mode 100644 index 0000000000..7a0527ccaa --- /dev/null +++ b/wmshell/multivalentTestsForDevice/robolectric/config/robolectric.properties @@ -0,0 +1,2 @@ +sdk=NEWEST_SDK + diff --git a/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt new file mode 100644 index 0000000000..9e1440d571 --- /dev/null +++ b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.os.UserHandle +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests operations and the resulting state managed by [BubblePositioner]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubblePositionerTest { + + private lateinit var positioner: BubblePositioner + private val context = ApplicationProvider.getApplicationContext() + private val resources: Resources + get() = context.resources + + private val defaultDeviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, 1000, 2000), + isLargeScreen = false, + isSmallTablet = false, + isLandscape = false, + isRtl = false, + insets = Insets.of(0, 0, 0, 0) + ) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + } + + @Test + fun testUpdate() { + val insets = Insets.of(10, 20, 5, 15) + val screenBounds = Rect(0, 0, 1000, 1200) + val availableRect = Rect(screenBounds) + availableRect.inset(insets) + positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds)) + assertThat(positioner.availableRect).isEqualTo(availableRect) + assertThat(positioner.isLandscape).isFalse() + assertThat(positioner.isLargeScreen).isFalse() + assertThat(positioner.insets).isEqualTo(insets) + } + + @Test + fun testShowBubblesVertically_phonePortrait() { + positioner.update(defaultDeviceConfig) + assertThat(positioner.showBubblesVertically()).isFalse() + } + + @Test + fun testShowBubblesVertically_phoneLandscape() { + positioner.update(defaultDeviceConfig.copy(isLandscape = true)) + assertThat(positioner.isLandscape).isTrue() + assertThat(positioner.showBubblesVertically()).isTrue() + } + + @Test + fun testShowBubblesVertically_tablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + assertThat(positioner.showBubblesVertically()).isTrue() + } + + /** If a resting position hasn't been set, calling it will return the default position. */ + @Test + fun testGetRestingPosition_returnsDefaultPosition() { + positioner.update(defaultDeviceConfig) + val restingPosition = positioner.getRestingPosition() + val defaultPosition = positioner.defaultStartPosition + assertThat(restingPosition).isEqualTo(defaultPosition) + } + + /** If a resting position has been set, it'll return that instead of the default position. */ + @Test + fun testGetRestingPosition_returnsRestingPosition() { + positioner.update(defaultDeviceConfig) + val restingPosition = PointF(100f, 100f) + positioner.restingPosition = restingPosition + assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition) + } + + /** Test that the default resting position on phone is in upper left. */ + @Test + fun testGetRestingPosition_bubble_onPhone() { + positioner.update(defaultDeviceConfig) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_bubble_onPhone_RTL() { + positioner.update(defaultDeviceConfig.copy(isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + /** Test that the default resting position on tablet is middle left. */ + @Test + fun testGetRestingPosition_chatBubble_onTablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_chatBubble_onTablet_RTL() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + /** Test that the default resting position on tablet is middle right. */ + @Test + fun testGetDefaultPosition_appBubble_onTablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + assertThat(startPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(startPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_appBubble_onTablet_RTL() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + assertThat(startPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(startPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_afterBoundsChange() { + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600)) + ) + + // Set the resting position to the right side + var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY()) + positioner.restingPosition = restingPosition + + // Now make the device smaller + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600)) + ) + + // Check the resting position is on the correct side + allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right) + } + + @Test + fun testHasUserModifiedDefaultPosition_false() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + positioner.restingPosition = positioner.defaultStartPosition + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + } + + @Test + fun testHasUserModifiedDefaultPosition_true() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + positioner.restingPosition = PointF(0f, 100f) + assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue() + } + + @Test + fun testBubbleBarExpandedViewHeightAndWidth() { + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680 + val expandedViewVerticalSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = + spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing + val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width) + + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test + fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() { + val screenWidth = 300 + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, screenWidth, 2600) + ) + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 180 + val expandedViewSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing + val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test + fun testGetExpandedViewHeight_max() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT) + } + + @Test + fun testGetExpandedViewHeight_customHeight_valid() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + val minHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) + val bubble = + Bubble( + "key", + ShortcutInfo.Builder(context, "id").build(), + minHeight + 100 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor() + ) {} + + // Ensure the height is the same as the desired value + assertThat(positioner.getExpandedViewHeight(bubble)) + .isEqualTo(bubble.getDesiredHeight(context)) + } + + @Test + fun testGetExpandedViewHeight_customHeight_tooSmall() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val bubble = + Bubble( + "key", + ShortcutInfo.Builder(context, "id").build(), + 10 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor() + ) {} + + // Ensure the height is the same as the desired value + val minHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) + assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight) + } + + @Test + fun testGetMaxExpandedViewHeight_onLargeTablet() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val manageButtonHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) + val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = + 1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2 + assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */)) + .isEqualTo(expectedHeight) + } + + @Test + fun testAreBubblesBottomAligned_largeScreen_true() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isTrue() + } + + @Test + fun testAreBubblesBottomAligned_largeScreen_landscape_false() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testAreBubblesBottomAligned_smallTablet_false() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isSmallTablet = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testAreBubblesBottomAligned_phone_false() { + val deviceConfig = + defaultDeviceConfig.copy( + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testExpandedViewY_phoneLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height so it'll always be top aligned + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_phonePortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // Always top aligned in phone portrait + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_smallTabletLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isSmallTablet = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on small tablets + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_smallTabletPortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + isSmallTablet = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on small tablets + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_largeScreenLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on landscape, large tablet + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_largeScreenPortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + val manageButtonHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) + val manageButtonPlusMargin = + manageButtonHeight + + 2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin) + val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) + + val expectedExpandedViewY = + positioner.availableRect.bottom - + manageButtonPlusMargin - + positioner.getExpandedViewHeightForLargeScreen() - + pointerWidth + + // Bubbles are bottom aligned on portrait, large tablet + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(expectedExpandedViewY) + } + + @Test + fun testGetTaskViewContentWidth_onLeft() { + positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) + val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */) + val paddings = + positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) + } + + @Test + fun testGetTaskViewContentWidth_onRight() { + positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) + val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */) + val paddings = + positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) + } + + @Test + fun testIsBubbleBarOnLeft_defaultsToRight() { + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() { + positioner.update(defaultDeviceConfig.copy(isRtl = true)) + + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true) + } + + private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) { + positioner.setShowingInBubbleBar(true) + val windowBounds = Rect(0, 0, 2000, 2600) + val insets = Insets.of(10, 20, 5, 15) + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = insets, + windowBounds = windowBounds + ) + positioner.update(deviceConfig) + + val bubbleBarHeight = 100 + positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight + + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + + val left: Int + val right: Int + if (onLeft) { + // Pin to the left, calculate right + left = deviceConfig.insets.left + expandedViewPadding + right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } else { + // Pin to the right, calculate left + right = + deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding + left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } + // Above the bubble bar + val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding + // Calculate right and top based on size + val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow) + val expectedBounds = Rect(left, top, right, bottom) + + val bounds = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds) + + assertThat(bounds).isEqualTo(expectedBounds) + } + + private val defaultYPosition: Float + /** + * Calculates the Y position bubbles should be placed based on the config. Based on the + * calculations in [BubblePositioner.getDefaultStartPosition] and + * [BubbleStackView.RelativeStackPosition]. + */ + get() { + val isTablet = positioner.isLargeScreen + + // On tablet the position is centered, on phone it is an offset from the top. + val desiredY = + if (isTablet) { + positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f + } else { + context.resources + .getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y) + .toFloat() + } + // Since we're visually centering the bubbles on tablet, use total screen height rather + // than the available height. + val height = + if (isTablet) { + positioner.screenRect.height() + } else { + positioner.availableRect.height() + } + val offsetPercent = (desiredY / height).coerceIn(0f, 1f) + val allowableStackRegion = + positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent + } +} diff --git a/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt new file mode 100644 index 0000000000..327e205955 --- /dev/null +++ b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.UserHandle +import android.platform.test.flag.junit.SetFlagsRule +import android.view.IWindowManager +import android.view.WindowManager +import android.view.WindowManagerGlobal +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.protolog.common.ProtoLog +import com.android.launcher3.icons.BubbleIconFactory +import com.android.wm.shell.Flags +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix +import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + +/** Unit tests for [BubbleStackView]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleStackViewTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var positioner: BubblePositioner + private lateinit var iconFactory: BubbleIconFactory + private lateinit var expandedViewManager: FakeBubbleExpandedViewManager + private lateinit var bubbleStackView: BubbleStackView + private lateinit var shellExecutor: ShellExecutor + private lateinit var windowManager: IWindowManager + private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory + private lateinit var bubbleData: BubbleData + private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager + private var sysuiProxy = mock() + + @Before + fun setUp() { + PhysicsAnimatorTestUtils.prepareForTest() + // Disable protolog tool when running the tests from studio + ProtoLog.REQUIRE_PROTOLOGTOOL = false + windowManager = WindowManagerGlobal.getWindowManagerService()!! + shellExecutor = TestShellExecutor() + val windowManager = context.getSystemService(WindowManager::class.java) + iconFactory = + BubbleIconFactory( + context, + context.resources.getDimensionPixelSize(R.dimen.bubble_size), + context.resources.getDimensionPixelSize(R.dimen.bubble_badge_size), + Color.BLACK, + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width + ) + ) + positioner = BubblePositioner(context, windowManager) + bubbleData = + BubbleData( + context, + BubbleLogger(UiEventLoggerFake()), + positioner, + BubbleEducationController(context), + shellExecutor + ) + bubbleStackViewManager = FakeBubbleStackViewManager() + expandedViewManager = FakeBubbleExpandedViewManager() + bubbleTaskViewFactory = FakeBubbleTaskViewFactory() + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + context + .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit() + .putBoolean(StackEducationView.PREF_STACK_EDUCATION, true) + .apply() + } + + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + + @Test + fun addBubble() { + val bubble = createAndInflateBubble() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + } + + @Test + fun tapBubbleToExpand() { + val bubble = createAndInflateBubble() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble.iconView!!.performClick() + // we're checking the expanded state in BubbleData because that's the source of truth. + // This will eventually propagate an update back to the stack view, but setting the + // entire pipeline is outside the scope of a unit test. + assertThat(bubbleData.isExpanded).isTrue() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate).isNotNull() + assertThat(lastUpdate!!.expandedChanged).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + } + + @Test + fun tapDifferentBubble_shouldReorder() { + val bubble1 = createAndInflateChatBubble(key = "bubble1") + val bubble2 = createAndInflateChatBubble(key = "bubble2") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble1) + bubbleStackView.addBubble(bubble2) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(bubbleStackView.bubbleCount).isEqualTo(2) + assertThat(bubbleData.bubbles).hasSize(2) + assertThat(bubbleData.selectedBubble).isEqualTo(bubble2) + assertThat(bubble2.iconView).isNotNull() + + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble2.iconView!!.performClick() + assertThat(bubbleData.isExpanded).isTrue() + + bubbleStackView.setSelectedBubble(bubble2) + bubbleStackView.isExpanded = true + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble2", "bubble1") + .inOrder() + + // wait for idle to allow the animation to start + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + // wait for the expansion animation to complete before interacting with the bubbles + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y) + + // tap on bubble1 to select it + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble1.iconView!!.performClick() + } + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + + // tap on bubble1 again to collapse the stack + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // we have to set the selected bubble in the stack view manually because we don't have a + // listener wired up. + bubbleStackView.setSelectedBubble(bubble1) + bubble1.iconView!!.performClick() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + assertThat(bubbleData.isExpanded).isFalse() + assertThat(lastUpdate!!.orderChanged).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble1", "bubble2") + .inOrder() + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_noOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + assertThat(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + // Overflow shouldn't be attached + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_hasOverflowContents_hasOverflow() { + // Add a bubble to the overflow + val bubble1 = createAndInflateChatBubble(key = "bubble1") + bubbleData.notificationEntryUpdated(bubble1, false, false) + bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE) + assertThat(bubbleData.overflowBubbles).isNotEmpty() + + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_hasOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + assertThat(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_true() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_false() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // The overflow should've been removed + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_ignored() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // showOverflow should've been ignored, so the overflow would be attached + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + private fun createAndInflateChatBubble(key: String): Bubble { + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() + val bubble = + Bubble( + key, + shortcutInfo, + /* desiredHeight= */ 6, + Resources.ID_NULL, + "title", + /* taskId= */ 0, + "locus", + /* isDismissable= */ true, + directExecutor() + ) {} + inflateBubble(bubble) + return bubble + } + + private fun createAndInflateBubble(): Bubble { + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor()) + inflateBubble(bubble) + return bubble + } + + private fun inflateBubble(bubble: Bubble) { + bubble.setInflateSynchronously(true) + bubbleData.notificationEntryUpdated(bubble, true, false) + + val semaphore = Semaphore(0) + val callback: BubbleViewInfoTask.Callback = + BubbleViewInfoTask.Callback { semaphore.release() } + bubble.inflate( + callback, + context, + expandedViewManager, + bubbleTaskViewFactory, + positioner, + bubbleStackView, + null, + iconFactory, + false + ) + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubble.isInflated).isTrue() + } + + private class FakeBubbleStackViewManager : BubbleStackViewManager { + + override fun onAllBubblesAnimatedOut() {} + + override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {} + + override fun checkNotificationPanelExpandedState(callback: Consumer) {} + + override fun hideCurrentInputMethod() {} + } + + private class TestShellExecutor : ShellExecutor { + + override fun execute(runnable: Runnable) { + runnable.run() + } + + override fun executeDelayed(r: Runnable, delayMillis: Long) { + r.run() + } + + override fun removeCallbacks(r: Runnable?) {} + + override fun hasCallback(r: Runnable): Boolean = false + } + + private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory { + override fun create(): BubbleTaskView { + val taskViewTaskController = mock() + val taskView = TaskView(context, taskViewTaskController) + return BubbleTaskView(taskView, shellExecutor) + } + } + + private inner class FakeBubbleExpandedViewManager : BubbleExpandedViewManager { + + override val overflowBubbles: List + get() = emptyList() + + override fun setOverflowListener(listener: BubbleData.Listener) {} + + override fun collapseStack() {} + + override fun updateWindowFlagsForBackpress(intercept: Boolean) {} + + override fun promoteBubbleFromOverflow(bubble: Bubble) {} + + override fun removeBubble(key: String, reason: Int) {} + + override fun dismissBubble(bubble: Bubble, reason: Int) {} + + override fun setAppBubbleTaskId(key: String, taskId: Int) {} + + override fun isStackExpanded(): Boolean = false + + override fun isShowingAsBubbleBar(): Boolean = false + + override fun hideCurrentInputMethod() {} + } +} diff --git a/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt new file mode 100644 index 0000000000..398fd554f0 --- /dev/null +++ b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.ComponentName +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.taskview.TaskView + +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleTaskViewTest { + + private lateinit var bubbleTaskView: BubbleTaskView + private val context = ApplicationProvider.getApplicationContext() + private lateinit var taskView: TaskView + + @Before + fun setUp() { + taskView = mock() + bubbleTaskView = BubbleTaskView(taskView, directExecutor()) + } + + @Test + fun onTaskCreated_updatesState() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(bubbleTaskView.taskId).isEqualTo(123) + assertThat(bubbleTaskView.componentName).isEqualTo(componentName) + assertThat(bubbleTaskView.isCreated).isTrue() + } + + @Test + fun onTaskCreated_callsDelegateListener() { + var actualTaskId = -1 + var actualComponentName: ComponentName? = null + val delegateListener = object : TaskView.Listener { + override fun onTaskCreated(taskId: Int, name: ComponentName) { + actualTaskId = taskId + actualComponentName = name + } + } + bubbleTaskView.delegateListener = delegateListener + + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(actualTaskId).isEqualTo(123) + assertThat(actualComponentName).isEqualTo(componentName) + } + + @Test + fun cleanup_invalidTaskId_doesNotRemoveTask() { + bubbleTaskView.cleanup() + verify(taskView, never()).removeTask() + } + + @Test + fun cleanup_validTaskId_removesTask() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + bubbleTaskView.cleanup() + verify(taskView).removeTask() + } +} diff --git a/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt new file mode 100644 index 0000000000..ace2c13105 --- /dev/null +++ b/wmshell/multivalentTestsForDevice/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.common.bubbles.BaseBubblePinController +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [BubbleExpandedViewPinController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleExpandedViewPinControllerTest { + + companion object { + @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + + const val BUBBLE_BAR_HEIGHT = 50 + } + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var positioner: BubblePositioner + private lateinit var container: FrameLayout + + private lateinit var controller: BubbleExpandedViewPinController + private lateinit var testListener: TestLocationChangeListener + + private val dropTargetView: View? + get() = container.findViewById(R.id.bubble_bar_drop_target) + + private val pointOnLeft = PointF(100f, 100f) + private val pointOnRight = PointF(1900f, 500f) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + container = FrameLayout(context) + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = + SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT + controller = BubbleExpandedViewPinController(context, container, positioner) + testListener = TestLocationChangeListener() + controller.setListener(testListener) + } + + @After + fun tearDown() { + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnRightSide() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(RIGHT) + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnLeftSide() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_rightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_leftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_rightToLeftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_leftToRightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_rightToLeft_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_leftToRight_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f + 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_rightToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_leftToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_rightToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_leftToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_rightToLeftToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_leftToRightToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_rightToLeftRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_leftToRightRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).containsExactly(RIGHT) + } + + private fun getExpectedDropTargetBoundsOnLeft(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + true /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun getExpectedDropTargetBoundsOnRight(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + false /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) + } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) + } + } + + private fun View.bounds(): Rect { + return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect -> + rect.offsetTo(x.toInt(), y.toInt()) + } + } + + internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener { + val locationChanges = mutableListOf() + val locationReleases = mutableListOf() + override fun onChange(location: BubbleBarLocation) { + locationChanges.add(location) + } + + override fun onRelease(location: BubbleBarLocation) { + locationReleases.add(location) + } + } +} diff --git a/wmshell/multivalentTestsForDeviceless/Android.bp b/wmshell/multivalentTestsForDeviceless/Android.bp new file mode 100644 index 0000000000..1ad19c9f30 --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/Android.bp @@ -0,0 +1,99 @@ +// 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 { + // 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"], + default_team: "trendy_team_multitasking_windowing", +} + +android_app { + name: "WindowManagerShellRobolectric", + platform_apis: true, + static_libs: [ + "WindowManager-Shell", + ], + manifest: "AndroidManifestRobolectric.xml", + use_resource_processor: true, +} + +android_robolectric_test { + name: "WMShellRobolectricTests", + instrumentation_for: "WindowManagerShellRobolectric", + upstream: true, + java_resource_dirs: [ + "robolectric/config", + ], + srcs: [ + "src/**/*.kt", + ], + // TODO(b/323188766): Include BubbleStackViewTest once the robolectric issue is fixed. + exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"], + static_libs: [ + "junit", + "androidx.core_core-animation-testing", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-robolectric-prebuilt", + "mockito-kotlin2", + "truth", + ], + auto_gen_config: true, +} + +android_test { + name: "WMShellMultivalentTestsOnDevice", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "WindowManager-Shell", + "junit", + "androidx.core_core-animation-testing", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "frameworks-base-testutils", + "mockito-kotlin2", + "mockito-target-extended-minus-junit4", + "truth", + "platform-test-annotations", + "platform-test-rules", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + kotlincflags: ["-Xjvm-default=all"], + optimize: { + enabled: false, + }, + test_suites: ["device-tests"], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--extra-packages", + "com.android.wm.shell", + ], + manifest: "AndroidManifest.xml", +} diff --git a/wmshell/multivalentTestsForDeviceless/AndroidManifest.xml b/wmshell/multivalentTestsForDeviceless/AndroidManifest.xml new file mode 100644 index 0000000000..f8f8338e5f --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/wmshell/multivalentTestsForDeviceless/AndroidManifestRobolectric.xml b/wmshell/multivalentTestsForDeviceless/AndroidManifestRobolectric.xml new file mode 100644 index 0000000000..ffcd7d46fb --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/AndroidManifestRobolectric.xml @@ -0,0 +1,3 @@ + + diff --git a/wmshell/multivalentTestsForDeviceless/AndroidTest.xml b/wmshell/multivalentTestsForDeviceless/AndroidTest.xml new file mode 100644 index 0000000000..36fe8ec337 --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/AndroidTest.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/wmshell/multivalentTestsForDeviceless/OWNERS b/wmshell/multivalentTestsForDeviceless/OWNERS new file mode 100644 index 0000000000..24c1a3a6d4 --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com + diff --git a/wmshell/multivalentTestsForDeviceless/robolectric/config/robolectric.properties b/wmshell/multivalentTestsForDeviceless/robolectric/config/robolectric.properties new file mode 100644 index 0000000000..7a0527ccaa --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/robolectric/config/robolectric.properties @@ -0,0 +1,2 @@ +sdk=NEWEST_SDK + diff --git a/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt new file mode 100644 index 0000000000..9e1440d571 --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.os.UserHandle +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests operations and the resulting state managed by [BubblePositioner]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubblePositionerTest { + + private lateinit var positioner: BubblePositioner + private val context = ApplicationProvider.getApplicationContext() + private val resources: Resources + get() = context.resources + + private val defaultDeviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, 1000, 2000), + isLargeScreen = false, + isSmallTablet = false, + isLandscape = false, + isRtl = false, + insets = Insets.of(0, 0, 0, 0) + ) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + } + + @Test + fun testUpdate() { + val insets = Insets.of(10, 20, 5, 15) + val screenBounds = Rect(0, 0, 1000, 1200) + val availableRect = Rect(screenBounds) + availableRect.inset(insets) + positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds)) + assertThat(positioner.availableRect).isEqualTo(availableRect) + assertThat(positioner.isLandscape).isFalse() + assertThat(positioner.isLargeScreen).isFalse() + assertThat(positioner.insets).isEqualTo(insets) + } + + @Test + fun testShowBubblesVertically_phonePortrait() { + positioner.update(defaultDeviceConfig) + assertThat(positioner.showBubblesVertically()).isFalse() + } + + @Test + fun testShowBubblesVertically_phoneLandscape() { + positioner.update(defaultDeviceConfig.copy(isLandscape = true)) + assertThat(positioner.isLandscape).isTrue() + assertThat(positioner.showBubblesVertically()).isTrue() + } + + @Test + fun testShowBubblesVertically_tablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + assertThat(positioner.showBubblesVertically()).isTrue() + } + + /** If a resting position hasn't been set, calling it will return the default position. */ + @Test + fun testGetRestingPosition_returnsDefaultPosition() { + positioner.update(defaultDeviceConfig) + val restingPosition = positioner.getRestingPosition() + val defaultPosition = positioner.defaultStartPosition + assertThat(restingPosition).isEqualTo(defaultPosition) + } + + /** If a resting position has been set, it'll return that instead of the default position. */ + @Test + fun testGetRestingPosition_returnsRestingPosition() { + positioner.update(defaultDeviceConfig) + val restingPosition = PointF(100f, 100f) + positioner.restingPosition = restingPosition + assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition) + } + + /** Test that the default resting position on phone is in upper left. */ + @Test + fun testGetRestingPosition_bubble_onPhone() { + positioner.update(defaultDeviceConfig) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_bubble_onPhone_RTL() { + positioner.update(defaultDeviceConfig.copy(isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + /** Test that the default resting position on tablet is middle left. */ + @Test + fun testGetRestingPosition_chatBubble_onTablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_chatBubble_onTablet_RTL() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = positioner.getRestingPosition() + assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(restingPosition.y).isEqualTo(defaultYPosition) + } + + /** Test that the default resting position on tablet is middle right. */ + @Test + fun testGetDefaultPosition_appBubble_onTablet() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + assertThat(startPosition.x).isEqualTo(allowableStackRegion.right) + assertThat(startPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_appBubble_onTablet_RTL() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + assertThat(startPosition.x).isEqualTo(allowableStackRegion.left) + assertThat(startPosition.y).isEqualTo(defaultYPosition) + } + + @Test + fun testGetRestingPosition_afterBoundsChange() { + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600)) + ) + + // Set the resting position to the right side + var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY()) + positioner.restingPosition = restingPosition + + // Now make the device smaller + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600)) + ) + + // Check the resting position is on the correct side + allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right) + } + + @Test + fun testHasUserModifiedDefaultPosition_false() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + positioner.restingPosition = positioner.defaultStartPosition + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + } + + @Test + fun testHasUserModifiedDefaultPosition_true() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) + assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() + positioner.restingPosition = PointF(0f, 100f) + assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue() + } + + @Test + fun testBubbleBarExpandedViewHeightAndWidth() { + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680 + val expandedViewVerticalSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = + spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing + val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width) + + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test + fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() { + val screenWidth = 300 + val deviceConfig = + defaultDeviceConfig.copy( + // portrait orientation + isLandscape = false, + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, screenWidth, 2600) + ) + positioner.setShowingInBubbleBar(true) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = 2500 + + val spaceBetweenTopInsetAndBubbleBarInLandscape = 180 + val expandedViewSpacing = + resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing + val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing + assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth) + assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight) + } + + @Test + fun testGetExpandedViewHeight_max() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT) + } + + @Test + fun testGetExpandedViewHeight_customHeight_valid() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + val minHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) + val bubble = + Bubble( + "key", + ShortcutInfo.Builder(context, "id").build(), + minHeight + 100 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor() + ) {} + + // Ensure the height is the same as the desired value + assertThat(positioner.getExpandedViewHeight(bubble)) + .isEqualTo(bubble.getDesiredHeight(context)) + } + + @Test + fun testGetExpandedViewHeight_customHeight_tooSmall() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val bubble = + Bubble( + "key", + ShortcutInfo.Builder(context, "id").build(), + 10 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor() + ) {} + + // Ensure the height is the same as the desired value + val minHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height) + assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight) + } + + @Test + fun testGetMaxExpandedViewHeight_onLargeTablet() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val manageButtonHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) + val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + val expectedHeight = + 1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2 + assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */)) + .isEqualTo(expectedHeight) + } + + @Test + fun testAreBubblesBottomAligned_largeScreen_true() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isTrue() + } + + @Test + fun testAreBubblesBottomAligned_largeScreen_landscape_false() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testAreBubblesBottomAligned_smallTablet_false() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isSmallTablet = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testAreBubblesBottomAligned_phone_false() { + val deviceConfig = + defaultDeviceConfig.copy( + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + assertThat(positioner.areBubblesBottomAligned()).isFalse() + } + + @Test + fun testExpandedViewY_phoneLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height so it'll always be top aligned + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_phonePortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // Always top aligned in phone portrait + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_smallTabletLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isSmallTablet = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on small tablets + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_smallTabletPortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + isSmallTablet = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on small tablets + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_largeScreenLandscape() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + // This bubble will have max height which is always top aligned on landscape, large tablet + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(positioner.getExpandedViewYTopAligned()) + } + + @Test + fun testExpandedViewY_largeScreenPortrait() { + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 1800, 2600) + ) + positioner.update(deviceConfig) + + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + + val manageButtonHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) + val manageButtonPlusMargin = + manageButtonHeight + + 2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin) + val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width) + + val expectedExpandedViewY = + positioner.availableRect.bottom - + manageButtonPlusMargin - + positioner.getExpandedViewHeightForLargeScreen() - + pointerWidth + + // Bubbles are bottom aligned on portrait, large tablet + assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) + .isEqualTo(expectedExpandedViewY) + } + + @Test + fun testGetTaskViewContentWidth_onLeft() { + positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) + val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */) + val paddings = + positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) + } + + @Test + fun testGetTaskViewContentWidth_onRight() { + positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) + val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */) + val paddings = + positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) + } + + @Test + fun testIsBubbleBarOnLeft_defaultsToRight() { + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() { + positioner.update(defaultDeviceConfig.copy(isRtl = true)) + + positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + + // Check that left and right return expected position + positioner.bubbleBarLocation = BubbleBarLocation.LEFT + assertThat(positioner.isBubbleBarOnLeft).isTrue() + positioner.bubbleBarLocation = BubbleBarLocation.RIGHT + assertThat(positioner.isBubbleBarOnLeft).isFalse() + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true) + } + + private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) { + positioner.setShowingInBubbleBar(true) + val windowBounds = Rect(0, 0, 2000, 2600) + val insets = Insets.of(10, 20, 5, 15) + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = insets, + windowBounds = windowBounds + ) + positioner.update(deviceConfig) + + val bubbleBarHeight = 100 + positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight + + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + + val left: Int + val right: Int + if (onLeft) { + // Pin to the left, calculate right + left = deviceConfig.insets.left + expandedViewPadding + right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } else { + // Pin to the right, calculate left + right = + deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding + left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } + // Above the bubble bar + val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding + // Calculate right and top based on size + val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow) + val expectedBounds = Rect(left, top, right, bottom) + + val bounds = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds) + + assertThat(bounds).isEqualTo(expectedBounds) + } + + private val defaultYPosition: Float + /** + * Calculates the Y position bubbles should be placed based on the config. Based on the + * calculations in [BubblePositioner.getDefaultStartPosition] and + * [BubbleStackView.RelativeStackPosition]. + */ + get() { + val isTablet = positioner.isLargeScreen + + // On tablet the position is centered, on phone it is an offset from the top. + val desiredY = + if (isTablet) { + positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f + } else { + context.resources + .getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y) + .toFloat() + } + // Since we're visually centering the bubbles on tablet, use total screen height rather + // than the available height. + val height = + if (isTablet) { + positioner.screenRect.height() + } else { + positioner.availableRect.height() + } + val offsetPercent = (desiredY / height).coerceIn(0f, 1f) + val allowableStackRegion = + positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent + } +} diff --git a/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt new file mode 100644 index 0000000000..327e205955 --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.UserHandle +import android.platform.test.flag.junit.SetFlagsRule +import android.view.IWindowManager +import android.view.WindowManager +import android.view.WindowManagerGlobal +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.protolog.common.ProtoLog +import com.android.launcher3.icons.BubbleIconFactory +import com.android.wm.shell.Flags +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix +import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + +/** Unit tests for [BubbleStackView]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleStackViewTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var positioner: BubblePositioner + private lateinit var iconFactory: BubbleIconFactory + private lateinit var expandedViewManager: FakeBubbleExpandedViewManager + private lateinit var bubbleStackView: BubbleStackView + private lateinit var shellExecutor: ShellExecutor + private lateinit var windowManager: IWindowManager + private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory + private lateinit var bubbleData: BubbleData + private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager + private var sysuiProxy = mock() + + @Before + fun setUp() { + PhysicsAnimatorTestUtils.prepareForTest() + // Disable protolog tool when running the tests from studio + ProtoLog.REQUIRE_PROTOLOGTOOL = false + windowManager = WindowManagerGlobal.getWindowManagerService()!! + shellExecutor = TestShellExecutor() + val windowManager = context.getSystemService(WindowManager::class.java) + iconFactory = + BubbleIconFactory( + context, + context.resources.getDimensionPixelSize(R.dimen.bubble_size), + context.resources.getDimensionPixelSize(R.dimen.bubble_badge_size), + Color.BLACK, + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width + ) + ) + positioner = BubblePositioner(context, windowManager) + bubbleData = + BubbleData( + context, + BubbleLogger(UiEventLoggerFake()), + positioner, + BubbleEducationController(context), + shellExecutor + ) + bubbleStackViewManager = FakeBubbleStackViewManager() + expandedViewManager = FakeBubbleExpandedViewManager() + bubbleTaskViewFactory = FakeBubbleTaskViewFactory() + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + context + .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit() + .putBoolean(StackEducationView.PREF_STACK_EDUCATION, true) + .apply() + } + + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + + @Test + fun addBubble() { + val bubble = createAndInflateBubble() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + } + + @Test + fun tapBubbleToExpand() { + val bubble = createAndInflateBubble() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble.iconView!!.performClick() + // we're checking the expanded state in BubbleData because that's the source of truth. + // This will eventually propagate an update back to the stack view, but setting the + // entire pipeline is outside the scope of a unit test. + assertThat(bubbleData.isExpanded).isTrue() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate).isNotNull() + assertThat(lastUpdate!!.expandedChanged).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + } + + @Test + fun tapDifferentBubble_shouldReorder() { + val bubble1 = createAndInflateChatBubble(key = "bubble1") + val bubble2 = createAndInflateChatBubble(key = "bubble2") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble1) + bubbleStackView.addBubble(bubble2) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(bubbleStackView.bubbleCount).isEqualTo(2) + assertThat(bubbleData.bubbles).hasSize(2) + assertThat(bubbleData.selectedBubble).isEqualTo(bubble2) + assertThat(bubble2.iconView).isNotNull() + + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble2.iconView!!.performClick() + assertThat(bubbleData.isExpanded).isTrue() + + bubbleStackView.setSelectedBubble(bubble2) + bubbleStackView.isExpanded = true + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble2", "bubble1") + .inOrder() + + // wait for idle to allow the animation to start + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + // wait for the expansion animation to complete before interacting with the bubbles + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y) + + // tap on bubble1 to select it + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble1.iconView!!.performClick() + } + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + + // tap on bubble1 again to collapse the stack + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // we have to set the selected bubble in the stack view manually because we don't have a + // listener wired up. + bubbleStackView.setSelectedBubble(bubble1) + bubble1.iconView!!.performClick() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + assertThat(bubbleData.isExpanded).isFalse() + assertThat(lastUpdate!!.orderChanged).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble1", "bubble2") + .inOrder() + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_noOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + assertThat(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + // Overflow shouldn't be attached + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_hasOverflowContents_hasOverflow() { + // Add a bubble to the overflow + val bubble1 = createAndInflateChatBubble(key = "bubble1") + bubbleData.notificationEntryUpdated(bubble1, false, false) + bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE) + assertThat(bubbleData.overflowBubbles).isNotEmpty() + + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun testCreateStackView_noOverflowContents_hasOverflow() { + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + positioner, + bubbleData, + null, + FloatingContentCoordinator(), + { sysuiProxy }, + shellExecutor + ) + + assertThat(bubbleData.overflowBubbles).isEmpty() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_true() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_false() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(true) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // The overflow should've been removed + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1) + } + + @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW) + @Test + fun showOverflow_ignored() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.showOverflow(false) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + // showOverflow should've been ignored, so the overflow would be attached + val bubbleOverflow = bubbleData.overflow + assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) + } + + private fun createAndInflateChatBubble(key: String): Bubble { + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() + val bubble = + Bubble( + key, + shortcutInfo, + /* desiredHeight= */ 6, + Resources.ID_NULL, + "title", + /* taskId= */ 0, + "locus", + /* isDismissable= */ true, + directExecutor() + ) {} + inflateBubble(bubble) + return bubble + } + + private fun createAndInflateBubble(): Bubble { + val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor()) + inflateBubble(bubble) + return bubble + } + + private fun inflateBubble(bubble: Bubble) { + bubble.setInflateSynchronously(true) + bubbleData.notificationEntryUpdated(bubble, true, false) + + val semaphore = Semaphore(0) + val callback: BubbleViewInfoTask.Callback = + BubbleViewInfoTask.Callback { semaphore.release() } + bubble.inflate( + callback, + context, + expandedViewManager, + bubbleTaskViewFactory, + positioner, + bubbleStackView, + null, + iconFactory, + false + ) + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubble.isInflated).isTrue() + } + + private class FakeBubbleStackViewManager : BubbleStackViewManager { + + override fun onAllBubblesAnimatedOut() {} + + override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {} + + override fun checkNotificationPanelExpandedState(callback: Consumer) {} + + override fun hideCurrentInputMethod() {} + } + + private class TestShellExecutor : ShellExecutor { + + override fun execute(runnable: Runnable) { + runnable.run() + } + + override fun executeDelayed(r: Runnable, delayMillis: Long) { + r.run() + } + + override fun removeCallbacks(r: Runnable?) {} + + override fun hasCallback(r: Runnable): Boolean = false + } + + private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory { + override fun create(): BubbleTaskView { + val taskViewTaskController = mock() + val taskView = TaskView(context, taskViewTaskController) + return BubbleTaskView(taskView, shellExecutor) + } + } + + private inner class FakeBubbleExpandedViewManager : BubbleExpandedViewManager { + + override val overflowBubbles: List + get() = emptyList() + + override fun setOverflowListener(listener: BubbleData.Listener) {} + + override fun collapseStack() {} + + override fun updateWindowFlagsForBackpress(intercept: Boolean) {} + + override fun promoteBubbleFromOverflow(bubble: Bubble) {} + + override fun removeBubble(key: String, reason: Int) {} + + override fun dismissBubble(bubble: Bubble, reason: Int) {} + + override fun setAppBubbleTaskId(key: String, taskId: Int) {} + + override fun isStackExpanded(): Boolean = false + + override fun isShowingAsBubbleBar(): Boolean = false + + override fun hideCurrentInputMethod() {} + } +} diff --git a/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt new file mode 100644 index 0000000000..398fd554f0 --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.ComponentName +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.taskview.TaskView + +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleTaskViewTest { + + private lateinit var bubbleTaskView: BubbleTaskView + private val context = ApplicationProvider.getApplicationContext() + private lateinit var taskView: TaskView + + @Before + fun setUp() { + taskView = mock() + bubbleTaskView = BubbleTaskView(taskView, directExecutor()) + } + + @Test + fun onTaskCreated_updatesState() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(bubbleTaskView.taskId).isEqualTo(123) + assertThat(bubbleTaskView.componentName).isEqualTo(componentName) + assertThat(bubbleTaskView.isCreated).isTrue() + } + + @Test + fun onTaskCreated_callsDelegateListener() { + var actualTaskId = -1 + var actualComponentName: ComponentName? = null + val delegateListener = object : TaskView.Listener { + override fun onTaskCreated(taskId: Int, name: ComponentName) { + actualTaskId = taskId + actualComponentName = name + } + } + bubbleTaskView.delegateListener = delegateListener + + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(actualTaskId).isEqualTo(123) + assertThat(actualComponentName).isEqualTo(componentName) + } + + @Test + fun cleanup_invalidTaskId_doesNotRemoveTask() { + bubbleTaskView.cleanup() + verify(taskView, never()).removeTask() + } + + @Test + fun cleanup_validTaskId_removesTask() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + bubbleTaskView.cleanup() + verify(taskView).removeTask() + } +} diff --git a/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt new file mode 100644 index 0000000000..ace2c13105 --- /dev/null +++ b/wmshell/multivalentTestsForDeviceless/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.common.bubbles.BaseBubblePinController +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [BubbleExpandedViewPinController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleExpandedViewPinControllerTest { + + companion object { + @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + + const val BUBBLE_BAR_HEIGHT = 50 + } + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var positioner: BubblePositioner + private lateinit var container: FrameLayout + + private lateinit var controller: BubbleExpandedViewPinController + private lateinit var testListener: TestLocationChangeListener + + private val dropTargetView: View? + get() = container.findViewById(R.id.bubble_bar_drop_target) + + private val pointOnLeft = PointF(100f, 100f) + private val pointOnRight = PointF(1900f, 500f) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + container = FrameLayout(context) + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + positioner.bubbleBarTopOnScreen = + SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT + controller = BubbleExpandedViewPinController(context, container, positioner) + testListener = TestLocationChangeListener() + controller.setListener(testListener) + } + + @After + fun tearDown() { + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnRightSide() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(RIGHT) + } + + /** Dragging on same side should not show drop target or trigger location changes */ + @Test + fun drag_stayOnLeftSide() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragEnd() + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_rightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag crosses to the other side. Show drop target and trigger a location change. */ + @Test + fun drag_leftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_rightToLeftToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drop target does not initially show on the side that the drag starts. Check that it shows up + * after the dragging the view to other side and back to the initial side. + */ + @Test + fun drag_leftToRightToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + + getInstrumentation().runOnMainSync { + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) } + waitForAnimateOut() + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_rightToLeft_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target + * should not show and location change should not trigger. + */ + @Test + fun drag_leftToRight_inExclusionRect() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f + 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_rightToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss target and back to the same side should not cause the drop target to show. + */ + @Test + fun drag_leftToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_rightToDismissToLeft() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft()) + + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** Drag to dismiss target and other side should show drop target on the other side. */ + @Test + fun drag_leftToDismissToRight() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onStuckToDismissTarget() + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight()) + + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_rightToLeftToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + + assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** + * Drag to dismiss should trigger a location change to the initial location, if the current + * location is different. And hide the drop target. + */ + @Test + fun drag_leftToRightToDismiss() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() } + waitForAnimateOut() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder() + assertThat(testListener.locationReleases).isEmpty() + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_rightToLeftRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(LEFT) + assertThat(testListener.locationReleases).containsExactly(LEFT) + } + + /** Finishing drag should remove drop target and send location update. */ + @Test + fun drag_leftToRightRelease() { + getInstrumentation().runOnMainSync { + controller.onDragStart(initialLocationOnLeft = true) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + getInstrumentation().runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).containsExactly(RIGHT) + assertThat(testListener.locationReleases).containsExactly(RIGHT) + } + + private fun getExpectedDropTargetBoundsOnLeft(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + true /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun getExpectedDropTargetBoundsOnRight(): Rect = + Rect().also { + positioner.getBubbleBarExpandedViewBounds( + false /* onLeft */, + false /* isOverflowExpanded */, + it + ) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) + } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) + } + } + + private fun View.bounds(): Rect { + return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect -> + rect.offsetTo(x.toInt(), y.toInt()) + } + } + + internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener { + val locationChanges = mutableListOf() + val locationReleases = mutableListOf() + override fun onChange(location: BubbleBarLocation) { + locationChanges.add(location) + } + + override fun onRelease(location: BubbleBarLocation) { + locationReleases.add(location) + } + } +} diff --git a/wmshell/proto/wm_shell_trace.proto b/wmshell/proto/wm_shell_trace.proto new file mode 100644 index 0000000000..b9e72525f3 --- /dev/null +++ b/wmshell/proto/wm_shell_trace.proto @@ -0,0 +1,27 @@ +/* + * 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; + +message WmShellTraceProto { + + // Not used, just a test value + optional bool test_value = 1; +} diff --git a/wmshell/proto/wm_shell_transition_trace.proto b/wmshell/proto/wm_shell_transition_trace.proto new file mode 100644 index 0000000000..5c5815818b --- /dev/null +++ b/wmshell/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 merge_target = 6; + optional int64 abort_time_ns = 7; +} + +message HandlerMapping { + required int32 id = 1; + required string name = 2; +} diff --git a/wmshell/res/anim/forced_resizable_enter.xml b/wmshell/res/anim/forced_resizable_enter.xml new file mode 100644 index 0000000000..01b8fdbe44 --- /dev/null +++ b/wmshell/res/anim/forced_resizable_enter.xml @@ -0,0 +1,21 @@ + + + diff --git a/wmshell/res/anim/forced_resizable_exit.xml b/wmshell/res/anim/forced_resizable_exit.xml new file mode 100644 index 0000000000..6f316a75db --- /dev/null +++ b/wmshell/res/anim/forced_resizable_exit.xml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/wmshell/res/animator/tv_window_menu_action_button_animator.xml b/wmshell/res/animator/tv_window_menu_action_button_animator.xml new file mode 100644 index 0000000000..b2d59396d5 --- /dev/null +++ b/wmshell/res/animator/tv_window_menu_action_button_animator.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/bubble_drop_target_background_color.xml b/wmshell/res/color/bubble_drop_target_background_color.xml new file mode 100644 index 0000000000..ab1ab984fd --- /dev/null +++ b/wmshell/res/color/bubble_drop_target_background_color.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/color/compat_background_ripple.xml b/wmshell/res/color/compat_background_ripple.xml new file mode 100644 index 0000000000..329e5b9b31 --- /dev/null +++ b/wmshell/res/color/compat_background_ripple.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/color/decor_button_dark_color.xml b/wmshell/res/color/decor_button_dark_color.xml new file mode 100644 index 0000000000..bf325bd84c --- /dev/null +++ b/wmshell/res/color/decor_button_dark_color.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/wmshell/res/color/decor_button_light_color.xml b/wmshell/res/color/decor_button_light_color.xml new file mode 100644 index 0000000000..2e48bca778 --- /dev/null +++ b/wmshell/res/color/decor_button_light_color.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/decor_title_color.xml b/wmshell/res/color/decor_title_color.xml new file mode 100644 index 0000000000..1ecc13e4da --- /dev/null +++ b/wmshell/res/color/decor_title_color.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/wmshell/res/color/desktop_mode_caption_button_color_selector_dark.xml b/wmshell/res/color/desktop_mode_caption_button_color_selector_dark.xml new file mode 100644 index 0000000000..52a59671ba --- /dev/null +++ b/wmshell/res/color/desktop_mode_caption_button_color_selector_dark.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/desktop_mode_caption_button_color_selector_light.xml b/wmshell/res/color/desktop_mode_caption_button_color_selector_light.xml new file mode 100644 index 0000000000..6d8a51cd6f --- /dev/null +++ b/wmshell/res/color/desktop_mode_caption_button_color_selector_light.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/desktop_mode_maximize_menu_button_color_selector.xml b/wmshell/res/color/desktop_mode_maximize_menu_button_color_selector.xml new file mode 100644 index 0000000000..640d184e64 --- /dev/null +++ b/wmshell/res/color/desktop_mode_maximize_menu_button_color_selector.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/letterbox_education_dismiss_button_background_ripple.xml b/wmshell/res/color/letterbox_education_dismiss_button_background_ripple.xml new file mode 100644 index 0000000000..43cba1a37b --- /dev/null +++ b/wmshell/res/color/letterbox_education_dismiss_button_background_ripple.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/color/letterbox_restart_button_background_ripple.xml b/wmshell/res/color/letterbox_restart_button_background_ripple.xml new file mode 100644 index 0000000000..a3ca74fac4 --- /dev/null +++ b/wmshell/res/color/letterbox_restart_button_background_ripple.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/color/letterbox_restart_dismiss_button_background_ripple.xml b/wmshell/res/color/letterbox_restart_dismiss_button_background_ripple.xml new file mode 100644 index 0000000000..a3ca74fac4 --- /dev/null +++ b/wmshell/res/color/letterbox_restart_dismiss_button_background_ripple.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/color/one_handed_tutorial_background_color.xml b/wmshell/res/color/one_handed_tutorial_background_color.xml new file mode 100644 index 0000000000..4f56e0f023 --- /dev/null +++ b/wmshell/res/color/one_handed_tutorial_background_color.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/wmshell/res/color/taskbar_background_dark.xml b/wmshell/res/color/taskbar_background_dark.xml new file mode 100644 index 0000000000..f75d842c0d --- /dev/null +++ b/wmshell/res/color/taskbar_background_dark.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/tv_window_menu_close_icon.xml b/wmshell/res/color/tv_window_menu_close_icon.xml new file mode 100644 index 0000000000..67467bbc72 --- /dev/null +++ b/wmshell/res/color/tv_window_menu_close_icon.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/color/tv_window_menu_close_icon_bg.xml b/wmshell/res/color/tv_window_menu_close_icon_bg.xml new file mode 100644 index 0000000000..4182bfeefa --- /dev/null +++ b/wmshell/res/color/tv_window_menu_close_icon_bg.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/tv_window_menu_icon.xml b/wmshell/res/color/tv_window_menu_icon.xml new file mode 100644 index 0000000000..45205d2a71 --- /dev/null +++ b/wmshell/res/color/tv_window_menu_icon.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/tv_window_menu_icon_bg.xml b/wmshell/res/color/tv_window_menu_icon_bg.xml new file mode 100644 index 0000000000..1bd26e1d65 --- /dev/null +++ b/wmshell/res/color/tv_window_menu_icon_bg.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/color/unfold_background.xml b/wmshell/res/color/unfold_background.xml new file mode 100644 index 0000000000..e33eb12601 --- /dev/null +++ b/wmshell/res/color/unfold_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/wmshell/res/drawable-night/reachability_education_ic_left_hand.xml b/wmshell/res/drawable-night/reachability_education_ic_left_hand.xml new file mode 100644 index 0000000000..fbcf6d70f6 --- /dev/null +++ b/wmshell/res/drawable-night/reachability_education_ic_left_hand.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable-night/reachability_education_ic_right_hand.xml b/wmshell/res/drawable-night/reachability_education_ic_right_hand.xml new file mode 100644 index 0000000000..d36df4ba53 --- /dev/null +++ b/wmshell/res/drawable-night/reachability_education_ic_right_hand.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/bubble_drop_target_background.xml b/wmshell/res/drawable/bubble_drop_target_background.xml new file mode 100644 index 0000000000..b928a0b207 --- /dev/null +++ b/wmshell/res/drawable/bubble_drop_target_background.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/wmshell/res/drawable/bubble_ic_create_bubble.xml b/wmshell/res/drawable/bubble_ic_create_bubble.xml new file mode 100644 index 0000000000..920671a242 --- /dev/null +++ b/wmshell/res/drawable/bubble_ic_create_bubble.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/bubble_ic_empty_overflow_dark.xml b/wmshell/res/drawable/bubble_ic_empty_overflow_dark.xml new file mode 100644 index 0000000000..8f8f1b6646 --- /dev/null +++ b/wmshell/res/drawable/bubble_ic_empty_overflow_dark.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wmshell/res/drawable/bubble_ic_empty_overflow_light.xml b/wmshell/res/drawable/bubble_ic_empty_overflow_light.xml new file mode 100644 index 0000000000..5e02f67700 --- /dev/null +++ b/wmshell/res/drawable/bubble_ic_empty_overflow_light.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wmshell/res/drawable/bubble_ic_overflow_button.xml b/wmshell/res/drawable/bubble_ic_overflow_button.xml new file mode 100644 index 0000000000..3acebc12a8 --- /dev/null +++ b/wmshell/res/drawable/bubble_ic_overflow_button.xml @@ -0,0 +1,24 @@ + + + + diff --git a/wmshell/res/drawable/bubble_ic_stop_bubble.xml b/wmshell/res/drawable/bubble_ic_stop_bubble.xml new file mode 100644 index 0000000000..8609576ce7 --- /dev/null +++ b/wmshell/res/drawable/bubble_ic_stop_bubble.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/bubble_manage_btn_bg.xml b/wmshell/res/drawable/bubble_manage_btn_bg.xml new file mode 100644 index 0000000000..657720ee60 --- /dev/null +++ b/wmshell/res/drawable/bubble_manage_btn_bg.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/bubble_manage_menu_bg.xml b/wmshell/res/drawable/bubble_manage_menu_bg.xml new file mode 100644 index 0000000000..8fd2e68f64 --- /dev/null +++ b/wmshell/res/drawable/bubble_manage_menu_bg.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/wmshell/res/drawable/bubble_manage_menu_row.xml b/wmshell/res/drawable/bubble_manage_menu_row.xml new file mode 100644 index 0000000000..c61ac1c5f2 --- /dev/null +++ b/wmshell/res/drawable/bubble_manage_menu_row.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/bubble_manage_menu_section.xml b/wmshell/res/drawable/bubble_manage_menu_section.xml new file mode 100644 index 0000000000..d99d64d8da --- /dev/null +++ b/wmshell/res/drawable/bubble_manage_menu_section.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/bubble_stack_user_education_bg.xml b/wmshell/res/drawable/bubble_stack_user_education_bg.xml new file mode 100644 index 0000000000..dcc78213fd --- /dev/null +++ b/wmshell/res/drawable/bubble_stack_user_education_bg.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/bubble_stack_user_education_bg_rtl.xml b/wmshell/res/drawable/bubble_stack_user_education_bg_rtl.xml new file mode 100644 index 0000000000..70b63e62f6 --- /dev/null +++ b/wmshell/res/drawable/bubble_stack_user_education_bg_rtl.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/wmshell/res/drawable/camera_compat_dismiss_button.xml b/wmshell/res/drawable/camera_compat_dismiss_button.xml new file mode 100644 index 0000000000..1c8cb914af --- /dev/null +++ b/wmshell/res/drawable/camera_compat_dismiss_button.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/wmshell/res/drawable/camera_compat_dismiss_ripple.xml b/wmshell/res/drawable/camera_compat_dismiss_ripple.xml new file mode 100644 index 0000000000..c81013966c --- /dev/null +++ b/wmshell/res/drawable/camera_compat_dismiss_ripple.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/camera_compat_treatment_applied_button.xml b/wmshell/res/drawable/camera_compat_treatment_applied_button.xml new file mode 100644 index 0000000000..c796b5967f --- /dev/null +++ b/wmshell/res/drawable/camera_compat_treatment_applied_button.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/wmshell/res/drawable/camera_compat_treatment_applied_ripple.xml b/wmshell/res/drawable/camera_compat_treatment_applied_ripple.xml new file mode 100644 index 0000000000..3e9fe6dc3b --- /dev/null +++ b/wmshell/res/drawable/camera_compat_treatment_applied_ripple.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/wmshell/res/drawable/camera_compat_treatment_suggested_button.xml b/wmshell/res/drawable/camera_compat_treatment_suggested_button.xml new file mode 100644 index 0000000000..af505d1cb7 --- /dev/null +++ b/wmshell/res/drawable/camera_compat_treatment_suggested_button.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + diff --git a/wmshell/res/drawable/camera_compat_treatment_suggested_ripple.xml b/wmshell/res/drawable/camera_compat_treatment_suggested_ripple.xml new file mode 100644 index 0000000000..c0f1c89b0c --- /dev/null +++ b/wmshell/res/drawable/camera_compat_treatment_suggested_ripple.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/wmshell/res/drawable/caption_decor_title.xml b/wmshell/res/drawable/caption_decor_title.xml new file mode 100644 index 0000000000..6114ad6e27 --- /dev/null +++ b/wmshell/res/drawable/caption_decor_title.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/wmshell/res/drawable/circular_progress.xml b/wmshell/res/drawable/circular_progress.xml new file mode 100644 index 0000000000..0d64527b6c --- /dev/null +++ b/wmshell/res/drawable/circular_progress.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/compat_hint_bubble.xml b/wmshell/res/drawable/compat_hint_bubble.xml new file mode 100644 index 0000000000..26848b13a1 --- /dev/null +++ b/wmshell/res/drawable/compat_hint_bubble.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/compat_hint_point.xml b/wmshell/res/drawable/compat_hint_point.xml new file mode 100644 index 0000000000..0e0ca37aaf --- /dev/null +++ b/wmshell/res/drawable/compat_hint_point.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/decor_back_button_dark.xml b/wmshell/res/drawable/decor_back_button_dark.xml new file mode 100644 index 0000000000..5ecba380fb --- /dev/null +++ b/wmshell/res/drawable/decor_back_button_dark.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/decor_close_button_dark.xml b/wmshell/res/drawable/decor_close_button_dark.xml new file mode 100644 index 0000000000..cf9e632f69 --- /dev/null +++ b/wmshell/res/drawable/decor_close_button_dark.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/wmshell/res/drawable/decor_desktop_mode_maximize_button_dark.xml b/wmshell/res/drawable/decor_desktop_mode_maximize_button_dark.xml new file mode 100644 index 0000000000..e5fe1b5431 --- /dev/null +++ b/wmshell/res/drawable/decor_desktop_mode_maximize_button_dark.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/decor_handle_dark.xml b/wmshell/res/drawable/decor_handle_dark.xml new file mode 100644 index 0000000000..ce242751c1 --- /dev/null +++ b/wmshell/res/drawable/decor_handle_dark.xml @@ -0,0 +1,32 @@ + + + + + + + diff --git a/wmshell/res/drawable/decor_maximize_button_dark.xml b/wmshell/res/drawable/decor_maximize_button_dark.xml new file mode 100644 index 0000000000..ab4e29ac97 --- /dev/null +++ b/wmshell/res/drawable/decor_maximize_button_dark.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/wmshell/res/drawable/decor_minimize_button_dark.xml b/wmshell/res/drawable/decor_minimize_button_dark.xml new file mode 100644 index 0000000000..91edbf1a7b --- /dev/null +++ b/wmshell/res/drawable/decor_minimize_button_dark.xml @@ -0,0 +1,24 @@ + + + + diff --git a/wmshell/res/drawable/desktop_mode_decor_handle_menu_background.xml b/wmshell/res/drawable/desktop_mode_decor_handle_menu_background.xml new file mode 100644 index 0000000000..15837adc2c --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_decor_handle_menu_background.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/wmshell/res/drawable/desktop_mode_header_ic_close.xml b/wmshell/res/drawable/desktop_mode_header_ic_close.xml new file mode 100644 index 0000000000..ff49edb7a6 --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_header_ic_close.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/wmshell/res/drawable/desktop_mode_ic_handle_menu_close.xml b/wmshell/res/drawable/desktop_mode_ic_handle_menu_close.xml new file mode 100644 index 0000000000..b7521d4200 --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_ic_handle_menu_close.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/wmshell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml b/wmshell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml new file mode 100644 index 0000000000..e2b724b8ab --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_ic_handle_menu_desktop.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/wmshell/res/drawable/desktop_mode_ic_handle_menu_floating.xml b/wmshell/res/drawable/desktop_mode_ic_handle_menu_floating.xml new file mode 100644 index 0000000000..b0ea98e5f7 --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_ic_handle_menu_floating.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/wmshell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml b/wmshell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml new file mode 100644 index 0000000000..99e1d268c9 --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_ic_handle_menu_fullscreen.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/wmshell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml b/wmshell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml new file mode 100644 index 0000000000..79a91250bb --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_ic_handle_menu_screenshot.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/wmshell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml b/wmshell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml new file mode 100644 index 0000000000..853ab60e04 --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_ic_handle_menu_splitscreen.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/wmshell/res/drawable/desktop_mode_maximize_menu_background.xml b/wmshell/res/drawable/desktop_mode_maximize_menu_background.xml new file mode 100644 index 0000000000..9566f2f140 --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_maximize_menu_background.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/wmshell/res/drawable/desktop_mode_maximize_menu_button_background.xml b/wmshell/res/drawable/desktop_mode_maximize_menu_button_background.xml new file mode 100644 index 0000000000..ed51498dfe --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_maximize_menu_button_background.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/desktop_mode_maximize_menu_layout_background.xml b/wmshell/res/drawable/desktop_mode_maximize_menu_layout_background.xml new file mode 100644 index 0000000000..a30cfb74bf --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_maximize_menu_layout_background.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml b/wmshell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml new file mode 100644 index 0000000000..86da9feacc --- /dev/null +++ b/wmshell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/desktop_windowing_transition_background.xml b/wmshell/res/drawable/desktop_windowing_transition_background.xml new file mode 100644 index 0000000000..4e673e65e0 --- /dev/null +++ b/wmshell/res/drawable/desktop_windowing_transition_background.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/wmshell/res/drawable/dismiss_circle_background.xml b/wmshell/res/drawable/dismiss_circle_background.xml new file mode 100644 index 0000000000..f7fda362d7 --- /dev/null +++ b/wmshell/res/drawable/dismiss_circle_background.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/floating_dismiss_gradient.xml b/wmshell/res/drawable/floating_dismiss_gradient.xml new file mode 100644 index 0000000000..8b3057d584 --- /dev/null +++ b/wmshell/res/drawable/floating_dismiss_gradient.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/floating_dismiss_gradient_transition.xml b/wmshell/res/drawable/floating_dismiss_gradient_transition.xml new file mode 100644 index 0000000000..772d0a5ea8 --- /dev/null +++ b/wmshell/res/drawable/floating_dismiss_gradient_transition.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/handle_menu_background.xml b/wmshell/res/drawable/handle_menu_background.xml new file mode 100644 index 0000000000..e307f007e4 --- /dev/null +++ b/wmshell/res/drawable/handle_menu_background.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/wmshell/res/drawable/home_icon.xml b/wmshell/res/drawable/home_icon.xml new file mode 100644 index 0000000000..1669d0167e --- /dev/null +++ b/wmshell/res/drawable/home_icon.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + diff --git a/wmshell/res/drawable/ic_baseline_expand_more_24.xml b/wmshell/res/drawable/ic_baseline_expand_more_24.xml new file mode 100644 index 0000000000..e9df936c3f --- /dev/null +++ b/wmshell/res/drawable/ic_baseline_expand_more_24.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/wmshell/res/drawable/ic_bubbles_shortcut_widget.xml b/wmshell/res/drawable/ic_bubbles_shortcut_widget.xml new file mode 100644 index 0000000000..b208f2fea7 --- /dev/null +++ b/wmshell/res/drawable/ic_bubbles_shortcut_widget.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/wmshell/res/drawable/ic_bubbles_shortcut_widget_background.xml b/wmshell/res/drawable/ic_bubbles_shortcut_widget_background.xml new file mode 100644 index 0000000000..510221fb28 --- /dev/null +++ b/wmshell/res/drawable/ic_bubbles_shortcut_widget_background.xml @@ -0,0 +1,24 @@ + + + + diff --git a/wmshell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml b/wmshell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml new file mode 100644 index 0000000000..a41b6a961b --- /dev/null +++ b/wmshell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/ic_expand_less.xml b/wmshell/res/drawable/ic_expand_less.xml new file mode 100644 index 0000000000..f450846488 --- /dev/null +++ b/wmshell/res/drawable/ic_expand_less.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/ic_floating_landscape.xml b/wmshell/res/drawable/ic_floating_landscape.xml new file mode 100644 index 0000000000..8ef3307ee8 --- /dev/null +++ b/wmshell/res/drawable/ic_floating_landscape.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/wmshell/res/drawable/ic_remove_no_shadow.xml b/wmshell/res/drawable/ic_remove_no_shadow.xml new file mode 100644 index 0000000000..265c5019c7 --- /dev/null +++ b/wmshell/res/drawable/ic_remove_no_shadow.xml @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_education_dialog_background.xml b/wmshell/res/drawable/letterbox_education_dialog_background.xml new file mode 100644 index 0000000000..e7c89d1f9c --- /dev/null +++ b/wmshell/res/drawable/letterbox_education_dialog_background.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml b/wmshell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml new file mode 100644 index 0000000000..72ebef625f --- /dev/null +++ b/wmshell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wmshell/res/drawable/letterbox_education_ic_light_bulb.xml b/wmshell/res/drawable/letterbox_education_ic_light_bulb.xml new file mode 100644 index 0000000000..4a1e7485ed --- /dev/null +++ b/wmshell/res/drawable/letterbox_education_ic_light_bulb.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_education_ic_reposition.xml b/wmshell/res/drawable/letterbox_education_ic_reposition.xml new file mode 100644 index 0000000000..22a8f39ca6 --- /dev/null +++ b/wmshell/res/drawable/letterbox_education_ic_reposition.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/wmshell/res/drawable/letterbox_education_ic_split_screen.xml b/wmshell/res/drawable/letterbox_education_ic_split_screen.xml new file mode 100644 index 0000000000..15e65f716b --- /dev/null +++ b/wmshell/res/drawable/letterbox_education_ic_split_screen.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_button_background_ripple.xml b/wmshell/res/drawable/letterbox_restart_button_background_ripple.xml new file mode 100644 index 0000000000..1f12514877 --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_button_background_ripple.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_checkbox_button.xml b/wmshell/res/drawable/letterbox_restart_checkbox_button.xml new file mode 100644 index 0000000000..c247c6e4c8 --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_checkbox_button.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_checkbox_checked.xml b/wmshell/res/drawable/letterbox_restart_checkbox_checked.xml new file mode 100644 index 0000000000..4f97e2c7ea --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_checkbox_checked.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_checkbox_unchecked.xml b/wmshell/res/drawable/letterbox_restart_checkbox_unchecked.xml new file mode 100644 index 0000000000..bb14d1961e --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_checkbox_unchecked.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_dialog_background.xml b/wmshell/res/drawable/letterbox_restart_dialog_background.xml new file mode 100644 index 0000000000..e3c18a2db6 --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_dialog_background.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml b/wmshell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml new file mode 100644 index 0000000000..3aa0981e45 --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_header_ic_arrows.xml b/wmshell/res/drawable/letterbox_restart_header_ic_arrows.xml new file mode 100644 index 0000000000..5053971a17 --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_header_ic_arrows.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/letterbox_restart_ic_arrows.xml b/wmshell/res/drawable/letterbox_restart_ic_arrows.xml new file mode 100644 index 0000000000..b6e0172af1 --- /dev/null +++ b/wmshell/res/drawable/letterbox_restart_ic_arrows.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/one_handed_tutorial_icon.xml b/wmshell/res/drawable/one_handed_tutorial_icon.xml new file mode 100644 index 0000000000..b32f34ef7c --- /dev/null +++ b/wmshell/res/drawable/one_handed_tutorial_icon.xml @@ -0,0 +1,14 @@ + + + + diff --git a/wmshell/res/drawable/pip_custom_close_bg.xml b/wmshell/res/drawable/pip_custom_close_bg.xml new file mode 100644 index 0000000000..39c3fe6f61 --- /dev/null +++ b/wmshell/res/drawable/pip_custom_close_bg.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/wmshell/res/drawable/pip_expand.xml b/wmshell/res/drawable/pip_expand.xml new file mode 100644 index 0000000000..d36c4f72ec --- /dev/null +++ b/wmshell/res/drawable/pip_expand.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/wmshell/res/drawable/pip_ic_close_white.xml b/wmshell/res/drawable/pip_ic_close_white.xml new file mode 100644 index 0000000000..62285e62fb --- /dev/null +++ b/wmshell/res/drawable/pip_ic_close_white.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/pip_ic_collapse.xml b/wmshell/res/drawable/pip_ic_collapse.xml new file mode 100644 index 0000000000..63e2a4035c --- /dev/null +++ b/wmshell/res/drawable/pip_ic_collapse.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/pip_ic_expand.xml b/wmshell/res/drawable/pip_ic_expand.xml new file mode 100644 index 0000000000..758b92c4f4 --- /dev/null +++ b/wmshell/res/drawable/pip_ic_expand.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/pip_ic_fullscreen_white.xml b/wmshell/res/drawable/pip_ic_fullscreen_white.xml new file mode 100644 index 0000000000..56699dc04e --- /dev/null +++ b/wmshell/res/drawable/pip_ic_fullscreen_white.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/wmshell/res/drawable/pip_ic_move_white.xml b/wmshell/res/drawable/pip_ic_move_white.xml new file mode 100644 index 0000000000..37f4c87006 --- /dev/null +++ b/wmshell/res/drawable/pip_ic_move_white.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/pip_ic_pause_white.xml b/wmshell/res/drawable/pip_ic_pause_white.xml new file mode 100644 index 0000000000..0c469f7abb --- /dev/null +++ b/wmshell/res/drawable/pip_ic_pause_white.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/wmshell/res/drawable/pip_ic_play_arrow_white.xml b/wmshell/res/drawable/pip_ic_play_arrow_white.xml new file mode 100644 index 0000000000..8567afa323 --- /dev/null +++ b/wmshell/res/drawable/pip_ic_play_arrow_white.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/wmshell/res/drawable/pip_ic_settings.xml b/wmshell/res/drawable/pip_ic_settings.xml new file mode 100644 index 0000000000..73ec167f1f --- /dev/null +++ b/wmshell/res/drawable/pip_ic_settings.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/wmshell/res/drawable/pip_ic_skip_next_white.xml b/wmshell/res/drawable/pip_ic_skip_next_white.xml new file mode 100644 index 0000000000..6c5542131a --- /dev/null +++ b/wmshell/res/drawable/pip_ic_skip_next_white.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/wmshell/res/drawable/pip_ic_skip_previous_white.xml b/wmshell/res/drawable/pip_ic_skip_previous_white.xml new file mode 100644 index 0000000000..6b5382b662 --- /dev/null +++ b/wmshell/res/drawable/pip_ic_skip_previous_white.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/wmshell/res/drawable/pip_icon.xml b/wmshell/res/drawable/pip_icon.xml new file mode 100644 index 0000000000..b19d907d1f --- /dev/null +++ b/wmshell/res/drawable/pip_icon.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/pip_menu_background.xml b/wmshell/res/drawable/pip_menu_background.xml new file mode 100644 index 0000000000..29907a61b4 --- /dev/null +++ b/wmshell/res/drawable/pip_menu_background.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/pip_resize_handle.xml b/wmshell/res/drawable/pip_resize_handle.xml new file mode 100644 index 0000000000..4d1e080cf4 --- /dev/null +++ b/wmshell/res/drawable/pip_resize_handle.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/wmshell/res/drawable/reachability_education_ic_left_hand.xml b/wmshell/res/drawable/reachability_education_ic_left_hand.xml new file mode 100644 index 0000000000..05d243dfd5 --- /dev/null +++ b/wmshell/res/drawable/reachability_education_ic_left_hand.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/reachability_education_ic_right_hand.xml b/wmshell/res/drawable/reachability_education_ic_right_hand.xml new file mode 100644 index 0000000000..7bf243fe7f --- /dev/null +++ b/wmshell/res/drawable/reachability_education_ic_right_hand.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/rounded_button.xml b/wmshell/res/drawable/rounded_button.xml new file mode 100644 index 0000000000..17a0bab56a --- /dev/null +++ b/wmshell/res/drawable/rounded_button.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/size_compat_restart_button.xml b/wmshell/res/drawable/size_compat_restart_button.xml new file mode 100644 index 0000000000..b3f8e801ba --- /dev/null +++ b/wmshell/res/drawable/size_compat_restart_button.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/wmshell/res/drawable/size_compat_restart_button_ripple.xml b/wmshell/res/drawable/size_compat_restart_button_ripple.xml new file mode 100644 index 0000000000..6551edf6d0 --- /dev/null +++ b/wmshell/res/drawable/size_compat_restart_button_ripple.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/tv_pip_menu_background.xml b/wmshell/res/drawable/tv_pip_menu_background.xml new file mode 100644 index 0000000000..0c62792157 --- /dev/null +++ b/wmshell/res/drawable/tv_pip_menu_background.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/wmshell/res/drawable/tv_pip_menu_border.xml b/wmshell/res/drawable/tv_pip_menu_border.xml new file mode 100644 index 0000000000..7085a2c72c --- /dev/null +++ b/wmshell/res/drawable/tv_pip_menu_border.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/wmshell/res/drawable/tv_split_menu_ic_focus.xml b/wmshell/res/drawable/tv_split_menu_ic_focus.xml new file mode 100644 index 0000000000..a348b148af --- /dev/null +++ b/wmshell/res/drawable/tv_split_menu_ic_focus.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/wmshell/res/drawable/tv_split_menu_ic_swap.xml b/wmshell/res/drawable/tv_split_menu_ic_swap.xml new file mode 100644 index 0000000000..c5d54c5fa4 --- /dev/null +++ b/wmshell/res/drawable/tv_split_menu_ic_swap.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/tv_window_button_bg.xml b/wmshell/res/drawable/tv_window_button_bg.xml new file mode 100644 index 0000000000..4c28e519af --- /dev/null +++ b/wmshell/res/drawable/tv_window_button_bg.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/wmshell/res/drawable/user_aspect_ratio_settings_button.xml b/wmshell/res/drawable/user_aspect_ratio_settings_button.xml new file mode 100644 index 0000000000..6e4752c9d2 --- /dev/null +++ b/wmshell/res/drawable/user_aspect_ratio_settings_button.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/wmshell/res/drawable/user_aspect_ratio_settings_button_ripple.xml b/wmshell/res/drawable/user_aspect_ratio_settings_button_ripple.xml new file mode 100644 index 0000000000..141a1ce60b --- /dev/null +++ b/wmshell/res/drawable/user_aspect_ratio_settings_button_ripple.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/background_panel.xml b/wmshell/res/layout/background_panel.xml new file mode 100644 index 0000000000..c3569d80fa --- /dev/null +++ b/wmshell/res/layout/background_panel.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/badged_image_view.xml b/wmshell/res/layout/badged_image_view.xml new file mode 100644 index 0000000000..5f07121ec7 --- /dev/null +++ b/wmshell/res/layout/badged_image_view.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_bar_drop_target.xml b/wmshell/res/layout/bubble_bar_drop_target.xml new file mode 100644 index 0000000000..9d29f7da87 --- /dev/null +++ b/wmshell/res/layout/bubble_bar_drop_target.xml @@ -0,0 +1,22 @@ + + diff --git a/wmshell/res/layout/bubble_bar_expanded_view.xml b/wmshell/res/layout/bubble_bar_expanded_view.xml new file mode 100644 index 0000000000..34f03c2f22 --- /dev/null +++ b/wmshell/res/layout/bubble_bar_expanded_view.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/wmshell/res/layout/bubble_bar_manage_education.xml b/wmshell/res/layout/bubble_bar_manage_education.xml new file mode 100644 index 0000000000..a0a06f1b37 --- /dev/null +++ b/wmshell/res/layout/bubble_bar_manage_education.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_bar_menu_item.xml b/wmshell/res/layout/bubble_bar_menu_item.xml new file mode 100644 index 0000000000..ddcd5c60d9 --- /dev/null +++ b/wmshell/res/layout/bubble_bar_menu_item.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_bar_menu_view.xml b/wmshell/res/layout/bubble_bar_menu_view.xml new file mode 100644 index 0000000000..82e5aee41f --- /dev/null +++ b/wmshell/res/layout/bubble_bar_menu_view.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_bar_stack_education.xml b/wmshell/res/layout/bubble_bar_stack_education.xml new file mode 100644 index 0000000000..b489a5c1ac --- /dev/null +++ b/wmshell/res/layout/bubble_bar_stack_education.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_expanded_view.xml b/wmshell/res/layout/bubble_expanded_view.xml new file mode 100644 index 0000000000..880e0e46b4 --- /dev/null +++ b/wmshell/res/layout/bubble_expanded_view.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/wmshell/res/layout/bubble_flyout.xml b/wmshell/res/layout/bubble_flyout.xml new file mode 100644 index 0000000000..65a07a7186 --- /dev/null +++ b/wmshell/res/layout/bubble_flyout.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_manage_button.xml b/wmshell/res/layout/bubble_manage_button.xml new file mode 100644 index 0000000000..f88d63d796 --- /dev/null +++ b/wmshell/res/layout/bubble_manage_button.xml @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_manage_menu.xml b/wmshell/res/layout/bubble_manage_menu.xml new file mode 100644 index 0000000000..d8ae9c8c64 --- /dev/null +++ b/wmshell/res/layout/bubble_manage_menu.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wmshell/res/layout/bubble_overflow_button.xml b/wmshell/res/layout/bubble_overflow_button.xml new file mode 100644 index 0000000000..e392cdc26c --- /dev/null +++ b/wmshell/res/layout/bubble_overflow_button.xml @@ -0,0 +1,22 @@ + + + diff --git a/wmshell/res/layout/bubble_overflow_container.xml b/wmshell/res/layout/bubble_overflow_container.xml new file mode 100644 index 0000000000..df5985c605 --- /dev/null +++ b/wmshell/res/layout/bubble_overflow_container.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + diff --git a/wmshell/res/layout/bubble_overflow_view.xml b/wmshell/res/layout/bubble_overflow_view.xml new file mode 100644 index 0000000000..78de76a546 --- /dev/null +++ b/wmshell/res/layout/bubble_overflow_view.xml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/wmshell/res/layout/bubble_stack_user_education.xml b/wmshell/res/layout/bubble_stack_user_education.xml new file mode 100644 index 0000000000..ed00a87b0f --- /dev/null +++ b/wmshell/res/layout/bubble_stack_user_education.xml @@ -0,0 +1,54 @@ + + + + + + + diff --git a/wmshell/res/layout/bubble_view.xml b/wmshell/res/layout/bubble_view.xml new file mode 100644 index 0000000000..2b4b9e9042 --- /dev/null +++ b/wmshell/res/layout/bubble_view.xml @@ -0,0 +1,21 @@ + + + diff --git a/wmshell/res/layout/bubbles_manage_button_education.xml b/wmshell/res/layout/bubbles_manage_button_education.xml new file mode 100644 index 0000000000..4f6bdfd98d --- /dev/null +++ b/wmshell/res/layout/bubbles_manage_button_education.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + diff --git a/wmshell/res/layout/caption_window_decor.xml b/wmshell/res/layout/caption_window_decor.xml new file mode 100644 index 0000000000..f3d2198720 --- /dev/null +++ b/wmshell/res/layout/caption_window_decor.xml @@ -0,0 +1,56 @@ + + + +