From ea96983827ac7c5aa934ff1b547a496105b1fbb2 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Wed, 13 May 2026 22:48:39 +0100 Subject: [PATCH 01/51] [feature] adding nothing lockscreen clocks provider Cherry-picked from Axion frameworks/base 74f20a068afb. Conflicts resolved: KeyguardUpdateMonitor.java (kept DozeParameters + QuickLookClient), DefaultKeyguardBlueprint.kt / SplitShadeKeyguardBlueprint.kt (kept smartspace/weather/slice/axdynamicbar sections), QuickLookClient.kt (used Axion's version with clock data callbacks). --- packages/SystemUI/Android.bp | 1 + packages/SystemUI/AndroidManifest.xml | 5 + packages/SystemUI/customization/Android.bp | 30 +- .../assets/fonts/nsegments_vf.otf | Bin 0 -> 23156 bytes .../clocks/common/res/values/dimens.xml | 2 +- .../clocks/DefaultClockFaceLayout.kt | 8 +- .../res/drawable/audio_bars_playing.xml | 457 +++++++++ .../res/drawable/graphic_tick.xml | 11 + .../res/drawable/graphic_tick_light.xml | 13 + .../res/drawable/ic_music_note.xml | 24 + .../customization/res/drawable/intervar_0.xml | 10 + .../res/drawable/intervar_0_light.xml | 10 + .../customization/res/drawable/intervar_1.xml | 10 + .../res/drawable/intervar_1_light.xml | 10 + .../customization/res/drawable/intervar_2.xml | 10 + .../res/drawable/intervar_2_light.xml | 10 + .../customization/res/drawable/intervar_3.xml | 10 + .../res/drawable/intervar_3_light.xml | 10 + .../customization/res/drawable/intervar_4.xml | 10 + .../res/drawable/intervar_4_light.xml | 10 + .../customization/res/drawable/intervar_5.xml | 10 + .../res/drawable/intervar_5_light.xml | 10 + .../customization/res/drawable/intervar_6.xml | 10 + .../res/drawable/intervar_6_light.xml | 10 + .../customization/res/drawable/intervar_7.xml | 10 + .../res/drawable/intervar_7_light.xml | 10 + .../customization/res/drawable/intervar_8.xml | 10 + .../res/drawable/intervar_8_light.xml | 10 + .../customization/res/drawable/intervar_9.xml | 10 + .../res/drawable/intervar_9_light.xml | 10 + .../res/drawable/london_ug_0.xml | 46 + .../res/drawable/london_ug_0_16b.xml | 67 ++ .../res/drawable/london_ug_1.xml | 34 + .../res/drawable/london_ug_1_16b.xml | 47 + .../res/drawable/london_ug_2.xml | 43 + .../res/drawable/london_ug_2_16b.xml | 62 ++ .../res/drawable/london_ug_3.xml | 43 + .../res/drawable/london_ug_3_16b.xml | 62 ++ .../res/drawable/london_ug_4.xml | 37 + .../res/drawable/london_ug_4_16b.xml | 52 + .../res/drawable/london_ug_5.xml | 43 + .../res/drawable/london_ug_5_16b.xml | 62 ++ .../res/drawable/london_ug_6.xml | 46 + .../res/drawable/london_ug_6_16b.xml | 67 ++ .../res/drawable/london_ug_7.xml | 37 + .../res/drawable/london_ug_7_16b.xml | 52 + .../res/drawable/london_ug_8.xml | 55 ++ .../res/drawable/london_ug_8_16b.xml | 82 ++ .../res/drawable/london_ug_9.xml | 46 + .../res/drawable/london_ug_9_16b.xml | 67 ++ .../customization/res/drawable/ndot_0.xml | 13 + .../customization/res/drawable/ndot_0_16b.xml | 10 + .../res/drawable/ndot_0_light.xml | 16 + .../res/drawable/ndot_0_light_16b.xml | 13 + .../customization/res/drawable/ndot_1.xml | 13 + .../customization/res/drawable/ndot_1_16b.xml | 10 + .../res/drawable/ndot_1_light.xml | 16 + .../res/drawable/ndot_1_light_16b.xml | 13 + .../customization/res/drawable/ndot_2.xml | 13 + .../customization/res/drawable/ndot_2_16b.xml | 10 + .../res/drawable/ndot_2_light.xml | 16 + .../res/drawable/ndot_2_light_16b.xml | 13 + .../customization/res/drawable/ndot_3.xml | 13 + .../customization/res/drawable/ndot_3_16b.xml | 10 + .../res/drawable/ndot_3_light.xml | 16 + .../res/drawable/ndot_3_light_16b.xml | 13 + .../customization/res/drawable/ndot_4.xml | 13 + .../customization/res/drawable/ndot_4_16b.xml | 10 + .../res/drawable/ndot_4_light.xml | 16 + .../res/drawable/ndot_4_light_16b.xml | 13 + .../customization/res/drawable/ndot_5.xml | 13 + .../customization/res/drawable/ndot_5_16b.xml | 10 + .../res/drawable/ndot_5_light.xml | 16 + .../res/drawable/ndot_5_light_16b.xml | 13 + .../customization/res/drawable/ndot_6.xml | 13 + .../customization/res/drawable/ndot_6_16b.xml | 10 + .../res/drawable/ndot_6_light.xml | 16 + .../res/drawable/ndot_6_light_16b.xml | 13 + .../customization/res/drawable/ndot_7.xml | 13 + .../customization/res/drawable/ndot_7_16b.xml | 10 + .../res/drawable/ndot_7_light.xml | 16 + .../res/drawable/ndot_7_light_16b.xml | 13 + .../customization/res/drawable/ndot_8.xml | 13 + .../customization/res/drawable/ndot_8_16b.xml | 10 + .../res/drawable/ndot_8_light.xml | 16 + .../res/drawable/ndot_8_light_16b.xml | 13 + .../customization/res/drawable/ndot_9.xml | 13 + .../customization/res/drawable/ndot_9_16b.xml | 10 + .../res/drawable/ndot_9_light.xml | 16 + .../res/drawable/ndot_9_light_16b.xml | 13 + .../customization/res/drawable/ntype_0.xml | 13 + .../res/drawable/ntype_0_16b.xml | 12 + .../customization/res/drawable/ntype_1.xml | 13 + .../res/drawable/ntype_1_16b.xml | 12 + .../customization/res/drawable/ntype_2.xml | 13 + .../res/drawable/ntype_2_16b.xml | 12 + .../customization/res/drawable/ntype_3.xml | 13 + .../res/drawable/ntype_3_16b.xml | 12 + .../customization/res/drawable/ntype_4.xml | 13 + .../res/drawable/ntype_4_16b.xml | 12 + .../customization/res/drawable/ntype_5.xml | 13 + .../res/drawable/ntype_5_16b.xml | 12 + .../customization/res/drawable/ntype_6.xml | 13 + .../res/drawable/ntype_6_16b.xml | 12 + .../customization/res/drawable/ntype_7.xml | 13 + .../res/drawable/ntype_7_16b.xml | 12 + .../customization/res/drawable/ntype_8.xml | 13 + .../res/drawable/ntype_8_16b.xml | 12 + .../customization/res/drawable/ntype_9.xml | 13 + .../res/drawable/ntype_9_16b.xml | 12 + .../drawable/old_quick_look_alarm_icon.xml | 85 ++ .../customization/res/drawable/polyline_0.xml | 14 + .../customization/res/drawable/polyline_1.xml | 14 + .../customization/res/drawable/polyline_2.xml | 14 + .../customization/res/drawable/polyline_3.xml | 14 + .../customization/res/drawable/polyline_4.xml | 14 + .../customization/res/drawable/polyline_5.xml | 14 + .../customization/res/drawable/polyline_6.xml | 14 + .../customization/res/drawable/polyline_7.xml | 14 + .../customization/res/drawable/polyline_8.xml | 14 + .../customization/res/drawable/polyline_9.xml | 14 + .../res/drawable/space_age_0.xml | 12 + .../res/drawable/space_age_1.xml | 12 + .../res/drawable/space_age_2.xml | 12 + .../res/drawable/space_age_3.xml | 12 + .../res/drawable/space_age_4.xml | 12 + .../res/drawable/space_age_5.xml | 12 + .../res/drawable/space_age_6.xml | 12 + .../res/drawable/space_age_7.xml | 12 + .../res/drawable/space_age_8.xml | 12 + .../res/drawable/space_age_9.xml | 12 + .../res/layout/clock_axion_age.xml | 4 + .../res/layout/clock_axion_age_large.xml | 5 + .../res/layout/clock_bitmap_compose.xml | 4 + .../res/layout/clock_bitmap_compose_large.xml | 5 + .../res/layout/clock_cyberpunk.xml | 4 + .../res/layout/clock_cyberpunk_large.xml | 5 + .../res/layout/clock_general.xml | 4 + .../res/layout/clock_general_large.xml | 5 + .../res/layout/clock_old_quick_look.xml | 4 + .../res/layout/clock_old_quick_look_large.xml | 5 + .../customization/res/values/axion_dimens.xml | 79 ++ .../customization/res/values/colors.xml | 22 + .../SystemUI/customization/res/values/ids.xml | 23 + .../customization/res/values/strings.xml | 21 +- .../shared/clocks/AxClockAnimations.kt | 76 ++ .../shared/clocks/AxClockController.kt | 102 ++ .../systemui/shared/clocks/AxClockEvents.kt | 94 ++ .../shared/clocks/AxClockFaceController.kt | 99 ++ .../systemui/shared/clocks/AxClockProvider.kt | 87 ++ .../systemui/shared/clocks/AxClockType.kt | 92 ++ .../systemui/shared/clocks/ClockConfigs.kt | 126 +++ .../shared/clocks/ClockSettingsRepository.kt | 180 ++++ .../shared/clocks/ClockViewExtensions.kt | 59 ++ .../shared/clocks/DepthWallpaperProvider.kt | 173 ++++ .../systemui/shared/clocks/WeatherUtils.kt | 58 ++ .../shared/clocks/view/AxClockAnimator.kt | 57 ++ .../shared/clocks/view/AxClockConstants.kt | 57 ++ .../clocks/view/AxClockDisplayResolver.kt | 128 +++ .../shared/clocks/view/AxClockHost.kt | 132 +++ .../shared/clocks/view/AxClockInteractor.kt | 116 +++ .../shared/clocks/view/AxClockModels.kt | 38 + .../shared/clocks/view/AxClockState.kt | 52 + .../shared/clocks/view/AxClockView.kt | 379 ++++++++ .../shared/clocks/view/AxClockViewModel.kt | 65 ++ .../shared/clocks/view/AxionAgeClockView.kt | 318 ++++++ .../view/BitmapDigitComposeClockView.kt | 910 ++++++++++++++++++ .../shared/clocks/view/ClockCanvasHelpers.kt | 272 ++++++ .../clocks/view/ClockDepthController.kt | 235 +++++ .../shared/clocks/view/ClockFaceStyle.kt | 223 +++++ .../shared/clocks/view/CyberpunkClockView.kt | 470 +++++++++ .../shared/clocks/view/DateDisplay.kt | 50 + .../shared/clocks/view/GeneralClockView.kt | 404 ++++++++ .../clocks/view/OldQuickLookClockView.kt | 255 +++++ .../shared/clocks/view/QuickLookController.kt | 81 ++ .../shared/clocks/view/QuickLookDateArea.kt | 139 +++ .../shared/clocks/view/QuickLookUtils.kt | 36 + .../keyguard/ui/clocks/CalendarSimpleData.kt | 61 ++ .../keyguard/ui/clocks/ClockAnimations.kt | 16 +- .../plugins/keyguard/ui/clocks/ClockData.kt | 26 + .../plugins/keyguard/ui/clocks/ClockEvents.kt | 8 + .../keyguard/ui/clocks/ClockFaceEvents.kt | 9 + .../keyguard/ui/clocks/ClockWeatherData.kt | 65 ++ .../keyguard/ui/clocks/SmartspaceData.kt | 75 ++ .../android/keyguard/ClockEventController.kt | 160 ++- .../keyguard/KeyguardUpdateMonitor.java | 56 +- .../KeyguardUpdateMonitorCallback.java | 8 + .../keyguard/dagger/ClockRegistryModule.java | 4 +- .../ui/binder/KeyguardClockViewBinder.kt | 21 +- .../blueprints/SplitShadeKeyguardBlueprint.kt | 4 +- .../sections/AodNotificationIconsSection.kt | 48 +- .../ui/view/layout/sections/ClockSection.kt | 62 +- .../systemui/quicklook/QuickLookClient.kt | 126 +++ .../systemui/statusbar/NotificationShelf.java | 4 + .../phone/NotificationIconContainer.java | 59 +- .../server/am/ContentProviderHelper.java | 1 + 196 files changed, 9411 insertions(+), 71 deletions(-) create mode 100644 packages/SystemUI/customization/assets/fonts/nsegments_vf.otf create mode 100644 packages/SystemUI/customization/res/drawable/audio_bars_playing.xml create mode 100644 packages/SystemUI/customization/res/drawable/graphic_tick.xml create mode 100644 packages/SystemUI/customization/res/drawable/graphic_tick_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ic_music_note.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_0.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_0_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_1.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_1_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_2.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_2_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_3.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_3_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_4.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_4_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_5.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_5_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_6.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_6_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_7.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_7_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_8.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_8_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_9.xml create mode 100644 packages/SystemUI/customization/res/drawable/intervar_9_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_0.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_0_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_1.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_1_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_2.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_2_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_3.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_3_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_4.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_4_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_5.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_5_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_6.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_6_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_7.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_7_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_8.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_8_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_9.xml create mode 100644 packages/SystemUI/customization/res/drawable/london_ug_9_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_0.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_0_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_0_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_0_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_1.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_1_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_1_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_1_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_2.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_2_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_2_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_2_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_3.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_3_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_3_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_3_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_4.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_4_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_4_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_4_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_5.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_5_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_5_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_5_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_6.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_6_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_6_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_6_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_7.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_7_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_7_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_7_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_8.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_8_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_8_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_8_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_9.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_9_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_9_light.xml create mode 100644 packages/SystemUI/customization/res/drawable/ndot_9_light_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_0.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_0_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_1.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_1_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_2.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_2_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_3.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_3_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_4.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_4_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_5.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_5_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_6.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_6_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_7.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_7_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_8.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_8_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_9.xml create mode 100644 packages/SystemUI/customization/res/drawable/ntype_9_16b.xml create mode 100644 packages/SystemUI/customization/res/drawable/old_quick_look_alarm_icon.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_0.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_1.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_2.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_3.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_4.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_5.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_6.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_7.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_8.xml create mode 100644 packages/SystemUI/customization/res/drawable/polyline_9.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_0.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_1.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_2.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_3.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_4.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_5.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_6.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_7.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_8.xml create mode 100644 packages/SystemUI/customization/res/drawable/space_age_9.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_axion_age.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_axion_age_large.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_bitmap_compose.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_cyberpunk.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_cyberpunk_large.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_general.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_general_large.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_old_quick_look.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml create mode 100644 packages/SystemUI/customization/res/values/axion_dimens.xml create mode 100644 packages/SystemUI/customization/res/values/colors.xml create mode 100644 packages/SystemUI/customization/res/values/ids.xml create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockController.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockEvents.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockViewExtensions.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DepthWallpaperProvider.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockDisplayResolver.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockModels.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockState.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockViewModel.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockFaceStyle.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DateDisplay.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookController.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/CalendarSimpleData.kt create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockData.kt create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockWeatherData.kt create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/SmartspaceData.kt diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index ed774d95d2d5c..66faf49f5290b 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -518,6 +518,7 @@ android_library { "//frameworks/libs/systemui:compilelib", "com.android.systemui.pods-api-aosp-handheld", "ax_platform_hooks", + "axquicklook-client", "SystemUI-res", "WifiTrackerLib", "WindowManager-Shell", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 21947c58c595d..322eaaf48b158 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -438,6 +438,11 @@ + + + + + >G<&j_5;6?K zE=id22J?g3EIW+W(wQ=1%|9J$OLu^ft{v+Bn8n%b?Ch*=XZw%e8$XWbkU!YRY`L)$9WRqXrX zUmwHs*Kl3Fv!QaAc+t5R&)>v#+h2XVvg!Ndb-hA-6PKc;wz{(F%bQ-#Li#`PyS)}S zGFLdL%PGM1lG=urJ=fL;QJ3=rT(4-T+_Ou}-GZcZ2M(de%7*F(yZ`i8LIfTVBKViP z9%*h7H;Mr~`X!#P5e|+*toz{3jQ$5dw`NpiWV}x|{^#iLmhkzN?|wm4ptR1w4{)6! z0(2LDcrS3y3yCUc1kWp+PF0e8N=MKH?Z8n4oqu$07MUU#c!pXbz9!Gkx5RSXkNuPF zQTGb*Cr*k&ai^>+a88`ZBhKTz%`Ga#`Z+45)VX& zxU)#~s&qhHC0EOE2bsM6UY-}yV0&J$al zmsEP5$Z^iA^g>Y(2+B12o)=i4(vHXpEK%tI?%%4?8RF`|ohls^S%FO|or!WPR2sAm z)T%VbE3ikUv&8%WaRKz#!59XnnFYd~bDC-R_2M)eUup>mRAFqTk(nTAC{P zs=TtUslKkUx}vtOQQqZ(_<*tBCaOh^Xuz>iw1{R=h}pbM+$6U0{oNShO3@_l$5j)a z)`>@uuMjzI6~+9&TC7e|$}(|_*o53Qm@O^Hu^f4qi=9CtS%PnEa_y{y93H*tTbc$iz=B6gw2 zcZ%Ea{}}(@%4Id?MyUbmLb0A(UuDjBi#@1;+7f+Mj;lJ{tH-esZ&oNO*m%axC4`6xcUJog^EpN%tycZVm>$mgT&*Nhe zj=_`SBtN*}oicI5JB470^ef|g;2XrPiTgnk1dzmk)(1H+U%h(Ky@jW43a+?u)xoV-y05#zE<$p(cjb@X~ucDujWAhHGQo|z<> zujh@d{u%N<|C;N1BKUB-CvtO2I=VgY!r%VV6Z1|Aq($a!|KN$j;daUsSUKQ{(DSd+ zyJL;N!%I1959TinFKIr4!V2nIJM-uCc67Jlxe%Enz1!kH2zbI+9U!F8W|&hy3} zZl}jYP#j$M&>@iY%vzU^eCW=L0|P}Q5L{Ho{u;h7Rt>W%++wI{N64^US=T7AOv z{`vheYE<6fI!_c|n}aTkIXfymFZ@gijY#PEpL^nS%`1BHv$*&YRXf+`KKZ0>G1|>Z z)pqZ~_x7S8=!X0B)AV2fZ;xPf>Cl~j-BMRhBAPhV;5D*XuA-Fk4|4uN%0HM`=5x<+ zr;+az)9Dnwa;iK3hBmjO!=tzaj#x)tx5Fg67mv z5Cznh9ipR5(x}KmGaaV~7)rGNG#|S27q#}uPSY&*IlbJ8pzF5wAK&45?+x7&5mhUjC)I%#v)hJnWgys$CjJI<^Z8~5WhFv*#n`szicc@sd0?p<*g{*5S|JuYd zpMKh((VC!zS(Ix-(I6Ur{%jp!MkeiNpcdA z70DAT=D8NPIO02jVXzmRPi7Lyn?5(OM_aW*wvEUo`@pM#4|19CkeV69v{l{K%Zt`E zO&wa-#3-3oa=LHBldo;G{sj%_JCj45a(J@P}tl&mZDlK6yM-hlIrO^b0VNJKRUI zBsx!c@?f$Md9$D04~aQ?3iHHBVVWoB{{>pqTLpV*m_}B=3Ngx3+l%!49ydk@i4}%H zl0J+oQ6c3yGEFMjx2|Cn~6;bYIJKO%k6>H+;0e zuN4#^tRv5@DR#KNxet>$XCEeqVrY3bpx~-=sqTrWN_~n)NQ7<#n?(SIA9{p;2wS4gQaM1QUk;uRWAR`tr#Um=h zzRA!R!9EG_4KsKra)N}6M`w!}a6N$28qBTL#S!M#B97Kp}EWGl!jD*XcAJ{dcct<%vG^vlcJ2;D%60NvzCbTg?iKiJjX zyRQd}GjmbTesw0ok!A_PvBs%6!6y=9QcKaoj3#LAV7X=rUZgLFMDA&ui4ak=hz2sk z(kW%8L+>h)J^)3THCS=&a33v7x2>n6hk+t=BSwoR_r;|h?OiCP@3)@FKPgPwm!5_843;okUv{E=3HC^Vc?qe7<41?aCHobXcRB!&_^&nxHL!?U3n~RAWYMf{NgJ(U^=`@f?-JiJQ zxKdb?4s=ojiB!V$K+lkYB%;|PiAtXXodSF8n+Ot$(P9b9I(=e^t%Ej+Z5@^-khVeD zX*-YoE|VK`7j_~oz)ooa1|tWiIZR4q?;!hA5}N>wC15}>HQ_KL0?e0%F@+I28+k)v zGcqs^TYOSM7K0gJTqEW}U6RJa_DG>vQ_Ke@q%3=DInj-D_$V0uXhCsZcn;5b8h zXiVtqzXuB+%90gdEE<7iA=|@+Go7A?OF2LjuM{BHdT{$dJyfO)T;kz4Ts2mQzVQ%^0-(j0Z_sPE(KoOQ$aY^sIv^+&64m6M0D4Kl;HGCWTCCw0~%^ zMGUajO$M2ap~#DnwK=@B9b;n*OEgXlnE+}eWRiv`3k0<%LMClnW4Rn_GdT^uG=Hh+NMxfGk{{LJRDi1B(XnSGv%DN2Y;sCwn$5>K$P}z(Wb=tEC6+uXYN4s4&1o3v z&Q9XxRu)l~fZDobD=vu&jN)TC>Y)h+8#Hty|>zIswoy1yY2hR4ZvRjN~Mr||E z^#hb-If-RphmmKnXBak*KoBYajKf}v1+aXqpF}6IUJ&3-{$hXN=dc$pHG=UP{36Nl z8u?L7>`zj^OM+vWgto!|rhFx|EuO3$%WEYvo=wN{>|#7Kl!B+h+)~dTdXv@|PfZn` z20vU(7|&r?1Km0e81K-NF9O6}{pwGqvBYfiC+X};sg@vwF+Y;pmYHZ%jXIEQkI&?B zLp%Qs{=XBRy+%xWi0vO{jYwF8^)+!)5{*%>guD^}Q``^;2}R^Z82bY&ALUEwZXzp* zck%BZMV}7V!R;Z9CY*O8b*Kqe=b;<`<0lX8Mgbq(Q|BKMgv&pTx;)@cbS)FnC)MTM zagc6lhfiI;XFefUib0yLF7L;$oaPWnutADVjCDW9vO%c1KeI_^*dWy6IA|)_AUFfq zAfRgu8zf}w)9n1m8#Wnd{6iR_qwlDZr=Lwst6yaKZvHc9O756`eCUHiOj z0wijM5?&ZpII0!?afMBWmsn=_Gy=-v7#F!O4?{elBxfv#3$w6<)Uh3kY?O%$bzlPy z8P8zb(9X#zq7KslC`mQ7g2|z|P7+LhNkA#Bl>}$TRAA23fKn>qCcm@-O$ShxTpmE# zih%oO3d+-CrhW2==}3$X_o+3-#|DF#>2@{A-2sVyK(#wSt1?DS!2oG|noXuSiJEe? z4Ki$Okcf4-SQMVh203bMkk67maTHMN3dX0>_9(IEhcbSH88mE_hC$o-^jyVW3!h5= z94Imh=0GM{NgRn!a}7RiB76#BLZ@cZCgRg<%FFl^!1be{Y!GdK*JHSO?m`z}3YDv> zKt&h;F3`LK)Bi_QJ_R}?;$O*!oFT4*@$xQu9d*PDeR z&V6b1mc&zD8b-)&v+MRB*9J2zsThM9skuH`eJatuns58|cEPNL-WldZJ!OOpGZ@Kb z9d@lPiexsk5i$iLM~wv9SE6$(I!+fMhw~@E2VSkNx{@wP2>WmPtNGeV=JF+>YzR_m0PZZuKK|s2r)UOP6l&0NO>L{#uPuP6Q zsG}wt+IaD$I-2$7cR)X|;uv)l9-+_F(FAqW|1LgOM?dYm_*fmC#x6b?jPuzpK8pB6 zJk(6>;*(Zl|GW6elKj~&zF32EJcb2aVSxS@zlE>(QoHz!RbOJ*$#(H+2TgyhK8^V8 zN%iR`z@820vsrb5gGTDp-Rg|t97HpYTb=|zH?tS3V^wu!+@kEF$f3>}lsGDQrGOlj1HhJa#d1h94Jx15_UA7~oz16DQ>ifVMtH&!uqy(OZ`D3VmJC+AhXaX?kU#sLIq8Rr<@VS@t7pV~N}9Asz@8!J#pJYz*z zTXb|!#4HD!7|~fG)n^Wf`jCks-DhkZc>OXP2M7gQp|AkS$-<2|6n38tx~Zp=y3c4s z@WrM3ZQ}q1JxpmFxWxA1xCk7%eOQOWLjJ@UR!BU}nqehX)yz*QEbSgO!qX->O|F02SXc6vnfAWTvaV>{JA-oEv|~< z=OLp_{5;7PHGal-HG^2&#LsWwy`Eu4mcFZO?Ym-s z6mOF{Al)ca#Lu&z3&rQ2!O-Uwg@LrexB>#`1u}o z<`Y5Y8}c5M87DJveII;?Fw;zmYK8Axe6;_Uc&Y%-8= z^nUVHneY&`C6~0onTV7CPV^ftu7bnJC~&zqdie{cf1IF`|KKo*Zv6&_$&cft5*(({ z?JURH$GQza0lJO%2Lg;huFzbi&kI~S=>Ap6mBTlYPRV=mWYGQa_!bRsRV2#}aAzG_ zJ&ipY)+>Iav18p$zG7#rSNtxY$l+8zo^!=^hdu}CI9boqS0yX2e(Z%1J_w1gw&F*B z@&l3b+mdu2PBK2~gUn0EslPl)-?XGZ`rKp|*HD16>C3)>$MA(!E;@)WUD7?_$Tr}n zGw_u9UZ&$@w8?8aa|!v}x&nM5H;c-kd-UyMeohxC%jw1sNq(2ENASa!FX@}dP7r5& eaoNel^Fo|MvRr2l&Q-XkFFgMm=WP5??*9Sq^jTT} literal 0 HcmV?d00001 diff --git a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml index b3cc9e1dc0b58..8b1edd8506eaa 100644 --- a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml +++ b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml @@ -45,4 +45,4 @@ 104dp 0dp 74dp - \ No newline at end of file + diff --git a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt index 8309689808799..514d42b103895 100644 --- a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt +++ b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt @@ -204,17 +204,13 @@ open class DefaultClockFaceLayout(val view: View) : ClockFaceLayout { } constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, WRAP_CONTENT) - constrainHeight( - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - res.getDimensionPixelSize(clocksR.dimen.small_clock_height), - ) + constrainHeight(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, WRAP_CONTENT) connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, START, PARENT_ID, START, - res.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - clockPreviewConfig.statusViewMarginHorizontal, + 0, ) val smallClockTopMargin = clockPreviewConfig.getSmallClockTopPadding() diff --git a/packages/SystemUI/customization/res/drawable/audio_bars_playing.xml b/packages/SystemUI/customization/res/drawable/audio_bars_playing.xml new file mode 100644 index 0000000000000..6a6706a73f642 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/audio_bars_playing.xml @@ -0,0 +1,457 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/graphic_tick.xml b/packages/SystemUI/customization/res/drawable/graphic_tick.xml new file mode 100644 index 0000000000000..5ffcd3e681b09 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/graphic_tick.xml @@ -0,0 +1,11 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/graphic_tick_light.xml b/packages/SystemUI/customization/res/drawable/graphic_tick_light.xml new file mode 100644 index 0000000000000..d594ad8a059cf --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/graphic_tick_light.xml @@ -0,0 +1,13 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ic_music_note.xml b/packages/SystemUI/customization/res/drawable/ic_music_note.xml new file mode 100644 index 0000000000000..30959a870a02b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ic_music_note.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_0.xml b/packages/SystemUI/customization/res/drawable/intervar_0.xml new file mode 100644 index 0000000000000..d0aa59874c0a3 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_0.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_0_light.xml b/packages/SystemUI/customization/res/drawable/intervar_0_light.xml new file mode 100644 index 0000000000000..b7176401530c1 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_0_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_1.xml b/packages/SystemUI/customization/res/drawable/intervar_1.xml new file mode 100644 index 0000000000000..6d13a64057a51 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_1.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_1_light.xml b/packages/SystemUI/customization/res/drawable/intervar_1_light.xml new file mode 100644 index 0000000000000..3b0c563a600e1 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_1_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_2.xml b/packages/SystemUI/customization/res/drawable/intervar_2.xml new file mode 100644 index 0000000000000..84fc90b4cd6c0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_2.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_2_light.xml b/packages/SystemUI/customization/res/drawable/intervar_2_light.xml new file mode 100644 index 0000000000000..1481fde632200 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_2_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_3.xml b/packages/SystemUI/customization/res/drawable/intervar_3.xml new file mode 100644 index 0000000000000..d4fe235dc32d9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_3.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_3_light.xml b/packages/SystemUI/customization/res/drawable/intervar_3_light.xml new file mode 100644 index 0000000000000..1045820ef287e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_3_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_4.xml b/packages/SystemUI/customization/res/drawable/intervar_4.xml new file mode 100644 index 0000000000000..d51ff6d2ca7fe --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_4.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_4_light.xml b/packages/SystemUI/customization/res/drawable/intervar_4_light.xml new file mode 100644 index 0000000000000..e556c47f6774e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_4_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_5.xml b/packages/SystemUI/customization/res/drawable/intervar_5.xml new file mode 100644 index 0000000000000..7d6ff10723e63 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_5.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_5_light.xml b/packages/SystemUI/customization/res/drawable/intervar_5_light.xml new file mode 100644 index 0000000000000..d12a4fcb9cee0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_5_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_6.xml b/packages/SystemUI/customization/res/drawable/intervar_6.xml new file mode 100644 index 0000000000000..cbd55a8980b5d --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_6.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_6_light.xml b/packages/SystemUI/customization/res/drawable/intervar_6_light.xml new file mode 100644 index 0000000000000..f5af044a0633e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_6_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_7.xml b/packages/SystemUI/customization/res/drawable/intervar_7.xml new file mode 100644 index 0000000000000..03b4316b4d8b5 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_7.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_7_light.xml b/packages/SystemUI/customization/res/drawable/intervar_7_light.xml new file mode 100644 index 0000000000000..d2c96bb7451d7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_7_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_8.xml b/packages/SystemUI/customization/res/drawable/intervar_8.xml new file mode 100644 index 0000000000000..5ee72cb869430 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_8.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_8_light.xml b/packages/SystemUI/customization/res/drawable/intervar_8_light.xml new file mode 100644 index 0000000000000..052c2828a1779 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_8_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_9.xml b/packages/SystemUI/customization/res/drawable/intervar_9.xml new file mode 100644 index 0000000000000..5a78fcf88a0e8 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_9.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_9_light.xml b/packages/SystemUI/customization/res/drawable/intervar_9_light.xml new file mode 100644 index 0000000000000..711acdb30f314 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_9_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_0.xml b/packages/SystemUI/customization/res/drawable/london_ug_0.xml new file mode 100644 index 0000000000000..6facb6e2c2680 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_0.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_0_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_0_16b.xml new file mode 100644 index 0000000000000..032ba2ae03775 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_0_16b.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_1.xml b/packages/SystemUI/customization/res/drawable/london_ug_1.xml new file mode 100644 index 0000000000000..6d649bf5e89aa --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_1.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_1_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_1_16b.xml new file mode 100644 index 0000000000000..82c9081fc260c --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_1_16b.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_2.xml b/packages/SystemUI/customization/res/drawable/london_ug_2.xml new file mode 100644 index 0000000000000..edf3fe92e95d8 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_2.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_2_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_2_16b.xml new file mode 100644 index 0000000000000..908761bdc2437 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_2_16b.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_3.xml b/packages/SystemUI/customization/res/drawable/london_ug_3.xml new file mode 100644 index 0000000000000..6af349f1cdaed --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_3.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_3_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_3_16b.xml new file mode 100644 index 0000000000000..515d82aaeb30e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_3_16b.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_4.xml b/packages/SystemUI/customization/res/drawable/london_ug_4.xml new file mode 100644 index 0000000000000..bc0b998828cd5 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_4.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_4_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_4_16b.xml new file mode 100644 index 0000000000000..4e6f4281aa815 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_4_16b.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_5.xml b/packages/SystemUI/customization/res/drawable/london_ug_5.xml new file mode 100644 index 0000000000000..aa2351927d9a3 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_5.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_5_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_5_16b.xml new file mode 100644 index 0000000000000..81f1b4d8bd105 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_5_16b.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_6.xml b/packages/SystemUI/customization/res/drawable/london_ug_6.xml new file mode 100644 index 0000000000000..8281c49c7188f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_6.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_6_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_6_16b.xml new file mode 100644 index 0000000000000..71bb38a80cdbd --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_6_16b.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_7.xml b/packages/SystemUI/customization/res/drawable/london_ug_7.xml new file mode 100644 index 0000000000000..ee3717159c762 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_7.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_7_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_7_16b.xml new file mode 100644 index 0000000000000..17622a99e0a43 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_7_16b.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_8.xml b/packages/SystemUI/customization/res/drawable/london_ug_8.xml new file mode 100644 index 0000000000000..6e4ed6db92622 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_8.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_8_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_8_16b.xml new file mode 100644 index 0000000000000..b8f9602e91566 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_8_16b.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_9.xml b/packages/SystemUI/customization/res/drawable/london_ug_9.xml new file mode 100644 index 0000000000000..07423814301a1 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_9.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_9_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_9_16b.xml new file mode 100644 index 0000000000000..9e83076f5ce49 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_9_16b.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0.xml b/packages/SystemUI/customization/res/drawable/ndot_0.xml new file mode 100644 index 0000000000000..72d0a564caf24 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_0_16b.xml new file mode 100644 index 0000000000000..865920ccba04b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0_light.xml b/packages/SystemUI/customization/res/drawable/ndot_0_light.xml new file mode 100644 index 0000000000000..d4e0bdead70a0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_0_light_16b.xml new file mode 100644 index 0000000000000..622a7ce03ebe7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1.xml b/packages/SystemUI/customization/res/drawable/ndot_1.xml new file mode 100644 index 0000000000000..764082fb9f338 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_1_16b.xml new file mode 100644 index 0000000000000..2a1e9429ac27f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1_light.xml b/packages/SystemUI/customization/res/drawable/ndot_1_light.xml new file mode 100644 index 0000000000000..d9fa490a77e50 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_1_light_16b.xml new file mode 100644 index 0000000000000..70363d723ec6c --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2.xml b/packages/SystemUI/customization/res/drawable/ndot_2.xml new file mode 100644 index 0000000000000..5cdb0b2eb06e4 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_2_16b.xml new file mode 100644 index 0000000000000..83949670df7c2 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2_light.xml b/packages/SystemUI/customization/res/drawable/ndot_2_light.xml new file mode 100644 index 0000000000000..151475be13679 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_2_light_16b.xml new file mode 100644 index 0000000000000..7e8d48601ccbd --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3.xml b/packages/SystemUI/customization/res/drawable/ndot_3.xml new file mode 100644 index 0000000000000..732b45960cabf --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_3_16b.xml new file mode 100644 index 0000000000000..be236f2ab9b02 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3_light.xml b/packages/SystemUI/customization/res/drawable/ndot_3_light.xml new file mode 100644 index 0000000000000..8575d64843ce9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_3_light_16b.xml new file mode 100644 index 0000000000000..2723f89076129 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4.xml b/packages/SystemUI/customization/res/drawable/ndot_4.xml new file mode 100644 index 0000000000000..aa2e4be002241 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_4_16b.xml new file mode 100644 index 0000000000000..513a5539a5b49 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4_light.xml b/packages/SystemUI/customization/res/drawable/ndot_4_light.xml new file mode 100644 index 0000000000000..253e7bf8761ee --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_4_light_16b.xml new file mode 100644 index 0000000000000..095a74f8d3d5a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5.xml b/packages/SystemUI/customization/res/drawable/ndot_5.xml new file mode 100644 index 0000000000000..7598807bd06b9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_5_16b.xml new file mode 100644 index 0000000000000..cdd6cd86536c7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5_light.xml b/packages/SystemUI/customization/res/drawable/ndot_5_light.xml new file mode 100644 index 0000000000000..35dd1f5ba08fd --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_5_light_16b.xml new file mode 100644 index 0000000000000..7ec960a66b3bf --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6.xml b/packages/SystemUI/customization/res/drawable/ndot_6.xml new file mode 100644 index 0000000000000..582aea74f5557 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_6_16b.xml new file mode 100644 index 0000000000000..80fa4f0c3cdca --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6_light.xml b/packages/SystemUI/customization/res/drawable/ndot_6_light.xml new file mode 100644 index 0000000000000..b55968bbd6341 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_6_light_16b.xml new file mode 100644 index 0000000000000..1f2c5b54c10ea --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7.xml b/packages/SystemUI/customization/res/drawable/ndot_7.xml new file mode 100644 index 0000000000000..768636a456b6e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_7_16b.xml new file mode 100644 index 0000000000000..45b05f4c819a4 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7_light.xml b/packages/SystemUI/customization/res/drawable/ndot_7_light.xml new file mode 100644 index 0000000000000..2a390a8c80666 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_7_light_16b.xml new file mode 100644 index 0000000000000..04c2dcf651f2f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8.xml b/packages/SystemUI/customization/res/drawable/ndot_8.xml new file mode 100644 index 0000000000000..212de49990a60 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_8_16b.xml new file mode 100644 index 0000000000000..1b4f6fe023e00 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8_light.xml b/packages/SystemUI/customization/res/drawable/ndot_8_light.xml new file mode 100644 index 0000000000000..c50de834cbe9a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_8_light_16b.xml new file mode 100644 index 0000000000000..1480f7f7da972 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9.xml b/packages/SystemUI/customization/res/drawable/ndot_9.xml new file mode 100644 index 0000000000000..81a088ded2f6f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_9_16b.xml new file mode 100644 index 0000000000000..dfc2b8cd21572 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9_light.xml b/packages/SystemUI/customization/res/drawable/ndot_9_light.xml new file mode 100644 index 0000000000000..b782fbc52baba --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_9_light_16b.xml new file mode 100644 index 0000000000000..f91d7a5ae9789 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_0.xml b/packages/SystemUI/customization/res/drawable/ntype_0.xml new file mode 100644 index 0000000000000..88eb6d4730433 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_0.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_0_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_0_16b.xml new file mode 100644 index 0000000000000..a6f2f8068b29e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_0_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_1.xml b/packages/SystemUI/customization/res/drawable/ntype_1.xml new file mode 100644 index 0000000000000..96ad18e8771ec --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_1.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_1_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_1_16b.xml new file mode 100644 index 0000000000000..fb308f76e8b56 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_1_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_2.xml b/packages/SystemUI/customization/res/drawable/ntype_2.xml new file mode 100644 index 0000000000000..d3677b3e826f9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_2.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_2_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_2_16b.xml new file mode 100644 index 0000000000000..063f76bd8e50f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_2_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_3.xml b/packages/SystemUI/customization/res/drawable/ntype_3.xml new file mode 100644 index 0000000000000..3d7e9d2cea685 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_3.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_3_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_3_16b.xml new file mode 100644 index 0000000000000..28cefa14dd4c0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_3_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_4.xml b/packages/SystemUI/customization/res/drawable/ntype_4.xml new file mode 100644 index 0000000000000..d8b43a487fe27 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_4.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_4_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_4_16b.xml new file mode 100644 index 0000000000000..edadcd2dfb6a9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_4_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_5.xml b/packages/SystemUI/customization/res/drawable/ntype_5.xml new file mode 100644 index 0000000000000..d6cc5d50fb59b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_5.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_5_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_5_16b.xml new file mode 100644 index 0000000000000..641c4ba8f212b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_5_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_6.xml b/packages/SystemUI/customization/res/drawable/ntype_6.xml new file mode 100644 index 0000000000000..facdf86bbed21 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_6_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_6_16b.xml new file mode 100644 index 0000000000000..12c290db1cdeb --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_6_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_7.xml b/packages/SystemUI/customization/res/drawable/ntype_7.xml new file mode 100644 index 0000000000000..532d782b97908 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_7.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_7_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_7_16b.xml new file mode 100644 index 0000000000000..601aff8a43502 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_7_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_8.xml b/packages/SystemUI/customization/res/drawable/ntype_8.xml new file mode 100644 index 0000000000000..1bbfb0cdf01eb --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_8.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_8_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_8_16b.xml new file mode 100644 index 0000000000000..36f2f192b61bc --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_8_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_9.xml b/packages/SystemUI/customization/res/drawable/ntype_9.xml new file mode 100644 index 0000000000000..44d543e71b358 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_9.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_9_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_9_16b.xml new file mode 100644 index 0000000000000..acba31f5ebb78 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_9_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/old_quick_look_alarm_icon.xml b/packages/SystemUI/customization/res/drawable/old_quick_look_alarm_icon.xml new file mode 100644 index 0000000000000..99052f9e96913 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/old_quick_look_alarm_icon.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/polyline_0.xml b/packages/SystemUI/customization/res/drawable/polyline_0.xml new file mode 100644 index 0000000000000..201e69cdfe929 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_0.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_1.xml b/packages/SystemUI/customization/res/drawable/polyline_1.xml new file mode 100644 index 0000000000000..b36776a04e2df --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_1.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_2.xml b/packages/SystemUI/customization/res/drawable/polyline_2.xml new file mode 100644 index 0000000000000..61a5a819f8f4f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_2.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_3.xml b/packages/SystemUI/customization/res/drawable/polyline_3.xml new file mode 100644 index 0000000000000..01c23ced13315 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_3.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_4.xml b/packages/SystemUI/customization/res/drawable/polyline_4.xml new file mode 100644 index 0000000000000..aa1ff3885279f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_4.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_5.xml b/packages/SystemUI/customization/res/drawable/polyline_5.xml new file mode 100644 index 0000000000000..3db0eb6beacd0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_5.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_6.xml b/packages/SystemUI/customization/res/drawable/polyline_6.xml new file mode 100644 index 0000000000000..f3d9a4ecfee7e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_6.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_7.xml b/packages/SystemUI/customization/res/drawable/polyline_7.xml new file mode 100644 index 0000000000000..566d26f4a9b9b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_7.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_8.xml b/packages/SystemUI/customization/res/drawable/polyline_8.xml new file mode 100644 index 0000000000000..0b7e1d51c9c7a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_8.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_9.xml b/packages/SystemUI/customization/res/drawable/polyline_9.xml new file mode 100644 index 0000000000000..5c61d0e21fd00 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_9.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_0.xml b/packages/SystemUI/customization/res/drawable/space_age_0.xml new file mode 100644 index 0000000000000..69fce63020d48 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_0.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_1.xml b/packages/SystemUI/customization/res/drawable/space_age_1.xml new file mode 100644 index 0000000000000..eb29b842e9d44 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_1.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_2.xml b/packages/SystemUI/customization/res/drawable/space_age_2.xml new file mode 100644 index 0000000000000..09da8433ad6a7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_2.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_3.xml b/packages/SystemUI/customization/res/drawable/space_age_3.xml new file mode 100644 index 0000000000000..cac8e6666620e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_3.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_4.xml b/packages/SystemUI/customization/res/drawable/space_age_4.xml new file mode 100644 index 0000000000000..4f411c36289c8 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_4.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_5.xml b/packages/SystemUI/customization/res/drawable/space_age_5.xml new file mode 100644 index 0000000000000..fbb17db752e7a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_5.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_6.xml b/packages/SystemUI/customization/res/drawable/space_age_6.xml new file mode 100644 index 0000000000000..0e22fcb7d624b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_6.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_7.xml b/packages/SystemUI/customization/res/drawable/space_age_7.xml new file mode 100644 index 0000000000000..0a9d4ac93c42b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_7.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_8.xml b/packages/SystemUI/customization/res/drawable/space_age_8.xml new file mode 100644 index 0000000000000..766c67473e87e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_8.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_9.xml b/packages/SystemUI/customization/res/drawable/space_age_9.xml new file mode 100644 index 0000000000000..fe7c90c03160c --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_9.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/layout/clock_axion_age.xml b/packages/SystemUI/customization/res/layout/clock_axion_age.xml new file mode 100644 index 0000000000000..6273236ebe3b9 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_axion_age.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_axion_age_large.xml b/packages/SystemUI/customization/res/layout/clock_axion_age_large.xml new file mode 100644 index 0000000000000..fbaffe4577cae --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_axion_age_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_bitmap_compose.xml b/packages/SystemUI/customization/res/layout/clock_bitmap_compose.xml new file mode 100644 index 0000000000000..5efc483c0bd95 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_bitmap_compose.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml b/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml new file mode 100644 index 0000000000000..fd57f89bc3f02 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_cyberpunk.xml b/packages/SystemUI/customization/res/layout/clock_cyberpunk.xml new file mode 100644 index 0000000000000..4c13aa50fd926 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_cyberpunk.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_cyberpunk_large.xml b/packages/SystemUI/customization/res/layout/clock_cyberpunk_large.xml new file mode 100644 index 0000000000000..130e2ca9e57eb --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_cyberpunk_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_general.xml b/packages/SystemUI/customization/res/layout/clock_general.xml new file mode 100644 index 0000000000000..e439231450a29 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_general.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_general_large.xml b/packages/SystemUI/customization/res/layout/clock_general_large.xml new file mode 100644 index 0000000000000..01be414c47682 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_general_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_old_quick_look.xml b/packages/SystemUI/customization/res/layout/clock_old_quick_look.xml new file mode 100644 index 0000000000000..e689238f5eebd --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_old_quick_look.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml b/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml new file mode 100644 index 0000000000000..53c57b24e3279 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/values/axion_dimens.xml b/packages/SystemUI/customization/res/values/axion_dimens.xml new file mode 100644 index 0000000000000..b48b06e030854 --- /dev/null +++ b/packages/SystemUI/customization/res/values/axion_dimens.xml @@ -0,0 +1,79 @@ + + + + + 28dp + 6dp + 16dp + 7dp + 8dp + 28dp + 18sp + 24dp + 8dp + 20dp + 4dp + 4dp + 4dp + 166dp + 44dp + 28dp + 14dp + 8dp + 12dp + 30dp + 24dp + 16dp + 134dp + 20dp + 28dp + 18dp + 2dp + 3dp + 6dp + 32dp + 10dp + 8dp + 6dp + 4dp + 10dp + 24dp + 28dp + 4dp + 80dp + 28dp + 22dp + 16dp + 8dp + 8dp + 16dp + 26dp + 4dp + 2dp + 0.3334dp + 5dp + 16dp + 6dp + 8dp + 36dp + + + 400dp + 4dp + 16dp + diff --git a/packages/SystemUI/customization/res/values/colors.xml b/packages/SystemUI/customization/res/values/colors.xml new file mode 100644 index 0000000000000..fe85c2605e46d --- /dev/null +++ b/packages/SystemUI/customization/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + #d71921 + #ffc700 + diff --git a/packages/SystemUI/customization/res/values/ids.xml b/packages/SystemUI/customization/res/values/ids.xml new file mode 100644 index 0000000000000..c40956ef42f10 --- /dev/null +++ b/packages/SystemUI/customization/res/values/ids.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/values/strings.xml b/packages/SystemUI/customization/res/values/strings.xml index 897c842b6d4b0..4a8419af3c7e0 100644 --- a/packages/SystemUI/customization/res/values/strings.xml +++ b/packages/SystemUI/customization/res/values/strings.xml @@ -22,4 +22,23 @@ Digital default - \ No newline at end of file + + GENERAL + GRAPHIC + LONDON_UG + NDOT + DEFAULT + OLD_QUICKLOOK + QUICKLOOK + POLYLINE + SPACE_AGE + CYBERPUNK + AXION_AGE + SEGMENTS + ? + __° + : ) + Calendar events and reminders + Schedule + EEEMMMd + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt new file mode 100644 index 0000000000000..2fa1dc1ec8e02 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.AxClockView +import com.android.systemui.shared.clocks.view.animateAppear +import com.android.systemui.shared.clocks.view.animateCharge + +class AxClockAnimations( + private val clockView: AxClockView, + dozeFraction: Float, + foldFraction: Float +) : ClockAnimations { + + internal val dozeState = ClockAnimationState(dozeFraction) + private val foldState = ClockAnimationState(foldFraction) + + override fun enter() { + if (!dozeState.isActive) { + clockView.animateAppear() + } + } + + override fun doze(fraction: Float) { + dozeState.update(fraction) + } + + override fun fold(fraction: Float) { + foldState.update(fraction) + } + + override fun charge() { + clockView.animateCharge() + } + + override fun onPickerCarouselSwiping(swipingFraction: Float) { + clockView.translationY = 0.5f * clockView.bottom * (1 - swipingFraction) + } + + override fun onFidgetTap(x: Float, y: Float) { + clockView.animateFidgetTap(x, y) + } + + override fun onPositionAnimated(args: ClockPositionAnimationArgs) {} + + override fun onFontAxesChanged(style: ClockAxisStyle) {} + + class ClockAnimationState(var fraction: Float) { + var isActive: Boolean = fraction > 0.5f + + fun update(newFraction: Float): Pair { + if (newFraction == fraction) { + return Pair(isActive, false) + } + val wasActive = isActive + val hasJumped = + (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f) + isActive = newFraction > fraction + fraction = newFraction + return Pair(wasActive != isActive, hasJumped) + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockController.kt new file mode 100644 index 0000000000000..d9a70a6f59bf6 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.ViewGroup +import android.util.Log +import android.widget.FrameLayout +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.BitmapDigitComposeClockView +import com.android.systemui.shared.clocks.view.AxClockView +import java.io.PrintWriter + +class AxClockController @JvmOverloads constructor( + context: Context, + clockType: AxClockType, + layoutInflater: LayoutInflater, + clockMessageBuffers: ClockMessageBuffers? = null +) : ClockController { + private val TAG = "AxClockController" + + override val smallClock: AxClockFaceController + override val largeClock: AxClockFaceController + override val eventListeners = ClockEventListeners() + override val events: ClockEvents + override val config: ClockConfig + + init { + val container = FrameLayout(context) + + val tickRate = if (clockType.bitmapFaceStyle?.needsPerSecondTick == true) { + ClockTickRate.PER_SECOND + } else { + ClockTickRate.PER_MINUTE + } + + val smallClockView = layoutInflater.inflate(clockType.viewId, container, false) as AxClockView + clockType.bitmapFaceStyle?.let { style -> + (smallClockView as? BitmapDigitComposeClockView)?.faceStyle = style + } + + smallClock = AxClockFaceController( + context, + smallClockView, + "lockscreen_clock_view", + tickRate, + clockMessageBuffers?.smallClockMessageBuffer, + isLargeClock = false + ) + + val largeClockView = layoutInflater.inflate(clockType.largeViewId, container, false) as AxClockView + largeClockView.isLargeClock = true + clockType.bitmapFaceStyle?.let { style -> + (largeClockView as? BitmapDigitComposeClockView)?.faceStyle = style + } + + largeClock = AxClockFaceController( + context, + largeClockView, + "lockscreen_clock_view_large", + tickRate, + clockMessageBuffers?.largeClockMessageBuffer, + isLargeClock = true + ) + + events = AxClockEvents(smallClockView, largeClockView) + + val clockName = context.getString(clockType.clockId) + + config = ClockConfig(clockName, "", "", false, false) + + Log.d(TAG, "init") + } + + override fun dump(pw: PrintWriter) {} + + override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) { + smallClock.animations = AxClockAnimations(smallClock.view, dozeFraction, foldFraction) + largeClock.animations = AxClockAnimations(largeClock.view, dozeFraction, foldFraction) + + events.onUiModeChanged(isDarkTheme) + + smallClock.events.onTimeTick() + largeClock.events.onTimeTick() + + Log.d(TAG, "initialize") + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockEvents.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockEvents.kt new file mode 100644 index 0000000000000..77c594aa99e7c --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockEvents.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.content.res.Resources +import android.icu.util.TimeZone +import android.text.format.DateFormat +import android.view.View +import com.android.systemui.plugins.keyguard.data.model.* +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.AxClockView +import java.util.* + +class AxClockEvents( + private val smallClockView: AxClockView, + private val largeClockView: AxClockView? = null +) : ClockEvents { + + private val views: List + get() = listOfNotNull(smallClockView, largeClockView) + + override var isReactiveTouchInteractionEnabled: Boolean = false + + override fun onAlarmDataChanged(data: AlarmData) { + views.forEach { it.onAlarmDataChanged(data) } + } + + override fun onWeatherDataChanged(data: WeatherData) {} + + override fun onZenDataChanged(data: ZenData) { + requireNotNull(data) { "ZenData cannot be null" } + } + + override fun onLocaleChanged(locale: Locale) { + requireNotNull(locale) { "Locale cannot be null" } + val is24Hour = DateFormat.is24HourFormat(smallClockView.context) + views.forEach { it.refreshFormat(is24Hour, locale) } + } + + override fun onTimeFormatChanged(formatKind: TimeFormatKind) { + val is24Hour = DateFormat.is24HourFormat(smallClockView.context) + views.forEach { it.refreshFormat(is24Hour) } + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + requireNotNull(timeZone) { "TimeZone cannot be null" } + views.forEach { it.onTimeZoneChanged(timeZone) } + + val is24Hour = DateFormat.is24HourFormat(smallClockView.context) + views.forEach { it.refreshFormat(is24Hour) } + } + + override fun onUiModeChanged(isDarkTheme: Boolean) { + views.forEach { it.onThemeChanged(isDarkTheme) } + } + + override fun onDateChanged() { + views.forEach { it.onDateChanged() } + } + + override fun onClockDataChanged(data: ClockData) { + views.forEach { it.onClockDataChanged(data) } + } + + override fun onMetadataChanged(track: String, artist: String, packageName: String) { + views.forEach { it.onMetadataChanged(track, artist, packageName) } + } + + override fun onPlaybackStateChanged(playing: Boolean) { + views.forEach { it.onPlaybackStateChanged(playing) } + } + + override fun onNowPlayingUpdate(nowPlayingText: String) { + views.forEach { it.onNowPlayingUpdate(nowPlayingText) } + } + override fun onClockLayoutChanged(isCentered: Boolean, isLargeClockVisible: Boolean) { + views.forEach { it.onClockLayoutChanged(isCentered, isLargeClockVisible) } + } + override fun onDepthEffectVisibilityChanged(visible: Boolean) { + views.forEach { it.onDepthEffectVisibilityChanged(visible) } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt new file mode 100644 index 0000000000000..026a3d76ddf09 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.view.View +import com.android.systemui.customization.clocks.DefaultClockFaceLayout +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.AxClockView + +class AxClockFaceController( + context: Context, + override val view: AxClockView, + clockViewId: String, + clockTickRate: ClockTickRate = ClockTickRate.PER_MINUTE, + messageBuffer: MessageBuffer? = null, + isLargeClock: Boolean = false +) : ClockFaceController { + + override var animations: ClockAnimations = AxClockAnimations(view, 0.0f, 0.0f) + internal set + + override val config: ClockFaceConfig = + ClockFaceConfig(clockTickRate, false, false, false) + + override var theme = ThemeConfig(true, null) + + override val events = + object : ClockFaceEvents { + override fun onTimeTick() = view.refreshTime() + + override fun onThemeChanged(theme: ThemeConfig) { + } + + override fun onTargetRegionChanged(targetRegion: Rect?) { + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.onFontSettingChanged() + } + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + + override fun onDozeChanged(dozing: Boolean) { + view.onDozeChanged(dozing) + } + + override fun onDozeAmountChanged(linear: Float, eased: Float) { + view.onDozeAmountChanged(linear, eased) + } + + override fun onPulsingChanged(pulsing: Boolean) { + view.onPulsingChanged(pulsing) + } + + override fun onScreenOff(screenOff: Boolean) { + view.onScreenOff(screenOff) + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + view.onRegionDarknessChanged(isRegionDark) + } + + override fun onStartedWakingUp() { + view.onStartedWakingUp() + } + + override fun onStartedGoingToSleep(isKeyguardVisible: Boolean) { + view.onStartedGoingToSleep(isKeyguardVisible) + } + + override fun onWakefulnessStateChanged( + isWakingUp: Boolean, + tapPosition: Point? + ) { + view.onWakefulnessStateChanged(isWakingUp, tapPosition) + } + } + + override val layout = + DefaultClockFaceLayout(view).apply { + views[0].id = if (isLargeClock) ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE else ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt new file mode 100644 index 0000000000000..29fc78501a30a --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.drawable.Drawable +import android.os.Vibrator +import android.util.Log +import android.view.LayoutInflater +import com.android.systemui.customization.R +import com.android.systemui.plugins.keyguard.ui.clocks.* + +const val useAxClocks = true + +class AxClockProvider( + private val layoutInflater: LayoutInflater, + private val resources: Resources, + private val isClockReactiveVariantsEnabled: Boolean = false, + private val vibrator: Vibrator?, +) : ClockProvider { + + private val tag = "AxClockProvider" + private var messageBuffers: ClockMessageBuffers? = null + + init { + Log.d(tag, "Initialized AxClockProvider") + } + + override fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig { + return ClockPickerConfig( + settings.clockId ?: "NTYPE", + resources.getString(R.string.clock_id_general), + resources.getString(R.string.clock_id_general), + resources.getDrawable(R.drawable.clock_default_thumbnail, null), + isReactiveToTone = true, + axes = emptyList(), + presetConfig = null, + ) + } + + override fun createClock(ctx: Context, settings: ClockSettings): AxClockController { + val clockId = settings.clockId + val resolvedType = AxClockType.values().firstOrNull { + clockId == resources.getString(it.clockId) + } ?: AxClockType.NTYPE + + return AxClockController(ctx, resolvedType, layoutInflater, messageBuffers) + } + + override fun getClocks(): List { + val availableTypes = buildList { + add(AxClockType.NTYPE) + add(AxClockType.NDOT) + add(AxClockType.GRAPHIC) + add(AxClockType.GENERAL) + add(AxClockType.LONDON_UG) + add(AxClockType.OLD_QUICKLOOK) + add(AxClockType.SPACE_AGE) + add(AxClockType.POLYLINE) + add(AxClockType.CYBERPUNK) + add(AxClockType.AXION_AGE) + add(AxClockType.SEGMENTS) + } + + return availableTypes.map { type -> + val id = resources.getString(type.clockId) + ClockMetadata(id) + } + } + + override fun initialize(buffers: ClockMessageBuffers?) { + messageBuffers = buffers + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt new file mode 100644 index 0000000000000..c562bd0d308a2 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.view.ClockFaceStyle + +enum class AxClockType( + val clockId: Int, + val viewId: Int, + val largeViewId: Int = viewId, + val bitmapFaceStyle: ClockFaceStyle? = null +) { + GENERAL( + clockId = R.string.clock_id_general, + viewId = R.layout.clock_general, + largeViewId = R.layout.clock_general_large + ), + GRAPHIC( + clockId = R.string.clock_id_graphic, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.GRAPHIC + ), + LONDON_UG( + clockId = R.string.clock_id_london_ug, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.LONDON_UG + ), + NDOT( + clockId = R.string.clock_id_ndot, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.NDOT + ), + NTYPE( + clockId = R.string.clock_id_ntype, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.NTYPE + ), + OLD_QUICKLOOK( + clockId = R.string.clock_id_old_quick_look, + viewId = R.layout.clock_old_quick_look, + largeViewId = R.layout.clock_old_quick_look_large + ), + SPACE_AGE( + clockId = R.string.clock_id_space_age, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.SPACE_AGE + ), + POLYLINE( + clockId = R.string.clock_id_polyline, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.POLYLINE + ), + CYBERPUNK( + clockId = R.string.clock_id_cyberpunk, + viewId = R.layout.clock_cyberpunk, + largeViewId = R.layout.clock_cyberpunk_large + ), + AXION_AGE( + clockId = R.string.clock_id_axion_age, + viewId = R.layout.clock_axion_age, + largeViewId = R.layout.clock_axion_age_large + ), + SEGMENTS( + clockId = R.string.clock_id_segments, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.SEGMENTS + ); + + companion object { + val entries: List = values().toList() + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt new file mode 100644 index 0000000000000..055158813a956 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.content.Context +import com.android.systemui.customization.R + +object ClockConfigs { + + const val ALIGNMENT_LEFT = "left" + const val ALIGNMENT_CENTER = "center" + const val ALIGNMENT_RIGHT = "right" + + data class ClockStyleConfig( + val position: Position, + val align: Align, + val visible: Boolean = true, + val customHeightRes: Int? = null, + val customDateMarginTop: Int? = null, + val placeholderTextRes: Int? = null, + ) + + enum class Position { ABOVE, BELOW } + enum class Align { LEFT, CENTER, RIGHT } + + fun resolveConfig(context: Context, className: String, isLarge: Boolean = false): ClockStyleConfig? { + val key = if (isLarge) "${className}_large" else className + val base = clockConfigMap[key] ?: clockConfigMap[className] ?: return null + if (base.position == Position.BELOW) return base + + val alignValue = ClockSettingsRepository.alignment.value + + val resolvedAlign = when (alignValue) { + ALIGNMENT_LEFT -> Align.LEFT + ALIGNMENT_CENTER -> Align.CENTER + ALIGNMENT_RIGHT -> Align.RIGHT + else -> base.align + } + + if (resolvedAlign == base.align) return base + return base.copy(align = resolvedAlign) + } + + data class DateParts(val main: String, val secondary: String) + + fun parseDateDisplay(date: String): DateParts { + val parts = date.split(" ") + return DateParts( + parts.getOrNull(0)?.trim().orEmpty(), + parts.getOrNull(1)?.trim().orEmpty(), + ) + } + + val clockConfigMap = mapOf( + "GeneralClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + placeholderTextRes = R.string.clock_text_placeholder, + ), + "OldQuickLookClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + placeholderTextRes = R.string.clock_old_quick_look_image_placeholder, + ), + "OldQuickLookClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + customHeightRes = R.dimen.large_clock_height, + ), + "GeneralClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + customHeightRes = R.dimen.large_clock_height, + ), + "BitmapDigitComposeClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "BitmapDigitComposeClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + customHeightRes = R.dimen.large_clock_height, + ), + "CyberpunkClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "CyberpunkClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + customHeightRes = R.dimen.large_clock_height, + ), + "AxionAgeClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "AxionAgeClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + customHeightRes = R.dimen.large_clock_height, + ), + ) +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt new file mode 100644 index 0000000000000..f577a00b8d827 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.json.JSONObject + +object ClockSettingsRepository { + + const val SETTING_CLOCK_FACE = "lock_screen_custom_clock_face" + const val SETTING_ALIGNMENT = "ax_clock_alignment" + const val SETTING_SIZE = "ax_clock_size" + const val SETTING_DATE_POSITION = "ax_clock_compose_date_position" + const val SETTING_CLOCK_COLOR = "ax_clock_color" + + const val COLOR_AUTO = "auto" + + const val ALIGNMENT_LEFT = "left" + const val ALIGNMENT_CENTER = "center" + const val ALIGNMENT_RIGHT = "right" + const val SIZE_DEFAULT = "default" + const val SIZE_LARGE = "large" + const val DATE_POSITION_ABOVE = "above" + const val DATE_POSITION_BELOW = "below" + + private const val LARGE_SIZE_SCALE = 1.15f + + @JvmField val clockFaceUri: Uri = Settings.Secure.getUriFor(SETTING_CLOCK_FACE) + @JvmField val alignmentUri: Uri = Settings.Secure.getUriFor(SETTING_ALIGNMENT) + @JvmField val sizeUri: Uri = Settings.Secure.getUriFor(SETTING_SIZE) + @JvmField val datePositionUri: Uri = Settings.Secure.getUriFor(SETTING_DATE_POSITION) + @JvmField val clockColorUri: Uri = Settings.Secure.getUriFor(SETTING_CLOCK_COLOR) + + private val _clockId = MutableStateFlow("DEFAULT") + val clockId: StateFlow = _clockId.asStateFlow() + + private val _alignment = MutableStateFlow(ALIGNMENT_CENTER) + val alignment: StateFlow = _alignment.asStateFlow() + + private val _sizeScale = MutableStateFlow(1f) + val sizeScale: StateFlow = _sizeScale.asStateFlow() + + private val _isDateBelow = MutableStateFlow(false) + val isDateBelow: StateFlow = _isDateBelow.asStateFlow() + + private val _clockColorOverride = MutableStateFlow(null) + val clockColorOverride: StateFlow = _clockColorOverride.asStateFlow() + + private val _shouldCenterIcons = MutableStateFlow(true) + val shouldCenterIcons: StateFlow = _shouldCenterIcons.asStateFlow() + + private var contentResolver: ContentResolver? = null + private var registered = false + private val handler = Handler(Looper.getMainLooper()) + + private val observer = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + val cr = contentResolver ?: return + when (uri) { + clockFaceUri -> { + _clockId.value = readClockId(cr) + _shouldCenterIcons.value = computeShouldCenter(cr) + } + alignmentUri -> { + _alignment.value = readAlignment(cr) + _shouldCenterIcons.value = computeShouldCenter(cr) + } + sizeUri -> { + _sizeScale.value = readSizeScale(cr) + } + datePositionUri -> { + _isDateBelow.value = readDateBelow(cr) + } + clockColorUri -> { + _clockColorOverride.value = readClockColor(cr) + } + } + } + } + + @JvmStatic + fun init(context: Context) { + contentResolver = context.contentResolver + val cr = context.contentResolver + + if (registered) { + _clockColorOverride.value = readClockColor(cr) + return + } + registered = true + + cr.registerContentObserver(clockFaceUri, false, observer) + cr.registerContentObserver(alignmentUri, false, observer) + cr.registerContentObserver(sizeUri, false, observer) + cr.registerContentObserver(datePositionUri, false, observer) + cr.registerContentObserver(clockColorUri, false, observer) + + _clockId.value = readClockId(cr) + _alignment.value = readAlignment(cr) + _sizeScale.value = readSizeScale(cr) + _isDateBelow.value = readDateBelow(cr) + _clockColorOverride.value = readClockColor(cr) + _shouldCenterIcons.value = computeShouldCenter(cr) + } + + @JvmStatic + fun shouldCenterIcons(context: Context): Boolean { + init(context) + return _shouldCenterIcons.value + } + + private fun readClockId(cr: ContentResolver): String { + return try { + val json = Settings.Secure.getString(cr, SETTING_CLOCK_FACE) + if (!json.isNullOrEmpty()) JSONObject(json).optString("clockId", "DEFAULT") + else "DEFAULT" + } catch (_: Exception) { + "DEFAULT" + } + } + + private fun readAlignment(cr: ContentResolver): String { + return Settings.Secure.getString(cr, SETTING_ALIGNMENT) ?: ALIGNMENT_CENTER + } + + private fun readSizeScale(cr: ContentResolver): Float { + return if (Settings.Secure.getString(cr, SETTING_SIZE) == SIZE_LARGE) LARGE_SIZE_SCALE + else 1f + } + + private fun readDateBelow(cr: ContentResolver): Boolean { + return Settings.Secure.getString(cr, SETTING_DATE_POSITION) == DATE_POSITION_BELOW + } + + private fun readClockColor(cr: ContentResolver): Int? { + val raw = Settings.Secure.getString(cr, SETTING_CLOCK_COLOR) + if (raw.isNullOrEmpty() || raw == COLOR_AUTO) return null + return try { + android.graphics.Color.parseColor(raw) + } catch (_: Exception) { + null + } + } + + private fun computeShouldCenter(cr: ContentResolver): Boolean { + val align = readAlignment(cr) + if (align == ALIGNMENT_LEFT || align == ALIGNMENT_RIGHT) return false + if (align == ALIGNMENT_CENTER) return true + val id = readClockId(cr) + return !isDefaultLeftAlignedClock(id) + } + + @JvmStatic + fun isDefaultLeftAlignedClock(clockId: String): Boolean { + return clockId.equals("GENERAL", ignoreCase = true) || + clockId.equals("OLD_QUICKLOOK", ignoreCase = true) || + clockId.equals("AXION_AGE", ignoreCase = true) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockViewExtensions.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockViewExtensions.kt new file mode 100644 index 0000000000000..cdd6d34b8f32d --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockViewExtensions.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.util.SparseArray +import android.view.View +import androidx.annotation.IdRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.customization.R + +private val viewCacheKey = Any() + +private const val MAX_SCALE_RATIO = 1.0f + +val Context.scaleRatio: Float + get() { + val displayMetrics = resources.displayMetrics + val sw = minOf(displayMetrics.widthPixels, displayMetrics.heightPixels) / displayMetrics.density + val ratio = sw / 420f + return minOf(ratio, MAX_SCALE_RATIO) + } + +fun Context.scaledDimen(resId: Int): Float { + return resources.getDimension(resId) * scaleRatio +} + +fun Context.scaledDimenInt(resId: Int): Int { + return scaledDimen(resId).toInt() +} + +fun createBitmaps(context: Context, resIds: IntArray): List { + return resIds.map { resId -> + ContextCompat.getDrawable(context, resId)?.toBitmap() + } +} + +inline fun View.bindView(@IdRes id: Int): T { + val cache = getTag(R.id.view_cache_key) as? SparseArray + ?: SparseArray().also { setTag(R.id.view_cache_key, it) } + + val view = cache.get(id) ?: findViewById(id)!!.also { cache.put(id, it) } + return view as T +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DepthWallpaperProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DepthWallpaperProvider.kt new file mode 100644 index 0000000000000..0ddd6da3830f6 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DepthWallpaperProvider.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.app.WallpaperManager +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.graphics.Path +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.util.Base64 +import android.util.Log +import java.nio.ByteBuffer +import java.nio.ByteOrder + +object DepthWallpaperProvider { + + private const val TAG = "DepthWallpaperProvider" + private const val SETTING_DEPTH_MASK = "ax_depth_subject_mask" + private const val SETTING_DEPTH_ENABLED = "ax_depth_clock_enabled" + private const val PATH_VERSION = 0x01 + + @Volatile + var subjectPath: Path? = null + private set + + @Volatile + var pathAspect: Float = 1f + private set + + @Volatile + var isEnabled: Boolean = false + private set + + private val listeners = mutableSetOf() + private val handler = Handler(Looper.getMainLooper()) + private var registered = false + private var contentResolver: ContentResolver? = null + private var wallpaperManager: WallpaperManager? = null + + interface DepthMaskListener { + fun onDepthDataChanged(path: Path?, pathAspect: Float) + } + + private val observer = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean) { + refreshAsync() + } + } + + fun init(context: Context) { + if (registered) return + registered = true + contentResolver = context.contentResolver + wallpaperManager = WallpaperManager.getInstance(context) + + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor(SETTING_DEPTH_MASK), + false, observer + ) + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor(SETTING_DEPTH_ENABLED), + false, observer + ) + + refreshAsync() + } + + fun addListener(listener: DepthMaskListener) { + listeners.add(listener) + listener.onDepthDataChanged( + if (isEnabled) subjectPath else null, + pathAspect + ) + if (subjectPath == null) { + refreshAsync() + } + } + + fun removeListener(listener: DepthMaskListener) { + listeners.remove(listener) + } + + private fun refreshAsync() { + val cr = contentResolver ?: return + Thread { + try { + val isLiveWallpaper = wallpaperManager?.wallpaperInfo != null + val enabled = !isLiveWallpaper && + Settings.Secure.getInt(cr, SETTING_DEPTH_ENABLED, 0) == 1 + val maskStr = if (enabled) { + Settings.Secure.getString(cr, SETTING_DEPTH_MASK) + } else null + + val pathResult = maskStr?.let { decodePath(it) } + + isEnabled = enabled + subjectPath = pathResult?.first + pathAspect = pathResult?.second ?: 1f + + handler.post { + notifyListeners(if (enabled) pathResult?.first else null) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to refresh depth data", e) + handler.post { notifyListeners(null) } + } + }.start() + } + + private fun decodePath(base64Str: String): Pair? { + return try { + val bytes = Base64.decode(base64Str, Base64.NO_WRAP) + if (bytes.size < 7) return null + + if (bytes[0].toInt() and 0xFF != PATH_VERSION) { + Log.w(TAG, "Unknown path format version: ${bytes[0]}") + return null + } + + val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN) + buf.get() + + val extractW = buf.short.toInt() and 0xFFFF + val extractH = buf.short.toInt() and 0xFFFF + val numContours = buf.short.toInt() and 0xFFFF + + if (extractW <= 0 || extractH <= 0 || numContours <= 0) return null + + val path = Path() + path.fillType = Path.FillType.WINDING + + for (c in 0 until numContours) { + if (buf.remaining() < 2) break + val numPoints = buf.short.toInt() and 0xFFFF + if (numPoints < 3 || buf.remaining() < numPoints * 4) continue + + for (p in 0 until numPoints) { + val x = (buf.short.toInt() and 0xFFFF).toFloat() + val y = (buf.short.toInt() and 0xFFFF).toFloat() + if (p == 0) path.moveTo(x, y) else path.lineTo(x, y) + } + path.close() + } + + Pair(path, extractW.toFloat() / extractH) + } catch (e: Exception) { + Log.e(TAG, "Failed to decode depth path", e) + null + } + } + + private fun notifyListeners(path: Path?) { + val aspect = pathAspect + for (listener in listeners) { + listener.onDepthDataChanged(path, aspect) + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt new file mode 100644 index 0000000000000..8a9827a1d8734 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import com.android.internal.util.android.OmniJawsClient +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData + +object WeatherUtils { + + fun getWeatherIcon(context: Context, conditionCode: Int): Drawable? { + return OmniJawsClient.get().getWeatherConditionImage(context, conditionCode) + } + + fun resolveWeatherBitmap( + context: Context, + weather: ClockWeatherData?, + iconSize: Int + ): Bitmap? { + if (weather == null || weather == ClockWeatherData.EMPTY) return null + + val iconBytes = weather.iconBytes + if (iconBytes != null && iconBytes.isNotEmpty()) { + return try { + val raw = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size) + ?: return null + Bitmap.createScaledBitmap(raw, iconSize, iconSize, true) + } catch (_: Exception) { null } + } + + if (weather.conditionCode == 0) return null + return try { + val drawable = getWeatherIcon(context, weather.conditionCode) ?: return null + val bmp = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bmp) + drawable.setBounds(0, 0, iconSize, iconSize) + drawable.draw(canvas) + bmp + } catch (_: Exception) { null } + } +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt new file mode 100644 index 0000000000000..2c4b0d09e66b8 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.animation.ValueAnimator +import android.content.Context +import android.os.VibrationEffect +import android.os.Vibrator +import android.util.Log +import androidx.compose.ui.geometry.Offset +import com.android.app.animation.Interpolators + +fun AxClockView.animateAppear() { + Log.d(tag, "animateAppear") + animAlpha = 0f + ValueAnimator.ofFloat(0f, 1f).apply { + duration = APPEAR_DURATION + interpolator = Interpolators.EMPHASIZED_DECELERATE + addUpdateListener { animAlpha = it.animatedValue as Float } + start() + } +} + +fun AxClockView.animateCharge() { + Log.d(tag, "animateCharge") + if (isPreviewMode) return + val cx = width / 2f + val cy = height / 2f + state.fidgetPosition.value = Offset(cx, cy) + state.fidgetTrigger.value = System.currentTimeMillis() + onChargeAnimation() +} + +fun AxClockView.animateFidgetTapDefault(x: Float, y: Float) { + state.fidgetPosition.value = Offset(x, y) + state.fidgetTrigger.value = System.currentTimeMillis() + val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator + if (useGlitchInteraction) { + vibrator?.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) + } else { + vibrator?.vibrate(FIDGET_HAPTICS) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt new file mode 100644 index 0000000000000..280e709536d16 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.os.VibrationEffect +import androidx.compose.animation.core.CubicBezierEasing + +internal const val FONT_FAMILY_BODY = "variable-title-small" +internal const val FONT_FAMILY_DATE = "variable-title-medium-emphasized" + +internal const val SMALL_CLOCK_BOTTOM_PAD_DP = 12f + +internal const val ALARM_VISIBILITY_HOURS = 12L + +internal const val CLOCK_PATTERN_12 = "hhmm" +internal const val CLOCK_PATTERN_12_STANDARD = "hh:mm" +internal const val CLOCK_PATTERN_24 = "HHmm" +internal const val CLOCK_PATTERN_24_STANDARD = "HH:mm" +internal const val CLOCK_PATTERN_ALL = "hhmmss" + +internal const val APPEAR_DURATION = 400L + +internal val FIDGET_HAPTICS: VibrationEffect = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43) + .compose() + +const val PREVIEW_TIME_12 = "1008" +const val PREVIEW_TIME_12_STANDARD = "10:08" +const val PREVIEW_TIME_24 = "1008" +const val PREVIEW_TIME_24_STANDARD = "10:08" +const val PREVIEW_TIME_ALL = "100830" +const val PREVIEW_DATE = "Wed, 12 Mar" + +internal const val COMPOSE_FIDGET_SQUEEZE = 0.95f +internal const val COMPOSE_FIDGET_EXPAND = 1.03f +internal const val COMPOSE_FIDGET_PHASE_MS = 120 +internal const val COMPOSE_FIDGET_SETTLE_MS = 150 +internal val COMPOSE_FIDGET_EASING = CubicBezierEasing(0.26873f, 0f, 0.45042f, 1f) + +internal const val DOZE_WAKE_START = 0.96f +internal const val DOZE_WAKE_MS = 600 +internal val DOZE_EASING = CubicBezierEasing(0.2f, 0f, 0f, 1f) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockDisplayResolver.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockDisplayResolver.kt new file mode 100644 index 0000000000000..29b86130f5b9b --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockDisplayResolver.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.app.PendingIntent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.axion.quicklook.client.R as QuickLookR +import com.android.systemui.customization.R +import com.android.systemui.plugins.keyguard.ui.clocks.CalendarSimpleData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData +import com.android.systemui.shared.clocks.WeatherUtils + +internal fun AxClockView.resolveDisplay( + clockData: ClockData, + media: ClockMediaState, + nowPlaying: String, + nowPlayingTapAction: PendingIntent?, + alarm: String, + date: String, +): DateDisplay { + if (media.isPlaying && media.trackTitle.isNotEmpty()) { + val fullText = if (media.artistName.isNotEmpty()) { + "${media.trackTitle} - ${media.artistName}" + } else { + "Now playing ${media.trackTitle}" + } + return DateDisplay.IconText(fullText, loadNowPlayingIcon(media.packageName), media.packageName.isEmpty()) + } + + if (nowPlaying.isNotBlank()) { + return DateDisplay.IconText(nowPlaying, loadNowPlayingIcon(media.packageName), false, nowPlayingTapAction) + } + + val activeSmartspace = clockData.smartspace.firstOrNull { + it.title.isNotEmpty() && !it.isSensitive + } + if (activeSmartspace != null) { + val text = if (activeSmartspace.subtitle.isNotEmpty()) { + "${activeSmartspace.title} · ${activeSmartspace.subtitle}" + } else { + activeSmartspace.title + } + return DateDisplay.IconText( + text, + decodeSmartspaceIcon(activeSmartspace.iconBytes), + true, + activeSmartspace.tapAction, + ) + } + + if (alarm.isNotBlank()) { + return DateDisplay.IconText(alarm, loadDrawableIcon(QuickLookR.drawable.ic_alarm), true) + } + + val cal = clockData.calendar.takeIf { it != CalendarSimpleData.EMPTY } + if (cal != null && cal.isEventVisible() && !cal.title.isNullOrEmpty()) { + return DateDisplay.IconText( + cal.description, + loadDrawableIcon(QuickLookR.drawable.ic_calendar), + true, + cal.tapAction, + ) + } + + val weather = clockData.weather.takeIf { it != ClockWeatherData.EMPTY } + if (weather != null && weather.temp.isNotEmpty()) { + return DateDisplay.Weather( + date, + weather.formattedTemp, + WeatherUtils.resolveWeatherBitmap(context, weather, iconSize), + weather.tintIcon, + weather.tapAction, + ) + } + + return DateDisplay.DateOnly(date) +} + +internal fun AxClockView.loadDrawableIcon(resId: Int): Bitmap? { + return try { + ContextCompat.getDrawable(context, resId) + ?.toBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888) + } catch (e: Exception) { + Log.e(tag, "Error loading icon", e) + null + } +} + +internal fun AxClockView.loadNowPlayingIcon(packageName: String): Bitmap? { + if (packageName.isNotEmpty()) { + try { + val appIcon = context.packageManager.getApplicationIcon(packageName) + return appIcon.toBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888) + } catch (_: Exception) {} + } + return loadDrawableIcon(QuickLookR.drawable.ic_now_playing_music_note) +} + +internal fun AxClockView.decodeSmartspaceIcon(iconBytes: ByteArray?): Bitmap? { + if (iconBytes == null || iconBytes.isEmpty()) return null + return try { + val raw = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size) ?: return null + Bitmap.createScaledBitmap(raw, iconSize, iconSize, true) + } catch (e: Exception) { + Log.e(tag, "Error decoding smartspace icon", e) + null + } +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt new file mode 100644 index 0000000000000..d7d98b3a0c55f --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import com.android.axion.compose.host.AxComposeView +import com.android.systemui.shared.clocks.ClockSettingsRepository + +class AxClockHost(private val clock: AxClockView) { + + private lateinit var composeView: AxComposeView + + fun attach(content: @Composable () -> Unit) { + clock.setWillNotDraw(false) + clock.clipChildren = false + clock.clipToPadding = false + clock.layoutDirection = View.LAYOUT_DIRECTION_LTR + ClockSettingsRepository.init(clock.context) + + composeView = AxComposeView(clock.context).apply { + setContent { Host { content() } } + } + + try { + clock.addView(composeView, ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + )) + } catch (e: Exception) { + Log.d("AxClockHost", "AxClockHost init failed error: $e") + } + } + + val view: AxComposeView get() = composeView + + @Composable + private fun Host(content: @Composable () -> Unit) { + val state = clock.state + + LaunchedEffect(Unit) { + ClockSettingsRepository.isDateBelow.collect { state.dateBelowState.value = it } + } + LaunchedEffect(Unit) { + ClockSettingsRepository.alignment.collect { state.alignmentState.value = it } + } + LaunchedEffect(Unit) { + ClockSettingsRepository.clockColorOverride.collect { state.clockColorOverrideState.value = it } + } + + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + val align = state.alignmentState.value + val trigger by state.fidgetTrigger + val isDoze by state.dozeFlow.collectAsState() + val animScale = remember { Animatable(1f) } + var initialDoze by remember { mutableStateOf(true) } + + LaunchedEffect(trigger) { + if (trigger == 0L || clock.useGlitchInteraction) return@LaunchedEffect + animScale.snapTo(1f) + animScale.animateTo(COMPOSE_FIDGET_SQUEEZE, tween(COMPOSE_FIDGET_PHASE_MS, easing = COMPOSE_FIDGET_EASING)) + animScale.animateTo(COMPOSE_FIDGET_EXPAND, tween(COMPOSE_FIDGET_PHASE_MS, easing = COMPOSE_FIDGET_EASING)) + animScale.animateTo(1f, tween(COMPOSE_FIDGET_SETTLE_MS, easing = COMPOSE_FIDGET_EASING)) + } + + LaunchedEffect(isDoze) { + if (initialDoze) { initialDoze = false; return@LaunchedEffect } + if (!isDoze) { + animScale.snapTo(DOZE_WAKE_START) + animScale.animateTo(1f, tween(DOZE_WAKE_MS, easing = DOZE_EASING)) + } + } + + val sizeModifier = if (clock.isLargeClock) { + Modifier.fillMaxWidth().wrapContentHeight() + } else { + Modifier.fillMaxSize() + } + Box( + modifier = sizeModifier + .graphicsLayer { + val s = clock.sizeScale + val a = animScale.value + scaleX = a + scaleY = s * a + when (align) { + ClockSettingsRepository.ALIGNMENT_LEFT -> + transformOrigin = TransformOrigin(0f, 0.5f) + ClockSettingsRepository.ALIGNMENT_RIGHT -> + transformOrigin = TransformOrigin(1f, 0.5f) + } + } + ) { + content() + } + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt new file mode 100644 index 0000000000000..bfb6e0e0dae25 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.content.Context +import android.icu.util.TimeZone +import android.text.format.DateFormat +import com.android.systemui.plugins.keyguard.data.model.AlarmData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import java.text.SimpleDateFormat +import java.util.* +import java.util.TimeZone as JavaTimeZone + +class AxClockInteractor( + private val context: Context, + val state: AxClockState, + val quickLook: QuickLookController, +) { + + val calendar: Calendar = Calendar.getInstance() + var format: String? = null + var timeStr: String = "" + internal set + var locale: Locale = Locale.getDefault() + + var needsSeconds: Boolean = false + var useStandardFormat: Boolean = false + + fun onAlarmDataChanged(data: AlarmData) { quickLook.onAlarmDataChanged(data) } + fun onClockDataChanged(data: ClockData) { quickLook.onClockDataChanged(data) } + fun onPlaybackStateChanged(playing: Boolean) { quickLook.onPlaybackStateChanged(playing) } + fun onMetadataChanged(track: String, artist: String, packageName: String) { quickLook.onMetadataChanged(track, artist, packageName) } + fun onNowPlayingUpdate(text: String) { quickLook.onNowPlayingUpdate(text) } + + fun onDozeChanged(doze: Boolean) { state.isDoze = doze } + fun onScreenOff(screenOff: Boolean) { state.isScreenOff = screenOff } + fun onRegionDarknessChanged(regionDark: Boolean) { state.isRegionDark = regionDark } + fun onFontSettingChanged() { state.fontVersion.intValue++ } + + fun onStartedWakingUp() { + state.isDoze = false + state.isScreenOff = false + } + + fun onTimeZoneChanged(timeZone: TimeZone) { + calendar.timeZone = JavaTimeZone.getTimeZone(timeZone.id) + } + + fun refreshFormat(use24: Boolean, newLocale: Locale = locale) { + val newFormat = when { + useStandardFormat -> if (use24) CLOCK_PATTERN_24_STANDARD else CLOCK_PATTERN_12_STANDARD + needsSeconds -> CLOCK_PATTERN_ALL + else -> if (use24) CLOCK_PATTERN_24 else CLOCK_PATTERN_12 + } + + if (format == newFormat && locale == newLocale) return + format = newFormat + locale = newLocale + refreshTime() + } + + fun refreshTime(): Boolean { + format ?: return false + calendar.timeInMillis = System.currentTimeMillis() + val newTime = SimpleDateFormat(format, Locale.ENGLISH).format(calendar.time) + refreshDate() + val changed = timeStr != newTime + if (changed) { + timeStr = newTime + } + state.timeState.value = timeStr + return changed + } + + fun refreshDate() { + val dateFormat = SimpleDateFormat("EEE, dd MMM", locale) + state.dateStrFlow.value = dateFormat.format(calendar.time) + } + + fun setupPreview(setPreviewMode: () -> Unit) { + setPreviewMode() + + val use24 = DateFormat.is24HourFormat(context) + refreshFormat(use24) + + val previewFormat = format ?: if (use24) CLOCK_PATTERN_24 else CLOCK_PATTERN_12 + timeStr = when (previewFormat) { + CLOCK_PATTERN_12_STANDARD, CLOCK_PATTERN_24_STANDARD -> PREVIEW_TIME_12_STANDARD + CLOCK_PATTERN_ALL -> PREVIEW_TIME_ALL + else -> PREVIEW_TIME_12 + } + state.dateStrFlow.value = PREVIEW_DATE + state.timeState.value = timeStr + state.dozeAmountFlow.value = 0f + } + + val talkBackContent: String + get() { + val pattern = if (DateFormat.is24HourFormat(context)) CLOCK_PATTERN_24_STANDARD else "hh:mm" + return SimpleDateFormat(pattern, Locale.ENGLISH).format(calendar.time) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockModels.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockModels.kt new file mode 100644 index 0000000000000..539834bfc4647 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockModels.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.graphics.Bitmap + +data class ClockMediaState( + val isPlaying: Boolean = false, + val trackTitle: String = "", + val artistName: String = "", + val packageName: String = "", +) + +data class ClockUiState( + val time: String = "", + val date: String = "", + val isDoze: Boolean = false, + val screenOff: Boolean = false, + val regionDark: Boolean = false, + val icon: Bitmap? = null, + val tintIcon: Boolean = true, + val display: DateDisplay = DateDisplay.DateOnly(""), + val colorOverride: Int? = null, +) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockState.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockState.kt new file mode 100644 index 0000000000000..d653b2a010d1e --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockState.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import com.android.systemui.shared.clocks.ClockSettingsRepository +import kotlinx.coroutines.flow.MutableStateFlow + +class AxClockState { + + val dozeFlow = MutableStateFlow(false) + val screenOffFlow = MutableStateFlow(false) + val regionDarkFlow = MutableStateFlow(false) + val dozeAmountFlow = MutableStateFlow(0f) + val dateStrFlow = MutableStateFlow("") + + val timeState = mutableStateOf("") + val fidgetTrigger = mutableStateOf(0L) + val fidgetPosition = mutableStateOf(Offset.Zero) + val dateBelowState = mutableStateOf(false) + val alignmentState = mutableStateOf(ClockSettingsRepository.alignment.value) + val clockColorOverrideState = mutableStateOf(ClockSettingsRepository.clockColorOverride.value) + val fontVersion = mutableIntStateOf(0) + + var isDoze: Boolean + get() = dozeFlow.value + set(value) { dozeFlow.value = value } + var isScreenOff: Boolean + get() = screenOffFlow.value + set(value) { screenOffFlow.value = value } + var isRegionDark: Boolean + get() = regionDarkFlow.value + set(value) { regionDarkFlow.value = value } + + val dateStr: String get() = dateStrFlow.value +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt new file mode 100644 index 0000000000000..82a33792d691f --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.content.Context +import android.content.res.Configuration +import android.graphics.* +import android.icu.util.TimeZone +import android.text.format.DateFormat +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.ViewGroup +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.customization.R +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.keyguard.data.model.AlarmData +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.ClockConfigs +import com.android.systemui.shared.clocks.ClockSettingsRepository +import com.android.systemui.shared.clocks.extensions.* +import java.util.Locale +import kotlinx.coroutines.* + +abstract class AxClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : ViewGroup(context, attrs, defStyleAttr, defStyleRes) { + + val state = AxClockState() + val quickLook = QuickLookController(this) + val interactor = AxClockInteractor(context, state, quickLook) + val viewModel = AxClockViewModel(state, quickLook) + internal val host = AxClockHost(this) + + private var uiScope: CoroutineScope? = null + + var isLargeClock = false + + var isPreviewMode = false + set(value) { + field = value + if (value) { + touchEnabled = false + depthEffectEnabled = false + animAlpha = 1f + } + } + + var animAlpha: Float = 1f + set(value) { + if (isPreviewMode && value != 1f) return + field = value + alpha = value + } + + var touchEnabled: Boolean = true + var onFidgetTapListener: ((Float, Float) -> Unit)? = null + + private val depthController = ClockDepthController(this) + var depthEffectEnabled: Boolean + get() = depthController.enabled + set(value) { depthController.enabled = value } + + protected open val clockHeightBase: Int get() = context.scaledDimenInt(R.dimen.clock_height) + val clockPaddingTop get() = context.scaledDimen(R.dimen.clock_padding_top) + val clockPaddingStart get() = context.scaledDimen(R.dimen.clock_padding_start) + val clockDateTextSize get() = context.scaledDimen(R.dimen.clock_date_text_size) + val clockDateMarginTop get() = context.scaledDimen(R.dimen.clock_date_margin_top) + val scaleRatio get() = context.scaleRatio + val sizeScale get() = if (isPreviewMode) 1f else ClockSettingsRepository.sizeScale.value + val iconSize get() = context.scaledDimenInt(R.dimen.clock_icon_secondary_size) + + protected val config: ClockConfigs.ClockStyleConfig? + get() { + val className = this::class.simpleName ?: return null + return ClockConfigs.resolveConfig(context, className, isLargeClock) + } + + val isLeftAligned: Boolean get() = config?.align == ClockConfigs.Align.LEFT + val isRightAligned: Boolean get() = config?.align == ClockConfigs.Align.RIGHT + val isSideAligned: Boolean get() = isLeftAligned || isRightAligned + + val clockHeight: Int + get() { + val resHeight = config?.customHeightRes?.let { context.scaledDimenInt(it) } ?: clockHeightBase + val bottomPad = if (!isLargeClock) { + (SMALL_CLOCK_BOTTOM_PAD_DP * context.resources.displayMetrics.density).toInt() + } else 0 + return ((resHeight + dateHeight) * sizeScale).toInt() + bottomPad + } + + val dateMarginTop: Int + get() { + val cfg = config ?: return 0 + if (!cfg.visible) return 0 + return (cfg.customDateMarginTop?.let { context.scaledDimen(it) } ?: clockDateMarginTop).toInt() + } + + val dateHeight: Int + get() { + val cfg = config ?: return 0 + if (!cfg.visible) return 0 + return when (cfg.position) { + ClockConfigs.Position.ABOVE -> (clockDateTextSize + dateMarginTop + clockPaddingTop).toInt() + ClockConfigs.Position.BELOW -> clockDateTextSize.toInt() + else -> 0 + } + } + + var isDoze: Boolean + get() = state.isDoze + set(value) { state.isDoze = value } + var isScreenOff: Boolean + get() = state.isScreenOff + set(value) { state.isScreenOff = value } + var isRegionDark: Boolean + get() = state.isRegionDark + set(value) { state.isRegionDark = value } + val dateStr: String get() = state.dateStr + + init { + host.attach { Content() } + } + + abstract override fun getTag(): String + + @Composable + protected abstract fun Content() + + internal open val useGlitchInteraction: Boolean = false + + open fun onAlarmDataChanged(data: AlarmData) { interactor.onAlarmDataChanged(data) } + open fun onClockDataChanged(data: ClockData) { interactor.onClockDataChanged(data) } + open fun onDateChanged() {} + open fun onThemeChanged(isDarkTheme: Boolean) {} + open fun onPlaybackStateChanged(playing: Boolean) { interactor.onPlaybackStateChanged(playing) } + open fun onMetadataChanged(track: String, artist: String, packageName: String) { interactor.onMetadataChanged(track, artist, packageName) } + open fun onNowPlayingUpdate(npText: String) { interactor.onNowPlayingUpdate(npText) } + open fun onClockLayoutChanged(isCentered: Boolean, isLargeClockVisible: Boolean) {} + fun onDepthEffectVisibilityChanged(visible: Boolean) { depthController.setDepthVisible(visible) } + fun setMessageBuffer(buffer: MessageBuffer) {} + open fun onDozeChanged(doze: Boolean) { interactor.onDozeChanged(doze) } + open fun onChargeAnimation() {} + open fun onPulsingChanged(doze: Boolean) {} + open fun onScreenOff(screenOff: Boolean) { interactor.onScreenOff(screenOff) } + open fun onRegionDarknessChanged(regionDark: Boolean) { interactor.onRegionDarknessChanged(regionDark) } + open fun onFontSettingChanged() { interactor.onFontSettingChanged() } + open fun onTimeZoneChanged(timeZone: TimeZone) { interactor.onTimeZoneChanged(timeZone) } + + open fun onDozeAmountChanged(linear: Float, eased: Float) { + if (isPreviewMode) return + state.dozeAmountFlow.value = eased + } + + open fun onStartedWakingUp() { + interactor.onStartedWakingUp() + uiScope?.launch { + delay(1250) + interactor.refreshTime() + } + } + + open fun onStartedGoingToSleep(isKeyguardVisible: Boolean) {} + open fun onWakefulnessStateChanged(isWakingUp: Boolean, tapPosition: Point?) {} + + open fun refreshFormat(use24: Boolean, newLocale: Locale = interactor.locale) { + interactor.needsSeconds = (this as? BitmapDigitComposeClockView)?.faceStyle?.needsPerSecondTick == true + interactor.useStandardFormat = this is OldQuickLookClockView + interactor.refreshFormat(use24, newLocale) + } + + open fun refreshTime() { + if (interactor.refreshTime()) { + contentDescription = interactor.talkBackContent + } + } + + fun refreshDate() { interactor.refreshDate() } + + open fun animateFidgetTap(x: Float, y: Float) = animateFidgetTapDefault(x, y) + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + if (!touchEnabled) return false + return super.dispatchTouchEvent(ev) + } + + override fun draw(canvas: Canvas) { + if (!depthController.shouldApplyDepth()) { + super.draw(canvas) + return + } + depthController.drawWithDepth(canvas) { super.draw(it) } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + Log.d(tag, "onAttachedToWindow") + ClockSettingsRepository.init(context) + uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + (parent as? ViewGroup)?.let { + it.clipChildren = false + it.clipToPadding = false + } + depthController.onAttached() + uiScope?.launch { + ClockSettingsRepository.sizeScale.collect { requestLayout() } + } + refreshTime() + state.timeState.value = interactor.timeStr + state.dateBelowState.value = ClockSettingsRepository.isDateBelow.value + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + Log.d(tag, "onDetachedFromWindow") + uiScope?.cancel() + depthController.onDetached() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + val newLocale = newConfig.locale + if (newLocale != interactor.locale) { + uiScope?.launch { refreshFormat(DateFormat.is24HourFormat(context), newLocale) } + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val w = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec) + if (isLargeClock) { + val maxH = MeasureSpec.getSize(heightMeasureSpec) + val cv = host.view + cv.measure( + MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(maxH, MeasureSpec.AT_MOST), + ) + setMeasuredDimension(w, cv.measuredHeight) + } else { + setMeasuredDimension(w, clockHeight) + val mw = measuredWidth + val mh = measuredHeight + if (mw > 0 && mh > 0) { + host.view.measure( + MeasureSpec.makeMeasureSpec(mw, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mh, MeasureSpec.EXACTLY), + ) + } + } + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + val cv = host.view + if (!cv.isAttachedToWindow) return + if (!isLargeClock && (isPreviewMode)) { + cv.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), + ) + } + cv.layout(0, 0, width, height) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + pivotX = w / 2f + pivotY = h / 2f + } + + protected open fun getContentBounds(): RectF? = null + + open fun setupPreview() { + interactor.setupPreview { + isPreviewMode = true + isDoze = false + isScreenOff = false + isRegionDark = false + } + } + + @Composable + protected fun rememberClockState(): ClockUiState = viewModel.rememberClockState() + + @Composable + protected fun tintColor(isDoze: Boolean, screenOff: Boolean, regionDark: Boolean): Color = + viewModel.tintColor(isDoze, screenOff, regionDark) + + protected val fidgetTapModifier: Modifier + @Composable get() { + val display = viewModel.rememberResolvedDisplay() + return Modifier.pointerInput(touchEnabled, display.tapAction) { + if (!touchEnabled) return@pointerInput + detectTapGestures { offset -> + if (!state.isDoze) { + onFidgetTapListener?.invoke(offset.x, offset.y) + display.tapAction?.let { fireTapAction(it) } + } + } + } + } + + @Composable + protected fun DigitArea( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, + ) { + Box(modifier = modifier.then(fidgetTapModifier)) { + content() + } + } + + @Composable + protected fun EnhancedDateArea( + modifier: Modifier = Modifier, + textColor: Color = tintColor(state.dozeFlow.value, state.screenOffFlow.value, state.regionDarkFlow.value) + .copy(alpha = if (state.dozeFlow.value) 0.6f else 0.8f), + textSize: TextUnit = 18.sp, + fontFamily: FontFamily = remember(state.fontVersion.intValue) { resolveDateFontFamily() }, + fontWeight: FontWeight = FontWeight.Medium, + letterSpacing: TextUnit = 0.sp, + iconSize: Dp = 16.dp, + uppercase: Boolean = false, + rowArrangement: Arrangement.Horizontal = when { + isLeftAligned -> Arrangement.Start + isRightAligned -> Arrangement.End + else -> Arrangement.Center + }, + ) { + val display = viewModel.rememberResolvedDisplay() + QuickLookDateArea( + modifier = modifier, + display = display, + dateStr = state.dateStr, + sizeScale = sizeScale, + textColor = textColor, + textSize = textSize, + fontFamily = fontFamily, + fontWeight = fontWeight, + letterSpacing = letterSpacing, + iconSize = iconSize, + uppercase = uppercase, + rowArrangement = rowArrangement, + ) + } + + companion object { + const val DEBUG = false + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockViewModel.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockViewModel.kt new file mode 100644 index 0000000000000..16ebc7415fbf4 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color + +class AxClockViewModel( + private val state: AxClockState, + private val quickLook: QuickLookController, +) { + + @Composable + fun rememberClockState(): ClockUiState { + val time by state.timeState + val isDoze by state.dozeFlow.collectAsState() + val screenOff by state.screenOffFlow.collectAsState() + val regionDark by state.regionDarkFlow.collectAsState() + val colorOverride by state.clockColorOverrideState + val date by state.dateStrFlow.collectAsState() + val display = quickLook.rememberResolvedDisplay(date) + val displayDate = when (display) { + is DateDisplay.Weather -> { + if (display.temp.isNotEmpty()) "${display.date} ${display.temp}" else display.date + } + is DateDisplay.IconText -> display.text + is DateDisplay.DateOnly -> display.text + } + return ClockUiState( + time, displayDate, isDoze, screenOff, regionDark, + display.icon, display.tintIcon, display, colorOverride, + ) + } + + @Composable + fun tintColor(isDoze: Boolean, screenOff: Boolean, regionDark: Boolean): Color { + val override by state.clockColorOverrideState + if (isDoze || screenOff) return Color.White + override?.let { return Color(it) } + return if (regionDark) Color.White else Color.Black + } + + @Composable + fun rememberResolvedDisplay(): DateDisplay { + val date by state.dateStrFlow.collectAsState() + return quickLook.rememberResolvedDisplay(date) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt new file mode 100644 index 0000000000000..82879b405e4ca --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch + +class AxionAgeClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override val useGlitchInteraction: Boolean = true + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val density = context.resources.displayMetrics.density + return ((SMALL_DIGIT_DP + SMALL_INFO_DP) * density * scaleRatio).toInt() + } + + override fun getTag(): String = + if (isLargeClock) "AxionAgeLargeClockView" else "AxionAgeClockView" + + @Composable + override fun Content() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val fidgetByTrigger by state.fidgetTrigger + + val weightFidget = remember { Animatable(0f) } + + LaunchedEffect(fidgetByTrigger) { + if (fidgetByTrigger > 0) { + launch { + weightFidget.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) + ) + weightFidget.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + } + } + } + + val large = isLargeClock + val fidgetValue = weightFidget.value + val textColor = tintColor(isDoze, screenOff, regionDark) + val isDark = isDoze || regionDark + val digitW = if (large) 90.dp else 48.dp + val digitH = if (large) 150.dp else 108.dp + val digitSpacing = if (large) 8.dp else 2.dp + val digitStroke = if (large) 1.5.dp else 1.dp + val glowExtra = if (large) 4f else 3f + val infoTextSize = if (large) 18.sp else 14.sp + val infoIconSize = if (large) 24.dp else 16.dp + val infoSpacing = if (large) 6.dp else 4.dp + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = when { + large -> Alignment.Center + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + }, + ) { + if (large) { + LargeLayout(time, date, isDark, fidgetValue, digitW, digitH, digitSpacing, digitStroke, glowExtra, infoTextSize, infoIconSize, infoSpacing, textColor) + } else { + SmallLayout(time, date, isDark, fidgetValue, digitW, digitH, digitSpacing, digitStroke, glowExtra, infoTextSize, infoIconSize, infoSpacing, textColor) + } + } + } + + @Composable + private fun LargeLayout( + time: String, date: String, isDark: Boolean, fidgetValue: Float, + digitW: Dp, digitH: Dp, digitSpacing: Dp, digitStroke: Dp, glowExtra: Float, + infoTextSize: androidx.compose.ui.unit.TextUnit, infoIconSize: Dp, infoSpacing: Dp, + textColor: Color, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (time.length >= 4) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.then(fidgetTapModifier), + ) { + Row(horizontalArrangement = Arrangement.spacedBy(digitSpacing)) { + AxDigit(time[0], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + AxDigit(time[1], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + } + Row(horizontalArrangement = Arrangement.spacedBy(digitSpacing)) { + AxDigit(time[2], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + AxDigit(time[3], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + } + } + } + EnhancedDateArea( + textColor = textColor, + textSize = infoTextSize, + iconSize = infoIconSize, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun SmallLayout( + time: String, date: String, isDark: Boolean, fidgetValue: Float, + digitW: Dp, digitH: Dp, digitSpacing: Dp, digitStroke: Dp, glowExtra: Float, + infoTextSize: androidx.compose.ui.unit.TextUnit, infoIconSize: Dp, infoSpacing: Dp, + textColor: Color, + ) { + val rowArrangement = when { + isLeftAligned -> Arrangement.Start + isRightAligned -> Arrangement.End + else -> Arrangement.Center + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = rowArrangement, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp), + ) { + if (time.length >= 4) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.then(fidgetTapModifier), + ) { + AxDigit(time[0], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + Spacer(modifier = Modifier.width(digitSpacing)) + AxDigit(time[1], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + Spacer(modifier = Modifier.width(digitSpacing)) + AxDigit(time[2], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + Spacer(modifier = Modifier.width(digitSpacing)) + AxDigit(time[3], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + EnhancedDateArea( + textColor = textColor, + textSize = infoTextSize, + iconSize = infoIconSize, + rowArrangement = Arrangement.Start, + ) + } + } + + @Composable + private fun AxDigit( + char: Char, isDark: Boolean, fidgetValue: Float, + w: Dp, h: Dp, strokeW: Dp, glowExtra: Float, + baseColor: Color, + ) { + val isDoze by state.dozeFlow.collectAsState() + val fillTop = when { + isDoze -> 0.6f + isDark -> 0.45f + fidgetValue * 0.2f + else -> 0.7f + fidgetValue * 0.15f + } + val fillBottom = when { + isDoze -> 0.3f + isDark -> 0.15f + fidgetValue * 0.1f + else -> 0.35f + fidgetValue * 0.1f + } + val brush = Brush.verticalGradient( + colors = listOf( + baseColor.copy(alpha = fillTop), + baseColor.copy(alpha = fillBottom), + ), + ) + val borderAlpha = when { + isDoze -> 0.5f + isDark -> 0.25f + fidgetValue * 0.15f + else -> 0.5f + fidgetValue * 0.15f + } + Canvas(modifier = Modifier.size(width = w, height = h)) { + drawDigit(char, brush, baseColor.copy(alpha = borderAlpha), strokeW.toPx(), baseColor, isDark, fidgetValue, glowExtra) + } + } + + private fun DrawScope.drawDigit( + char: Char, + brush: Brush, + borderColor: Color, + strokeWidth: Float, + accentColor: Color, + isDark: Boolean, + fidgetValue: Float, + glowExtra: Float, + ) { + val w = size.width + val h = size.height + val padding = w * 0.15f + val dw = w - padding * 2 + val dh = h - padding * 2 + val centerX = w / 2 + val centerY = h / 2 + + val fidgetWall = fidgetValue * (dw * -0.12f) + val wall = dw * 0.35f + fidgetWall + val path = Path() + + when (char) { + '0' -> path.addRoundRect(RoundRect(padding, padding, w - padding, h - padding, CornerRadius(dw / 2))) + '1' -> { path.moveTo(centerX, padding); path.lineTo(centerX, h - padding) } + '2' -> { + path.arcTo(Rect(padding, padding, w - padding, padding + dw), 180f, 180f, true) + path.lineTo(padding, h - padding) + path.lineTo(w - padding, h - padding) + } + '3' -> { + path.arcTo(Rect(padding, padding, w - padding, padding + dh / 2), 180f, 270f, false) + path.arcTo(Rect(padding, centerY, w - padding, h - padding), 270f, 270f, false) + } + '4' -> { + path.moveTo(w - padding, h - padding); path.lineTo(w - padding, padding) + path.moveTo(w - padding, centerY); path.lineTo(padding, centerY); path.lineTo(padding, padding) + } + '5' -> { + path.moveTo(w - padding, padding); path.lineTo(padding, padding); path.lineTo(padding, centerY) + path.arcTo(Rect(padding, centerY, w - padding, h - padding), -90f, 270f, false) + } + '6' -> { + path.moveTo(w - padding, padding); path.lineTo(padding, h - padding - dw / 2) + path.addOval(Rect(padding, h - padding - dw, w - padding, h - padding)) + } + '7' -> { + path.moveTo(padding, padding); path.lineTo(w - padding, padding); path.lineTo(centerX, h - padding) + } + '8' -> { + path.addOval(Rect(padding, padding, w - padding, centerY)) + path.addOval(Rect(padding, centerY, w - padding, h - padding)) + } + '9' -> { + path.moveTo(padding, h - padding); path.lineTo(w - padding, padding + dw / 2) + path.addOval(Rect(padding, padding, w - padding, padding + dw)) + } + } + + val strokeStyle = Stroke(width = wall, cap = StrokeCap.Round, join = StrokeJoin.Round) + drawPath(path = path, brush = brush, style = strokeStyle) + drawPath(path = path, color = borderColor, style = Stroke(width = wall + strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round)) + + val glowAlpha = if (isDark) 0.2f else 0.12f + drawPath( + path = path, + color = accentColor.copy(alpha = glowAlpha), + style = Stroke(width = wall + glowExtra, cap = StrokeCap.Round, join = StrokeJoin.Round), + ) + } + + companion object { + private const val SMALL_DIGIT_DP = 108f + private const val SMALL_INFO_DP = 24f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt new file mode 100644 index 0000000000000..d67e8c23797fd --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt @@ -0,0 +1,910 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Point +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Typeface +import android.text.TextUtils +import android.text.format.DateFormat +import android.util.AttributeSet +import android.util.LruCache +import android.view.animation.PathInterpolator +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.basicMarquee +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.extensions.createBitmaps +import com.android.systemui.shared.clocks.extensions.scaledDimen +import com.android.systemui.shared.clocks.extensions.scaleRatio +import kotlin.math.min + +class BitmapDigitComposeClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + var faceStyle: ClockFaceStyle = ClockFaceStyle.DEFAULT + set(value) { + field = value + cachedConfig = BitmapFaceConfigs.getConfig(value) + (cachedConfig?.renderMode as? RenderMode.FontDigit)?.let { initFontMode(it) } + } + + private var cachedConfig: BitmapFaceConfig? = BitmapFaceConfigs.getConfig(faceStyle) + + private val typefaceCache = LruCache(16) + private val fontPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val fontWeights = IntArray(DIGIT_COUNT) { 0 } + private val digitAnimators = arrayOfNulls(DIGIT_COUNT) + private val digitAnimStarted = BooleanArray(DIGIT_COUNT) + private val digitRects = arrayOfNulls(DIGIT_COUNT) + private var isWakingUp = true + private var isKeyguardVisibleState = true + private var maxRadius = -1 + private var tapPos = Point(0, 0) + private var dozingAmountChanging = false + private var fidgetWeightAnimator: ValueAnimator? = null + private val weightVersion = mutableIntStateOf(0) + private val screenWidth = context.resources.displayMetrics.widthPixels + private val screenHeight = context.resources.displayMetrics.heightPixels + + private val forceResetRunnable = Runnable { + val mode = fontDigitMode ?: return@Runnable + if (!isWakingUp) return@Runnable + for (i in 0 until DIGIT_COUNT) { + animateDigitTo(i, mode.lsFontWeight, FORCE_ANIM_MS) + } + dozingAmountChanging = false + } + + private val fontDigitMode: RenderMode.FontDigit? + get() = (cachedConfig?.renderMode as? RenderMode.FontDigit) + + private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val handPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + strokeCap = Paint.Cap.ROUND + } + private val tickPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + + private fun initFontMode(mode: RenderMode.FontDigit) { + fontPaint.textSize = mode.fontSize * context.scaleRatio + for (i in 0 until DIGIT_COUNT) { + fontWeights[i] = mode.lsFontWeight + } + } + + private fun getCachedTypeface(weight: Int, fontPath: String): Typeface { + val snapped = (weight / WEIGHT_SNAP) * WEIGHT_SNAP + return typefaceCache.get(snapped) ?: Typeface.Builder(context.assets, fontPath) + .setFontVariationSettings("'wght' $snapped") + .build() + .also { typefaceCache.put(snapped, it) } + } + + private fun dateTextColor( + config: BitmapFaceConfig, + isDoze: Boolean, + screenOff: Boolean, + regionDark: Boolean, + ): Color { + val base = androidColorToComposeColor(config.clockColor(isDoze, screenOff, regionDark)) + return base.copy(alpha = if (isDoze) 0.6f else 0.8f) + } + + private fun animateDigitTo( + index: Int, + target: Int, + duration: Long = RIPPLE_ANIM_MS, + ) { + val current = fontWeights[index] + if (current == target) return + digitAnimators[index]?.cancel() + digitAnimators[index] = ValueAnimator.ofInt(current, target).apply { + this.duration = duration + interpolator = RIPPLE_INTERPOLATOR + addUpdateListener { + fontWeights[index] = it.animatedValue as Int + weightVersion.intValue++ + } + start() + } + } + + private fun cancelDigitAnimators() { + for (i in 0 until DIGIT_COUNT) { + digitAnimators[i]?.cancel() + digitAnimators[i] = null + } + } + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val config = cachedConfig ?: return super.clockHeightBase + val density = context.resources.displayMetrics.density + val contentHeight = when (val mode = config.renderMode) { + is RenderMode.BitmapDigit -> { + if (config.digitResIds.isEmpty()) return super.clockHeightBase + val d = ContextCompat.getDrawable(context, config.digitResIds[0]) + ?: return super.clockHeightBase + d.intrinsicHeight * scaleRatio * config.smallScaleMultiplier + } + is RenderMode.FontDigit -> { + fontPaint.textSize = mode.fontSize * scaleRatio + val metrics = fontPaint.fontMetrics + metrics.descent - metrics.ascent + } + is RenderMode.AnalogClock -> return super.clockHeightBase + } + return (contentHeight + (DATE_ROW_DP + config.dateSpacingDp + config.topPaddingDp + config.bottomPaddingDp) * density).toInt() + } + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val config = cachedConfig ?: return + when (config.renderMode) { + is RenderMode.BitmapDigit -> SmallBitmapContent(config) + is RenderMode.FontDigit -> SmallFontContent(config, config.renderMode) + is RenderMode.AnalogClock -> SmallAnalogContent(config) + } + } + + @Composable + private fun SmallBitmapContent(config: BitmapFaceConfig) { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + + val bitmaps = remember(config) { loadDigitBitmaps(config.digitResIds) } + val lightBitmaps = remember(config) { + config.digitLightResIds?.let { loadDigitBitmaps(it) } + } + + val scale = context.scaleRatio * config.smallScaleMultiplier + val spacing = if (config.negativeSpacing) { + -context.scaledDimen(config.digitSpacingRes) + } else { + context.scaledDimen(config.digitSpacingRes) + } + val overlapPadding = if (config.overlapSpacingRes != 0) context.scaledDimen(config.overlapSpacingRes) else 0f + + val dotSize = context.scaledDimen(config.dotSizeRes) + val dotMargin = context.scaledDimen(config.dotMarginRes) + val dotCenterMargin = context.scaledDimen(config.dotCenterMarginRes) + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + + val sampleBitmap = remember(config) { + ContextCompat.getDrawable(context, config.digitResIds[0]) + } + val canvasHeight = remember(sampleBitmap, scale) { + ((sampleBitmap?.intrinsicHeight ?: 0) * scale).coerceAtLeast(1f) + } + val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } + + val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + + SmallShell(config, dateColor) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp) + .then(fidgetTapModifier), + ) { + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val totalWidth = computeTotalWidth( + time, bitmaps, scale, spacing, overlapPadding, + config, dotCenterMargin + ) + if (totalWidth <= 0f) return@Canvas + + val finalWidth = minOf(totalWidth, size.width) + val startX = when { + isLeftAligned -> clockPaddingStart + isRightAligned -> size.width - clockPaddingStart - finalWidth + else -> (size.width - finalWidth) / 2f + } + + val minuteStartIndex = if (time.length == 4) 2 else 1 + val minuteTint = if (config.minuteAlpha < 1f && !isDoze && !screenOff) { + tintColor.copy(alpha = config.minuteAlpha) + } else { + tintColor + } + + var x = startX + time.forEachIndexed { index, char -> + val useLightVariant = shouldUseLightVariant(config, time, index, isDoze, screenOff) + val bitmapMap = if (useLightVariant && lightBitmaps != null) lightBitmaps else bitmaps + val bitmap = bitmapMap[char] ?: return@forEachIndexed + + val yOffset = (size.height - bitmap.height * scale) / 2f + + val digitTint = if (index >= minuteStartIndex) minuteTint else tintColor + drawScaledBitmap(bitmap, x, yOffset, scale, digitTint) + + x += bitmap.width * scale + x += getCustomSpacing(config, time, index, spacing, overlapPadding) + + if (shouldDrawSeparator(config, time, index)) { + x += drawDotSeparator( + x, yOffset, bitmap.height * scale, + dotSize, dotMargin, dotCenterMargin, tintColor + ) + } else if (index < time.lastIndex) { + x += spacing + } + } + } + } + } + + @Composable + private fun SmallFontContent(config: BitmapFaceConfig, mode: RenderMode.FontDigit) { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + + val scale = context.scaleRatio + val canvasHeight = remember(scale) { + fontPaint.textSize = mode.fontSize * scale + val metrics = fontPaint.fontMetrics + (metrics.descent - metrics.ascent).coerceAtLeast(1f) + } + val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } + val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + + SmallShell(config, dateColor) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp) + .then(fidgetTapModifier), + ) { + weightVersion.intValue + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val nativeCanvas = drawContext.canvas.nativeCanvas + fontPaint.textSize = mode.fontSize * scale + fontPaint.color = config.clockColor(isDoze, screenOff, regionDark) + + val displayTime = formatDisplayTime(time) + if (displayTime.isEmpty()) return@Canvas + + val digitWidths = FloatArray(displayTime.length) { i -> + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(i) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.measureText(displayTime[i].toString()) + } + val totalWidth = digitWidths.sum() + + val metrics = fontPaint.fontMetrics + val baselineY = (size.height / 2f) - ((metrics.top + metrics.bottom) / 2f) + + val indices = displayTime.indices.let { range -> + if (isWakingUp && !isSideAligned) range.reversed() else range.toList() + } + + val firstBounds = Rect() + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(0) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.getTextBounds(displayTime[0].toString(), 0, 1, firstBounds) + val leftBearing = firstBounds.left.toFloat() + + var x = when { + isLeftAligned -> clockPaddingStart - leftBearing + isRightAligned -> size.width - clockPaddingStart - totalWidth + isWakingUp -> (size.width / 2f) + (totalWidth / 2f) + else -> (size.width - totalWidth) / 2f + } + + for (i in indices) { + if (isWakingUp && !isSideAligned) { + x -= digitWidths[i] + } + + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(i) { mode.lsFontWeight }, mode.fontPath + ) + nativeCanvas.drawText(displayTime[i].toString(), x, baselineY, fontPaint) + + val charRect = Rect() + fontPaint.getTextBounds(displayTime[i].toString(), 0, 1, charRect) + charRect.offset(x.toInt(), baselineY.toInt()) + val loc = IntArray(2) + getLocationOnScreen(loc) + charRect.offset(loc[0], loc[1]) + if (i < DIGIT_COUNT) digitRects[i] = charRect + + if (!isWakingUp || isSideAligned) { + x += digitWidths[i] + } + } + } + } + } + + @Composable + private fun SmallAnalogContent(config: BitmapFaceConfig) { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + + SmallShell(config, dateColor) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .analogDrawModifier(config, time, isDoze, screenOff, regionDark), + ) + } + } + + @Composable + private fun SmallShell( + config: BitmapFaceConfig, + textColor: Color, + content: @Composable ColumnScope.() -> Unit, + ) { + val dateBelow by state.dateBelowState + val horizontalAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val topPadding = config.topPaddingDp.dp + val bottomPadding = config.bottomPaddingDp.dp + val dateSpacing = config.dateSpacingDp.dp + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + val datePaddingModifier = if (isSideAligned) Modifier.padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ) else Modifier + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = horizontalAlign, + ) { + if (!dateBelow) { + if (topPadding > 0.dp) Spacer(modifier = Modifier.height(topPadding)) + EnhancedDateArea(modifier = datePaddingModifier, textColor = textColor) + Spacer(modifier = Modifier.height(dateSpacing)) + } + + content() + + if (dateBelow) { + Spacer(modifier = Modifier.height(dateSpacing)) + EnhancedDateArea(modifier = datePaddingModifier, textColor = textColor) + if (topPadding > 0.dp) Spacer(modifier = Modifier.height(topPadding)) + } + + if (!dateBelow && bottomPadding > 0.dp) Spacer(modifier = Modifier.height(bottomPadding)) + } + } + + @Composable + private fun LargeContent() { + val config = cachedConfig ?: return + when (config.renderMode) { + is RenderMode.BitmapDigit -> LargeBitmapContent(config) + is RenderMode.FontDigit -> LargeFontContent(config, config.renderMode) + is RenderMode.AnalogClock -> LargeAnalogContent(config) + } + } + + @Composable + private fun LargeBitmapContent(config: BitmapFaceConfig) { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + val bitmaps = remember(config) { + loadDigitBitmaps(config.digitLargeResIds ?: config.digitResIds) + } + + val baseScale = minOf(context.scaleRatio, MAX_TABLET_SCALE) + val scale = baseScale * config.largeScaleMultiplier + val digitSpacing = context.scaledDimen(config.largeDigitSpacingRes) + val lineSpacing = context.scaledDimen(config.largeLineSpacingRes) + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + + val finalSpacing = if (config.negativeSpacing) { + -context.scaledDimen(config.largeDigitSpacingRes) + } else { + digitSpacing + } + + val sampleBmp = remember(config) { bitmaps['0'] } + val digitHeightPx = (sampleBmp?.height ?: 0) * scale + val canvasHeightDp = with(LocalDensity.current) { + (digitHeightPx * 2 + lineSpacing).toDp() + } + + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp) + .then(fidgetTapModifier), + ) { + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val (hours, minutes) = splitTimeLines(time) + if (hours.isEmpty()) return@Canvas + + val hoursWidth = computeLineWidth(hours, bitmaps, scale, finalSpacing) + val minutesWidth = computeLineWidth(minutes, bitmaps, scale, finalSpacing) + + val sampleBitmap = bitmaps['0'] ?: return@Canvas + val digitHeight = sampleBitmap.height * scale + + val hoursX = (size.width - hoursWidth) / 2f + val minutesX = (size.width - minutesWidth) / 2f + + drawDigitLine(hours, bitmaps, scale, hoursX, 0f, finalSpacing, tintColor) + drawDigitLine(minutes, bitmaps, scale, minutesX, digitHeight + lineSpacing, finalSpacing, tintColor) + } + + Spacer(modifier = Modifier.height(16.dp)) + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun LargeFontContent(config: BitmapFaceConfig, mode: RenderMode.FontDigit) { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + + val scale = context.scaleRatio + val largeFontSize = mode.fontSize * scale * mode.largeScale + val canvasHeightDp = remember(largeFontSize, mode.lineSpacing, scale) { + val tempPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + textSize = largeFontSize + typeface = getCachedTypeface(mode.lsFontWeight, mode.fontPath) + } + val metrics = tempPaint.fontMetrics + val lineHeight = metrics.descent - metrics.ascent + val totalPx = lineHeight * 2 + mode.lineSpacing * scale + totalPx / context.resources.displayMetrics.density + } + + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp.dp) + .then(fidgetTapModifier), + ) { + weightVersion.intValue + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val nativeCanvas = drawContext.canvas.nativeCanvas + fontPaint.textSize = largeFontSize + fontPaint.color = config.clockColor(isDoze, screenOff, regionDark) + + val displayTime = formatDisplayTime(time) + if (displayTime.isEmpty()) return@Canvas + + val (hours, minutes) = splitTimeLines(displayTime) + if (hours.isEmpty()) return@Canvas + + val metrics = fontPaint.fontMetrics + val lineHeight = metrics.descent - metrics.ascent + val startY = -metrics.ascent + + var globalIdx = 0 + var hx = 0f + val hourWidths = FloatArray(hours.length) { i -> + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.measureText(hours[i].toString()) + } + val hoursWidth = hourWidths.sum() + hx = (size.width - hoursWidth) / 2f + for (i in hours.indices) { + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath + ) + nativeCanvas.drawText(hours[i].toString(), hx, startY, fontPaint) + hx += hourWidths[i] + } + globalIdx += hours.length + + val minWidths = FloatArray(minutes.length) { i -> + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.measureText(minutes[i].toString()) + } + val minutesWidth = minWidths.sum() + var mx = (size.width - minutesWidth) / 2f + val minY = startY + lineHeight + mode.lineSpacing * scale + for (i in minutes.indices) { + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath + ) + nativeCanvas.drawText(minutes[i].toString(), mx, minY, fontPaint) + mx += minWidths[i] + } + } + + Spacer(modifier = Modifier.height(16.dp)) + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun LargeAnalogContent(config: BitmapFaceConfig) { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Box(modifier = Modifier.fillMaxSize().analogDrawModifier(config, time, isDoze, screenOff, regionDark)) + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 24.dp), + contentAlignment = Alignment.Center, + ) { + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + } + + private fun loadDigitBitmaps(resIds: IntArray): Map { + val result = mutableMapOf() + resIds.forEachIndexed { index, resId -> + ContextCompat.getDrawable(context, resId)?.toBitmap()?.let { bmp -> + result['0' + index] = bmp + } + } + return result + } + + private fun Modifier.analogDrawModifier( + config: BitmapFaceConfig, + time: String, + isDoze: Boolean, + screenOff: Boolean, + regionDark: Boolean, + ): Modifier = drawWithContent { + drawContent() + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@drawWithContent + + val canvas = drawContext.canvas.nativeCanvas + val clockSz = min(size.width, size.height) + val cx = size.width / 2f + val cy = size.height / 2f + val color = config.clockColor(isDoze, screenOff, regionDark) + val highlightColor = ContextCompat.getColor(context, R.color.clock_dot_color) + val scale = context.scaleRatio + val dotSize = context.scaledDimen(R.dimen.dot_size) + val handSize = context.scaledDimen(R.dimen.clock_hand_size) + + val bitmaps = createBitmaps(context, config.tickResIds) + val tick = bitmaps.getOrNull(0) + val tickLight = bitmaps.getOrNull(1) + + if (tick != null && tickLight != null) { + tickPaint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + val tw = tick.width * scale + val left = cx - tw / 2f + val top = cy - tick.height * scale / 2f + val matrix = Matrix().apply { + postScale(scale, scale) + postTranslate(left, top) + } + if (!isDoze && !screenOff) { + canvas.drawBitmap(tickLight, matrix, tickPaint) + } + canvas.drawBitmap(tick, matrix, tickPaint) + } + + dotPaint.color = color + dotPaint.xfermode = null + val outerR = dotSize * 1.5f / 2f + canvas.drawOval( + RectF(cx - outerR, cy - outerR, cx + outerR, cy + outerR), + dotPaint, + ) + + try { + val seconds = time.takeLast(2).toInt() + val minutes = time.dropLast(2).takeLast(2).toInt() + val hours = time.dropLast(4).toInt() + val hourAngle = (hours + minutes / 60f) * 5 + val minuteAngle = minutes + seconds / 60f + + drawClockHand(canvas, handPaint, hourAngle, handSize * 2, 0.22f, 0.38f, color, cx, cy, clockSz) + drawClockHand(canvas, handPaint, minuteAngle, handSize, 0.22f, 0.42f, color, cx, cy, clockSz) + drawClockHand(canvas, handPaint, seconds.toFloat(), handSize / 2, 0.19f, 0.42f, highlightColor, cx, cy, clockSz) + } catch (_: NumberFormatException) {} + + if (!isDoze && !screenOff) { + dotPaint.color = highlightColor + dotPaint.xfermode = null + } else { + dotPaint.color = android.graphics.Color.TRANSPARENT + dotPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } + val innerR = dotSize / 2f + canvas.drawOval( + RectF(cx - innerR, cy - innerR, cx + innerR, cy + innerR), + dotPaint, + ) + } + + private fun formatDisplayTime(time: String): String { + if (time.length < 3) return time + if (!DateFormat.is24HourFormat(context)) { + val hour = time.substring(0, 2).toIntOrNull() ?: return time + if (hour < 10) return time.drop(1) + } + return time + } + + + + override fun onDozeAmountChanged(linear: Float, eased: Float) { + super.onDozeAmountChanged(linear, eased) + val mode = fontDigitMode ?: return + dozingAmountChanging = true + if (!isWakingUp) handler?.removeCallbacks(forceResetRunnable) + + val target = if (linear == 0f) mode.lsFontWeight + else if (linear == 1f) mode.aodFontWeight + else null + + if (target != null) { + for (i in 0 until DIGIT_COUNT) { + val anim = digitAnimators[i] + if (anim != null && anim.isRunning) continue + if (fontWeights[i] != target) { + animateDigitTo(i, target, SETTLE_ANIM_MS) + } + } + dozingAmountChanging = false + return + } + + if (!isWakingUp && !isKeyguardVisibleState) return + + val hasDigitRects = digitRects.any { it != null } + if (!hasDigitRects) { + val weight = if (isWakingUp) { + (mode.aodFontWeight + (mode.lsFontWeight - mode.aodFontWeight) * (1f - linear)).toInt() + } else { + (mode.lsFontWeight - (mode.lsFontWeight - mode.aodFontWeight) * linear).toInt() + } + for (i in 0 until DIGIT_COUNT) { + fontWeights[i] = weight + } + weightVersion.intValue++ + return + } + + val targetWeight = if (isWakingUp) mode.lsFontWeight else mode.aodFontWeight + val radius = maxRadius * (1f - linear) + for (i in 0 until DIGIT_COUNT) { + val rect = digitRects[i] ?: continue + val reached = if (isWakingUp) { + circleIntersectsRect(tapPos.x.toFloat(), tapPos.y.toFloat(), radius, rect) + } else { + circleNotFullyContainsRect(tapPos.x.toFloat(), tapPos.y.toFloat(), radius, rect) + } + if (reached && !digitAnimStarted[i]) { + animateDigitTo(i, targetWeight, RIPPLE_ANIM_MS) + digitAnimStarted[i] = true + } + } + } + + override fun onStartedWakingUp() { + super.onStartedWakingUp() + if (fontDigitMode == null) return + isWakingUp = true + dozingAmountChanging = false + handler?.postDelayed(forceResetRunnable, FORCE_RESET_DELAY_MS) + } + + override fun onStartedGoingToSleep(isKeyguardVisible: Boolean) { + val mode = fontDigitMode ?: return + isWakingUp = false + dozingAmountChanging = false + isKeyguardVisibleState = isKeyguardVisible + if (!isKeyguardVisible) { + for (i in 0 until DIGIT_COUNT) { + animateDigitTo(i, mode.aodFontWeight, SETTLE_ANIM_MS) + } + } + } + + override fun onWakefulnessStateChanged(isWakingUp: Boolean, tapPosition: Point?) { + if (fontDigitMode == null) return + this.isWakingUp = isWakingUp + val pos = tapPosition ?: Point(screenWidth / 2, screenHeight / 2) + digitAnimStarted.fill(false) + cancelDigitAnimators() + maxRadius = maxOf( + pos.x, screenWidth - pos.x, + pos.y, screenHeight - pos.y + ) + tapPos = pos + } + + override fun animateFidgetTap(x: Float, y: Float) { + super.animateFidgetTap(x, y) + animateWeightPulse() + } + + override fun onChargeAnimation() { + animateWeightPulse() + } + + private fun animateWeightPulse() { + val mode = fontDigitMode ?: return + fidgetWeightAnimator?.cancel() + + val squeeze = ValueAnimator.ofInt(mode.lsFontWeight, FIDGET_THIN_WEIGHT).apply { + duration = FIDGET_WEIGHT_DURATION + interpolator = FIDGET_INTERPOLATOR + addUpdateListener { anim -> + val w = anim.animatedValue as Int + for (i in 0 until DIGIT_COUNT) fontWeights[i] = w + weightVersion.intValue++ + } + } + val restore = ValueAnimator.ofInt(FIDGET_THIN_WEIGHT, mode.lsFontWeight).apply { + duration = FIDGET_WEIGHT_DURATION + interpolator = FIDGET_INTERPOLATOR + addUpdateListener { anim -> + val w = anim.animatedValue as Int + for (i in 0 until DIGIT_COUNT) fontWeights[i] = w + weightVersion.intValue++ + } + } + + fidgetWeightAnimator = squeeze + AnimatorSet().apply { + playSequentially(squeeze, restore) + start() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + fidgetWeightAnimator?.cancel() + handler?.removeCallbacks(forceResetRunnable) + cancelDigitAnimators() + } + + override fun getTag(): String = + if (isLargeClock) "BitmapDigitComposeLargeClockView" else "BitmapDigitComposeClockView" + + companion object { + private const val MAX_TABLET_SCALE = 1.2f + private const val DATE_ROW_DP = 24f + private const val DIGIT_COUNT = 4 + private const val WEIGHT_SNAP = 10 + private const val RIPPLE_ANIM_MS = 500L + private const val SETTLE_ANIM_MS = 300L + private const val FORCE_ANIM_MS = 350L + private const val FORCE_RESET_DELAY_MS = 800L + private const val FIDGET_THIN_WEIGHT = 300 + private const val FIDGET_WEIGHT_DURATION = 250L + private val RIPPLE_INTERPOLATOR = PathInterpolator(0.6f, 0f, 0.2f, 1f) + private val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt new file mode 100644 index 0000000000000..a454ba0ebc61c --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.graphics.Bitmap +import android.graphics.Paint +import android.graphics.Rect +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import kotlin.math.cos +import kotlin.math.sin + +internal fun DrawScope.drawScaledBitmap( + bitmap: Bitmap, + x: Float, + y: Float, + scale: Float, + tint: Color, +) { + val imageBitmap = bitmap.asImageBitmap() + val dstWidth = (bitmap.width * scale).toInt() + val dstHeight = (bitmap.height * scale).toInt() + drawImage( + image = imageBitmap, + srcOffset = IntOffset.Zero, + srcSize = IntSize(bitmap.width, bitmap.height), + dstOffset = IntOffset(x.toInt(), y.toInt()), + dstSize = IntSize(dstWidth, dstHeight), + colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn), + ) +} + +internal fun DrawScope.drawDotSeparator( + x: Float, + baseY: Float, + digitHeight: Float, + dotSize: Float, + dotMargin: Float, + dotCenterMargin: Float, + tint: Color, +): Float { + val centerX = x + dotCenterMargin + val radius = dotSize / 2 + val dotY = baseY + digitHeight / 2 - dotMargin / 2 + + drawOval( + color = tint, + topLeft = Offset(centerX - radius, dotY - radius), + size = Size(dotSize, dotSize), + ) + drawOval( + color = tint, + topLeft = Offset(centerX - radius, dotY + dotMargin - radius), + size = Size(dotSize, dotSize), + ) + + return dotCenterMargin * 2 +} + +internal fun DrawScope.drawDigitLine( + digits: String, + bitmaps: Map, + scale: Float, + startX: Float, + y: Float, + spacing: Float, + tint: Color, +) { + var x = startX + digits.forEach { char -> + val bitmap = bitmaps[char] ?: return@forEach + drawScaledBitmap(bitmap, x, y, scale, tint) + x += bitmap.width * scale + spacing + } +} + +internal fun drawClockHand( + canvas: android.graphics.Canvas, + paint: Paint, + position: Float, + thickness: Float, + startMul: Float, + endMul: Float, + color: Int, + cx: Float, + cy: Float, + clockSize: Float, +) { + val angleRad = Math.toRadians((position * 6 - 90).toDouble()) + val startLen = startMul * clockSize + val endLen = endMul * clockSize + val sx = cx - cos(angleRad).toFloat() * startLen + val sy = cy - sin(angleRad).toFloat() * startLen + val ex = cx + cos(angleRad).toFloat() * endLen + val ey = cy + sin(angleRad).toFloat() * endLen + paint.color = color + paint.strokeWidth = thickness + canvas.drawLine(sx, sy, ex, ey, paint) +} + +internal fun shouldDrawSeparator(config: BitmapFaceConfig, time: String, index: Int): Boolean { + if (!config.hasSeparator) return false + return when (time.length) { + 4 -> index == 1 + 3 -> index == 0 + else -> false + } +} + +internal fun shouldUseLightVariant( + config: BitmapFaceConfig, + time: String, + index: Int, + isDoze: Boolean, + screenOff: Boolean, +): Boolean { + if (!config.hasLightVariants) return false + if (isDoze || screenOff) return false + return when (config.lightVariantRule) { + BitmapFaceConfig.LightVariantRule.NONE -> false + BitmapFaceConfig.LightVariantRule.SECOND_HALF -> when (time.length) { + 3 -> index >= 1 + 4 -> index >= 2 + else -> false + } + } +} + +internal fun getCustomSpacing( + config: BitmapFaceConfig, + time: String, + index: Int, + baseSpacing: Float, + overlapPadding: Float, +): Float { + if (config.overlapPairs.isNotEmpty()) { + return getNTypeCustomSpacing(config, time, index, overlapPadding) + } + if (config.lightVariantRule == BitmapFaceConfig.LightVariantRule.SECOND_HALF) { + return getNDotCustomSpacing(time, index, baseSpacing, overlapPadding) + } + return 0f +} + +private fun getNTypeCustomSpacing( + config: BitmapFaceConfig, + time: String, + index: Int, + overlapPadding: Float, +): Float { + val str = when { + time.length == 4 && (index == 0 || index == 2) -> time.substring(index, index + 2) + time.length == 3 && index == 1 -> time.substring(index) + else -> "" + } + val multiplier = config.overlapPairs[str] ?: return 0f + return -overlapPadding * multiplier +} + +private fun getNDotCustomSpacing( + time: String, + index: Int, + clockPadding: Float, + overlapPadding: Float, +): Float { + return when (time.length) { + 3 -> when (index) { + 0 -> clockPadding + 1 -> { + val secondHalf = time.substring(1) + if ('1' !in secondHalf) overlapPadding else clockPadding + } + else -> 0f + } + 4 -> when (index) { + 0 -> { + val firstHalf = time.substring(0, 2) + if ('1' in firstHalf) clockPadding else overlapPadding + } + 1 -> clockPadding + 2 -> { + val secondHalf = time.substring(2) + if ('1' !in secondHalf) overlapPadding else clockPadding + } + else -> 0f + } + else -> 0f + } +} + +internal fun computeTotalWidth( + time: String, + bitmaps: Map, + scale: Float, + spacing: Float, + overlapPadding: Float, + config: BitmapFaceConfig, + dotCenterMargin: Float, +): Float { + var total = 0f + time.forEachIndexed { index, char -> + val bmp = bitmaps[char] ?: return@forEachIndexed + total += bmp.width * scale + total += getCustomSpacing(config, time, index, spacing, overlapPadding) + if (shouldDrawSeparator(config, time, index)) { + total += dotCenterMargin * 2 + } else if (index < time.lastIndex) { + total += spacing + } + } + return total +} + +internal fun computeLineWidth( + digits: String, + bitmaps: Map, + scale: Float, + spacing: Float, +): Float { + var total = 0f + digits.forEachIndexed { index, char -> + val bmp = bitmaps[char] ?: return@forEachIndexed + total += bmp.width * scale + if (index < digits.lastIndex) { + total += spacing + } + } + return total +} + +internal fun circleIntersectsRect(cx: Float, cy: Float, radius: Float, rect: Rect): Boolean { + val dx = cx - cx.coerceIn(rect.left.toFloat(), rect.right.toFloat()) + val dy = cy - cy.coerceIn(rect.top.toFloat(), rect.bottom.toFloat()) + return (dx * dx + dy * dy) <= radius * radius +} + +internal fun circleNotFullyContainsRect(cx: Float, cy: Float, radius: Float, rect: Rect): Boolean { + val r2 = radius * radius + return arrayOf( + rect.left.toFloat() to rect.top.toFloat(), + rect.right.toFloat() to rect.top.toFloat(), + rect.left.toFloat() to rect.bottom.toFloat(), + rect.right.toFloat() to rect.bottom.toFloat(), + ).any { (px, py) -> + val dx = px - cx + val dy = py - cy + (dx * dx + dy * dy) > r2 + } +} + +internal fun androidColorToComposeColor(color: Int): Color = Color(color) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt new file mode 100644 index 0000000000000..a444892e508f0 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt @@ -0,0 +1,235 @@ +package com.android.systemui.shared.clocks.view + +import android.animation.ValueAnimator +import android.graphics.* +import android.view.View +import com.android.app.animation.Interpolators +import com.android.systemui.shared.clocks.DepthWallpaperProvider +import android.content.res.Resources +import android.provider.Settings +import android.util.DisplayMetrics + +class ClockDepthController(private val view: View) { + + var enabled = true + + private var subjectPath: Path? = null + private var pathAspect: Float = 1f + private var depthActive = false + private var depthVisible = true + private var maskAlpha = 0f + private var revealProgress = 0f + private var maskAnimator: ValueAnimator? = null + + private val transformedPath = Path() + private val pathMatrix = Matrix() + private val location = IntArray(2) + private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + style = Paint.Style.FILL + } + + private val wallpaperMaxScale: Float = try { + val id = Resources.getSystem().getIdentifier( + "config_wallpaperMaxScale", "dimen", "android" + ) + if (id != 0) Resources.getSystem().getFloat(id) else 1.1f + } catch (_: Exception) { 1.1f } + + private fun getZoom(): Float { + return try { + val str = Settings.Secure.getString( + view.context.contentResolver, "ax_depth_zoom" + ) + str?.toFloatOrNull() ?: wallpaperMaxScale + } catch (_: Exception) { wallpaperMaxScale } + } + + private val listener = object : DepthWallpaperProvider.DepthMaskListener { + override fun onDepthDataChanged(path: Path?, pathAspect: Float) { + val wasActive = depthActive + subjectPath = path + this@ClockDepthController.pathAspect = pathAspect + depthActive = path != null && !path.isEmpty + + if (!depthVisible) { + view.postInvalidateOnAnimation() + return + } + + maskAnimator?.cancel() + if (depthActive && !wasActive) { + animateReveal() + } else if (!depthActive && wasActive) { + animateHide() + } else { + view.postInvalidateOnAnimation() + } + } + } + + fun onAttached() { + if (!enabled) return + DepthWallpaperProvider.init(view.context) + DepthWallpaperProvider.addListener(listener) + } + + fun onDetached() { + maskAnimator?.cancel() + maskAnimator = null + maskAlpha = 0f + revealProgress = 0f + DepthWallpaperProvider.removeListener(listener) + subjectPath = null + depthActive = false + } + + fun setDepthVisible(visible: Boolean) { + if (depthVisible == visible) return + depthVisible = visible + + if (!depthActive) return + + maskAnimator?.cancel() + if (visible) { + animateReveal() + } else { + maskAlpha = 0f + revealProgress = 0f + view.postInvalidateOnAnimation() + } + } + + fun shouldApplyDepth(): Boolean { + val path = subjectPath + return depthActive && depthVisible && path != null && !path.isEmpty && maskAlpha > 0f + } + + fun drawWithDepth(canvas: Canvas, drawSuper: (Canvas) -> Unit) { + val path = subjectPath ?: run { drawSuper(canvas); return } + + view.getLocationOnScreen(location) + val viewX = location[0].toFloat() + val viewY = location[1].toFloat() + + val realMetrics = DisplayMetrics() + view.context.display?.getRealMetrics(realMetrics) + val screenW = realMetrics.widthPixels.toFloat() + val screenH = realMetrics.heightPixels.toFloat() + + val zoom = getZoom() + + val wallAspect = pathAspect + val screenAspect = screenW / screenH + val cropLeft: Float + val cropTop: Float + val visibleW: Float + val visibleH: Float + if (wallAspect > screenAspect) { + visibleW = (screenAspect / wallAspect) * 10000f + visibleH = 10000f + cropLeft = (10000f - visibleW) / 2f + cropTop = 0f + } else { + visibleW = 10000f + visibleH = (wallAspect / screenAspect) * 10000f + cropLeft = 0f + cropTop = (10000f - visibleH) / 2f + } + + pathMatrix.reset() + pathMatrix.setTranslate(-cropLeft, -cropTop) + pathMatrix.postScale(screenW / visibleW, screenH / visibleH) + if (zoom != 1f) { + pathMatrix.postScale(zoom, zoom, screenW / 2f, screenH / 2f) + } + pathMatrix.postTranslate(-viewX, -viewY) + + val viewScaleX = view.scaleX + val viewScaleY = view.scaleY + if (viewScaleX != 1f || viewScaleY != 1f) { + pathMatrix.postScale(1f / viewScaleX, 1f / viewScaleY) + } + + transformedPath.reset() + path.transform(pathMatrix, transformedPath) + + val pathBounds = RectF() + transformedPath.computeBounds(pathBounds, true) + + val layerLeft = -viewX + val layerTop = -viewY + val layerRight = screenW - viewX + val layerBottom = screenH - viewY + val layerRect = RectF(layerLeft, layerTop, layerRight, layerBottom) + + if (!RectF.intersects(pathBounds, layerRect)) { + drawSuper(canvas) + return + } + + if (revealProgress >= 1f && maskAlpha >= 1f) { + canvas.save() + canvas.clipOutPath(transformedPath) + drawSuper(canvas) + canvas.restore() + } else { + if (revealProgress < 1f) { + val bounds = RectF() + transformedPath.computeBounds(bounds, false) + val revealMatrix = Matrix() + val s = REVEAL_MIN_SCALE + (1f - REVEAL_MIN_SCALE) * revealProgress + revealMatrix.setScale(s, s, bounds.centerX(), bounds.centerY()) + revealMatrix.postTranslate(0f, PARALLAX_PX * (1f - revealProgress)) + transformedPath.transform(revealMatrix) + } + + val layerCount = canvas.saveLayer(layerLeft, layerTop, layerRight, layerBottom, null) + drawSuper(canvas) + + maskPaint.alpha = (maskAlpha * 255f).toInt().coerceIn(0, 255) + maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + canvas.drawPath(transformedPath, maskPaint) + maskPaint.xfermode = null + maskPaint.alpha = 255 + + canvas.restoreToCount(layerCount) + } + } + + private fun animateReveal() { + maskAnimator?.cancel() + maskAnimator = ValueAnimator.ofFloat(maskAlpha, 1f).apply { + duration = REVEAL_DURATION + interpolator = Interpolators.EMPHASIZED_DECELERATE + addUpdateListener { + val v = it.animatedValue as Float + maskAlpha = v + revealProgress = v + view.postInvalidateOnAnimation() + } + start() + } + } + + private fun animateHide() { + maskAnimator?.cancel() + maskAnimator = ValueAnimator.ofFloat(maskAlpha, 0f).apply { + duration = REVEAL_DURATION / 2 + interpolator = Interpolators.EMPHASIZED_ACCELERATE + addUpdateListener { + val v = it.animatedValue as Float + maskAlpha = v + revealProgress = v + view.postInvalidateOnAnimation() + } + start() + } + } + + private companion object { + const val REVEAL_DURATION = 600L + const val REVEAL_MIN_SCALE = 0.97f + const val PARALLAX_PX = 24f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockFaceStyle.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockFaceStyle.kt new file mode 100644 index 0000000000000..2387db353a7ce --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockFaceStyle.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.graphics.Color +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.ClockSettingsRepository +import kotlin.math.roundToInt + +enum class ClockFaceStyle(val key: String) { + NTYPE("ntype"), + NDOT("ndot"), + LONDON_UG("london_ug"), + SPACE_AGE("space_age"), + POLYLINE("polyline"), + CYBERPUNK("cyberpunk"), + AXION_AGE("axion_age"), + SEGMENTS("segments"), + GRAPHIC("graphic"); + + val isComposeClock: Boolean + get() = this != CYBERPUNK && this != AXION_AGE + + val needsPerSecondTick: Boolean + get() = this == GRAPHIC + + companion object { + const val SETTINGS_KEY = "ax_clock_compose_face" + val DEFAULT = NTYPE + + fun fromKey(key: String?): ClockFaceStyle = + entries.firstOrNull { it.key == key } ?: DEFAULT + } +} + +sealed interface RenderMode { + data object BitmapDigit : RenderMode + data class FontDigit( + val fontPath: String, + val fontSize: Float, + val lsFontWeight: Int, + val aodFontWeight: Int, + val largeScale: Float = 1.85f, + val lineSpacing: Float = 16f, + ) : RenderMode + data object AnalogClock : RenderMode +} + +data class BitmapFaceConfig( + val renderMode: RenderMode = RenderMode.BitmapDigit, + val digitResIds: IntArray = intArrayOf(), + val digitLightResIds: IntArray? = null, + val digitLargeResIds: IntArray? = null, + val digitLightLargeResIds: IntArray? = null, + val digitSpacingRes: Int = R.dimen.clock_padding, + val topMarginRes: Int = R.dimen.clock_center_date_margin_top, + val clockOffsetRes: Int = 0, + val smallScaleMultiplier: Float = 1f, + val largeScaleMultiplier: Float = 1.8f, + val largeDigitSpacingRes: Int = R.dimen.large_clock_digit_spacing, + val largeLineSpacingRes: Int = R.dimen.large_clock_line_spacing, + val hasSeparator: Boolean = false, + val negativeSpacing: Boolean = false, + val hasLightVariants: Boolean = false, + val colorMode: ColorMode = ColorMode.STANDARD, + val overlapSpacingRes: Int = 0, + val overlapPairs: Map = emptyMap(), + val lightVariantRule: LightVariantRule = LightVariantRule.NONE, + val dotSizeRes: Int = R.dimen.dot_size, + val dotMarginRes: Int = R.dimen.dot_margin, + val dotCenterMarginRes: Int = R.dimen.dot_margin_center, + val minuteAlpha: Float = 1f, + val dateSpacingDp: Float = DATE_SPACING_DEFAULT, + val topPaddingDp: Float = 0f, + val bottomPaddingDp: Float = 0f, + val tickResIds: IntArray = intArrayOf(), +) { + enum class ColorMode { STANDARD, LONDON_UG } + enum class LightVariantRule { NONE, SECOND_HALF } + + companion object { + const val DATE_SPACING_DEFAULT = 24f + } + + fun clockColor(isDoze: Boolean, isScreenOff: Boolean, isRegionDark: Boolean): Int { + if (isDoze || isScreenOff) return Color.WHITE + ClockSettingsRepository.clockColorOverride.value?.let { return it } + return when (colorMode) { + ColorMode.STANDARD -> { + if (isRegionDark) Color.WHITE else Color.BLACK + } + ColorMode.LONDON_UG -> { + if (isRegionDark) Color.WHITE + else Color.argb((0.6f * 255).roundToInt(), 0, 0, 0) + } + } + } +} + +object BitmapFaceConfigs { + val configs: Map = mapOf( + ClockFaceStyle.NTYPE to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.ntype_0, R.drawable.ntype_1, R.drawable.ntype_2, + R.drawable.ntype_3, R.drawable.ntype_4, R.drawable.ntype_5, + R.drawable.ntype_6, R.drawable.ntype_7, R.drawable.ntype_8, + R.drawable.ntype_9 + ), + digitLargeResIds = intArrayOf( + R.drawable.ntype_0_16b, R.drawable.ntype_1_16b, R.drawable.ntype_2_16b, + R.drawable.ntype_3_16b, R.drawable.ntype_4_16b, R.drawable.ntype_5_16b, + R.drawable.ntype_6_16b, R.drawable.ntype_7_16b, R.drawable.ntype_8_16b, + R.drawable.ntype_9_16b + ), + topMarginRes = R.dimen.bitmap_digit_clocks_margin_top_v2, + clockOffsetRes = R.dimen.clock_offset, + hasSeparator = true, + largeScaleMultiplier = 1.1f, + overlapSpacingRes = R.dimen.overlap_small_padding, + overlapPairs = mapOf("14" to 6f, "17" to 1f, "19" to 2f), + dateSpacingDp = 0f, + ), + ClockFaceStyle.NDOT to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.ndot_0, R.drawable.ndot_1, R.drawable.ndot_2, + R.drawable.ndot_3, R.drawable.ndot_4, R.drawable.ndot_5, + R.drawable.ndot_6, R.drawable.ndot_7, R.drawable.ndot_8, + R.drawable.ndot_9 + ), + digitLightResIds = intArrayOf( + R.drawable.ndot_0_light, R.drawable.ndot_1_light, R.drawable.ndot_2_light, + R.drawable.ndot_3_light, R.drawable.ndot_4_light, R.drawable.ndot_5_light, + R.drawable.ndot_6_light, R.drawable.ndot_7_light, R.drawable.ndot_8_light, + R.drawable.ndot_9_light + ), + topMarginRes = R.dimen.bitmap_digit_clocks_margin_top_v2, + clockOffsetRes = R.dimen.ndot_clock_offset, + hasLightVariants = true, + largeScaleMultiplier = 1.5f, + overlapSpacingRes = R.dimen.overlap_padding, + lightVariantRule = BitmapFaceConfig.LightVariantRule.SECOND_HALF, + minuteAlpha = 0.6f, + dateSpacingDp = 0f, + ), + ClockFaceStyle.LONDON_UG to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.london_ug_0, R.drawable.london_ug_1, R.drawable.london_ug_2, + R.drawable.london_ug_3, R.drawable.london_ug_4, R.drawable.london_ug_5, + R.drawable.london_ug_6, R.drawable.london_ug_7, R.drawable.london_ug_8, + R.drawable.london_ug_9 + ), + topMarginRes = R.dimen.bitmap_digit_clocks_margin_top_v2, + smallScaleMultiplier = 56f / 32f, + largeScaleMultiplier = 2.2f, + colorMode = BitmapFaceConfig.ColorMode.LONDON_UG, + dateSpacingDp = 20f, + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ClockFaceStyle.SPACE_AGE to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.space_age_0, R.drawable.space_age_1, R.drawable.space_age_2, + R.drawable.space_age_3, R.drawable.space_age_4, R.drawable.space_age_5, + R.drawable.space_age_6, R.drawable.space_age_7, R.drawable.space_age_8, + R.drawable.space_age_9 + ), + digitSpacingRes = R.dimen.space_age_clock_sticky_offset, + negativeSpacing = true, + smallScaleMultiplier = 0.85f, + largeScaleMultiplier = 1.5f, + dateSpacingDp = 20f, + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ClockFaceStyle.POLYLINE to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.polyline_0, R.drawable.polyline_1, R.drawable.polyline_2, + R.drawable.polyline_3, R.drawable.polyline_4, R.drawable.polyline_5, + R.drawable.polyline_6, R.drawable.polyline_7, R.drawable.polyline_8, + R.drawable.polyline_9 + ), + digitSpacingRes = R.dimen.polyline_clock_padding, + largeScaleMultiplier = 1.5f, + dateSpacingDp = 20f, + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ClockFaceStyle.SEGMENTS to BitmapFaceConfig( + renderMode = RenderMode.FontDigit( + fontPath = "fonts/nsegments_vf.otf", + fontSize = 280f, + lsFontWeight = 700, + aodFontWeight = 100, + largeScale = 1.85f, + lineSpacing = 16f, + ), + dateSpacingDp = 0f, + topPaddingDp = 16f, + ), + ClockFaceStyle.GRAPHIC to BitmapFaceConfig( + renderMode = RenderMode.AnalogClock, + tickResIds = intArrayOf(R.drawable.graphic_tick, R.drawable.graphic_tick_light), + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ) + + fun getConfig(style: ClockFaceStyle): BitmapFaceConfig? = configs[style] +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt new file mode 100644 index 0000000000000..725c94d97ab71 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +class CyberpunkClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override val useGlitchInteraction: Boolean = true + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze) = rememberClockState() + val fidget by state.fidgetTrigger + + val cpYellow = Color(0xFFFCEE0A) + val cpCyan = Color(0xFF00F0FF) + val cpRed = Color(0xFFFF5E5E) + + val primaryTimeColor = if (isDoze) Color.White else cpYellow + val frameColor = if (isDoze) Color.White else cpYellow + val textColor = if (isDoze) Color.White else cpCyan + + val statColor1 = if (isDoze) Color.White else cpRed + val statColor2 = if (isDoze) Color.White else cpCyan + val statColor3 = if (isDoze) Color.White else cpYellow + + val glitchProgress = remember { Animatable(0f) } + + LaunchedEffect(fidget) { + if (fidget > 0) { + glitchProgress.snapTo(1f) + glitchProgress.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + } + } + + val progress = glitchProgress.value + val glitchSeed = remember(fidget) { kotlin.random.Random.nextFloat() } + val contentAlign = when { + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + Box( + modifier = Modifier.fillMaxSize().padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + contentAlignment = contentAlign + ) { + Box( + modifier = Modifier + .then(fidgetTapModifier) + .graphicsLayer { + if (progress > 0f) { + translationX = (if (glitchSeed > 0.5f) 8f else -8f) * progress + alpha = if (glitchSeed > 0.7f) 0.4f else 1f + } + } + ) { + if (progress > 0.05f && !isDoze) { + Box( + modifier = Modifier + .offset(x = (-6).dp * progress * 1.5f, y = 2.dp * progress) + .graphicsLayer { + alpha = 0.6f * progress + scaleX = 1.02f + } + ) { + ClockBadge( + time, date, + cpCyan, + frameColor.copy(alpha = 0.3f), + statColor1.copy(alpha = 0.5f), statColor2.copy(alpha = 0.5f), statColor3.copy(alpha = 0.5f), + cpCyan, + progress, isDoze + ) + } + } + + if (progress > 0.05f && !isDoze) { + Box( + modifier = Modifier + .offset(x = 6.dp * progress * 1.5f, y = (-2).dp * progress) + .graphicsLayer { + alpha = 0.6f * progress + scaleX = 1.02f + } + ) { + ClockBadge( + time, date, + cpRed, + frameColor.copy(alpha = 0.3f), + statColor1.copy(alpha = 0.5f), statColor2.copy(alpha = 0.5f), statColor3.copy(alpha = 0.5f), + cpRed, + progress, isDoze + ) + } + } + + ClockBadge(time, date, primaryTimeColor, frameColor, statColor1, statColor2, statColor3, textColor, progress, isDoze) + } + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze) = rememberClockState() + val fidget by state.fidgetTrigger + + val cpYellow = Color(0xFFFCEE0A) + val cpCyan = Color(0xFF00F0FF) + val primaryTimeColor = if (isDoze) Color.White else cpYellow + val accentColor = if (isDoze) Color.White else cpCyan + + val glitchProgress = remember { Animatable(0f) } + + LaunchedEffect(fidget) { + if (fidget > 0) { + glitchProgress.snapTo(1f) + glitchProgress.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 400) + ) + } + } + + val progress = glitchProgress.value + val glitchSeed = remember(fidget) { kotlin.random.Random.nextFloat() } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .then(fidgetTapModifier) + .graphicsLayer { + if (progress > 0f) { + translationX = (if (glitchSeed > 0.5f) 15f else -15f) * progress + alpha = if (glitchSeed > 0.8f) 0.3f else 1f + } + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val (hours, minutes) = splitTimeLines(time) + + Text( + text = hours, + style = TextStyle( + fontSize = 160.sp, + fontWeight = FontWeight.Black, + fontFamily = FontFamily.Monospace, + color = primaryTimeColor, + letterSpacing = (-8).sp, + lineHeight = 160.sp, + drawStyle = if (isDoze) Stroke(width = 8f) else Fill + ) + ) + + val barColor = if (isDoze) Color.White else accentColor + + Row( + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .height(2.dp) + .weight(1f) + .background(barColor.copy(alpha = 0.6f)) + ) + + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "//", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = barColor.copy(alpha = 0.8f), + letterSpacing = (-1).sp + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = dateStr.uppercase().ifEmpty { "2077" }, + modifier = Modifier.padding(horizontal = 4.dp), + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = barColor, + letterSpacing = 2.sp + ) + ) + + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "//", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = barColor.copy(alpha = 0.8f), + letterSpacing = (-1).sp + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + + Box( + modifier = Modifier + .height(2.dp) + .weight(1f) + .background(barColor.copy(alpha = 0.6f)) + ) + } + + Text( + text = minutes, + style = TextStyle( + fontSize = 160.sp, + fontWeight = FontWeight.Black, + fontFamily = FontFamily.Monospace, + color = primaryTimeColor, + letterSpacing = (-8).sp, + lineHeight = 160.sp, + drawStyle = if (isDoze) Stroke(width = 8f) else Fill + ) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + EnhancedDateArea( + textColor = barColor, + textSize = 14.sp, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + iconSize = 16.dp, + uppercase = true, + rowArrangement = Arrangement.Center, + ) + } + + if (progress > 0.05f && !isDoze) { + Box( + modifier = Modifier + .fillMaxSize() + .offset(x = (if (glitchSeed > 0.5f) 12.dp else -12.dp) * progress) + .graphicsLayer { alpha = progress * 0.4f } + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val (gh, gm) = splitTimeLines(time) + Text(gh, style = TextStyle(fontSize = 160.sp, fontWeight = FontWeight.Black, fontFamily = FontFamily.Monospace, color = cpCyan, letterSpacing = (-8).sp, lineHeight = 160.sp)) + Spacer(modifier = Modifier.height(30.dp)) + Text(gm, style = TextStyle(fontSize = 160.sp, fontWeight = FontWeight.Black, fontFamily = FontFamily.Monospace, color = cpCyan, letterSpacing = (-8).sp, lineHeight = 160.sp)) + } + } + } + } + } + + @Composable + private fun ClockBadge( + time: String, + date: String, + primaryColor: Color, + frameColor: Color, + c1: Color, c2: Color, c3: Color, + textColor: Color, + progress: Float, + isDoze: Boolean + ) { + val colAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val badgeSeed = remember(progress > 0f) { kotlin.random.Random.nextFloat() } + + Column( + horizontalAlignment = colAlign, + verticalArrangement = Arrangement.Center, + modifier = Modifier.graphicsLayer { + if (progress > 0f) { + rotationZ = (badgeSeed - 0.5f) * 3f * progress + } + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + val blockColor = if (isDoze) Color.White else primaryColor + + Box(modifier = Modifier.width(4.dp).height(6.dp).background(blockColor)) + Box(modifier = Modifier.width(10.dp).height(6.dp).background(blockColor)) + Box(modifier = Modifier.width(20.dp).height(6.dp).background(blockColor)) + Box(modifier = Modifier.width(12.dp).height(6.dp).background(blockColor)) + } + + Spacer(modifier = Modifier.height(10.dp)) + + Box( + modifier = Modifier + .border( + width = 1.5.dp, + color = frameColor.copy(alpha = 0.8f), + shape = CutCornerShape(topStart = 16.dp, bottomEnd = 16.dp) + ) + .padding(horizontal = 24.dp, vertical = 6.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = time, + style = TextStyle( + fontSize = 80.sp, + fontWeight = FontWeight.Black, + fontFamily = FontFamily.Monospace, + color = primaryColor, + letterSpacing = (-4).sp, + drawStyle = if (isDoze) Stroke(width = 5f) else Fill + ) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Column( + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + StatBar(c1, 1.0f) + StatBar(c2, 0.75f) + StatBar(c3, 0.5f) + } + + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "V.2077", + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = if (isDoze) Color.White else primaryColor, + letterSpacing = 0.sp + ) + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = date.uppercase().ifEmpty { "2077" }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = if (isDoze) Color.White else primaryColor, + letterSpacing = 1.sp, + ), + ) + } + } + + @Composable + private fun StatBar(color: Color, widthScale: Float) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(4.dp) + .background(color) + ) + Spacer(modifier = Modifier.width(3.dp)) + + Box( + modifier = Modifier + .height(4.dp) + .width(32.dp * widthScale) + .background(color.copy(alpha = 0.8f)) + ) + } + } + + override fun getTag(): String = if (isLargeClock) "CyberpunkLargeClockView" else "CyberpunkClockView" +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DateDisplay.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DateDisplay.kt new file mode 100644 index 0000000000000..7490aa6fc43e1 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DateDisplay.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.app.PendingIntent +import android.graphics.Bitmap + +sealed interface DateDisplay { + val text: String + val icon: Bitmap? + val tintIcon: Boolean + val tapAction: PendingIntent? + + data class DateOnly(override val text: String) : DateDisplay { + override val icon: Bitmap? get() = null + override val tintIcon: Boolean get() = true + override val tapAction: PendingIntent? get() = null + } + + data class IconText( + override val text: String, + override val icon: Bitmap?, + override val tintIcon: Boolean, + override val tapAction: PendingIntent? = null, + ) : DateDisplay + + data class Weather( + val date: String, + val temp: String, + override val icon: Bitmap?, + override val tintIcon: Boolean, + override val tapAction: PendingIntent? = null, + ) : DateDisplay { + override val text: String get() = date + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt new file mode 100644 index 0000000000000..d380630872dab --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Typeface +import android.text.TextUtils +import android.util.AttributeSet +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.extensions.scaledDimen +import com.android.systemui.shared.clocks.extensions.scaleRatio +import java.util.Locale +import kotlin.math.min + +class GeneralClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = + if (isLargeClock) "GeneralLargeClockView" else "GeneralClockView" + + private val digitResIds = intArrayOf( + R.drawable.intervar_0, R.drawable.intervar_1, R.drawable.intervar_2, + R.drawable.intervar_3, R.drawable.intervar_4, R.drawable.intervar_5, + R.drawable.intervar_6, R.drawable.intervar_7, R.drawable.intervar_8, + R.drawable.intervar_9 + ) + + private val digitLightResIds = intArrayOf( + R.drawable.intervar_0_light, R.drawable.intervar_1_light, R.drawable.intervar_2_light, + R.drawable.intervar_3_light, R.drawable.intervar_4_light, R.drawable.intervar_5_light, + R.drawable.intervar_6_light, R.drawable.intervar_7_light, R.drawable.intervar_8_light, + R.drawable.intervar_9_light + ) + + private var bitmaps: Map = loadDigitBitmaps(digitResIds) + private var lightBitmaps: Map = loadDigitBitmaps(digitLightResIds) + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val d = ContextCompat.getDrawable(context, digitResIds[0]) + ?: return super.clockHeightBase + val density = context.resources.displayMetrics.density + return (d.intrinsicHeight * scaleRatio + TEXT_AREA_DP * density * scaleRatio).toInt() + } + + private val textPrimarySize = 28.sp + private val textMajorSize = 20.sp + + override fun onFontSettingChanged() { + super.onFontSettingChanged() + bitmaps = loadDigitBitmaps(digitResIds) + lightBitmaps = loadDigitBitmaps(digitLightResIds) + } + + private fun loadDigitBitmaps(resIds: IntArray): Map { + val result = mutableMapOf() + resIds.forEachIndexed { index, resId -> + ContextCompat.getDrawable(context, resId)?.toBitmap()?.let { bmp -> + result['0' + index] = bmp + } + } + return result + } + + @Composable + override fun Content() { + if (isLargeClock) { + LargeContent() + } else { + SmallContent() + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + val largeScale = min(context.scaleRatio, MAX_TABLET_SCALE) * LARGE_SCALE_MULTIPLIER + val digitSpacing = context.scaledDimen(R.dimen.large_clock_digit_spacing) + val lineSpacing = context.scaledDimen(R.dimen.large_clock_line_spacing) + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val sampleBmp = remember { bitmaps['0'] } + val digitH = (sampleBmp?.height ?: 0) * largeScale + val canvasHeightDp = with(LocalDensity.current) { + (digitH * 2 + lineSpacing).toDp() + } + + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp) + .then(fidgetTapModifier), + ) { + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val (hours, minutes) = splitTimeLines(time) + if (hours.isEmpty()) return@Canvas + + val sampleDigit = bitmaps['0'] ?: return@Canvas + val dh = sampleDigit.height * largeScale + + fun lineWidth(digits: String): Float { + var w = 0f + digits.forEachIndexed { i, c -> + val bmp = bitmaps[c] ?: return@forEachIndexed + w += bmp.width * largeScale + if (i < digits.lastIndex) w += digitSpacing + } + return w + } + + fun drawLine(digits: String, lineX: Float, lineY: Float) { + var x = lineX + digits.forEach { c -> + val bmp = bitmaps[c] ?: return@forEach + drawImage( + image = bmp.asImageBitmap(), + srcOffset = IntOffset.Zero, + srcSize = IntSize(bmp.width, bmp.height), + dstOffset = IntOffset(x.toInt(), lineY.toInt()), + dstSize = IntSize((bmp.width * largeScale).toInt(), (bmp.height * largeScale).toInt()), + colorFilter = ColorFilter.tint(tintColor, BlendMode.SrcIn), + ) + x += bmp.width * largeScale + digitSpacing + } + } + + val hoursW = lineWidth(hours) + val minutesW = lineWidth(minutes) + + drawLine(hours, (size.width - hoursW) / 2f, 0f) + drawLine(minutes, (size.width - minutesW) / 2f, dh + lineSpacing) + } + + Spacer(modifier = Modifier.height(12.dp)) + + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark, icon, tintIcon, display) = rememberClockState() + + val scale = context.scaleRatio + val paddingV = context.scaledDimen(R.dimen.clock_padding) + val dotSz = context.scaledDimen(R.dimen.dot_small_size) + val dotMgn = context.scaledDimen(R.dimen.dot_margin) + + val tintColor = tintColor(isDoze, screenOff, regionDark) + val horizontalAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + val placeholderText = config?.placeholderTextRes?.let { context.getString(it) } + val hasSpecialContent = display !is DateDisplay.DateOnly + val bottomText = when (display) { + is DateDisplay.Weather -> (display as DateDisplay.Weather).temp + else -> date + } + val useLight = hasSpecialContent + + val sampleBmp = remember { bitmaps['0'] } + val canvasHeight = remember(sampleBmp, scale) { + ((sampleBmp?.height ?: 0) * scale).coerceAtLeast(1f) + } + val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = horizontalAlign, + ) { + Text( + text = dateStr, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = tintColor.copy(alpha = if (isDoze) 0.6f else 0.8f), + letterSpacing = 0.5.sp, + ), + modifier = Modifier.padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + top = 4.dp, + bottom = 4.dp, + ), + ) + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp) + .then(fidgetTapModifier), + ) { + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val bitmapMap = if (useLight) lightBitmaps else bitmaps + + var totalWidth = 0f + for ((i, char) in time.withIndex()) { + val bmp = bitmapMap[char] ?: continue + totalWidth += bmp.width * scale + if (time.length - i == 3) totalWidth += 2 * paddingV + } + if (totalWidth <= 0f) return@Canvas + if (totalWidth > size.width) totalWidth = size.width + + var x = when { + isLeftAligned -> clockPaddingStart + isRightAligned -> size.width - clockPaddingStart - totalWidth + else -> (size.width - totalWidth) / 2f + } + + for ((i, char) in time.withIndex()) { + val bmp = bitmapMap[char] ?: continue + val yOffset = (size.height - bmp.height * scale) / 2f + + drawImage( + image = bmp.asImageBitmap(), + srcOffset = IntOffset.Zero, + srcSize = IntSize(bmp.width, bmp.height), + dstOffset = IntOffset(x.toInt(), yOffset.toInt()), + dstSize = IntSize((bmp.width * scale).toInt(), (bmp.height * scale).toInt()), + colorFilter = ColorFilter.tint(tintColor, BlendMode.SrcIn), + ) + x += bmp.width * scale + + if (time.length - i == 3) { + val centerX = x + paddingV + val centerY = size.height / 2f + val dotRadius = dotSz / 2 + val topDotY = centerY - (dotMgn / 2 + dotRadius) + val bottomDotY = centerY + (dotMgn / 2 + dotRadius) + drawOval( + color = tintColor, + topLeft = Offset(centerX - dotRadius, topDotY - dotRadius), + size = Size(dotSz, dotSz), + ) + drawOval( + color = tintColor, + topLeft = Offset(centerX - dotRadius, bottomDotY - dotRadius), + size = Size(dotSz, dotSz), + ) + x += 2 * paddingV + } + } + } + + BottomArea( + placeholderText = placeholderText, + hasSpecialContent = hasSpecialContent, + text = bottomText, + icon = icon, + tintIcon = tintIcon, + textColor = tintColor, + startPadding = sidePadding, + horizontalAlign = horizontalAlign, + ) + } + } + + @Composable + private fun BottomArea( + placeholderText: String?, + hasSpecialContent: Boolean, + text: String, + icon: Bitmap?, + tintIcon: Boolean, + textColor: Color, + startPadding: androidx.compose.ui.unit.Dp, + horizontalAlign: Alignment.Horizontal, + ) { + Column( + horizontalAlignment = horizontalAlign, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding( + start = if (isRightAligned) 0.dp else startPadding, + end = if (isRightAligned) startPadding else 0.dp, + bottom = 4.dp, + ), + ) { + if (hasSpecialContent) { + Row(verticalAlignment = Alignment.CenterVertically) { + icon?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.width(7.dp)) + } + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = textMajorSize, + fontWeight = FontWeight.Medium, + color = textColor, + ), + ) + } + } else if (placeholderText != null) { + val isJpLang = Locale.getDefault().language == "ja" + val fontName = if (isJpLang) "NDot77JPExtended" else "nothingdot" + val fontFamily = FontFamily(Typeface.create(fontName, Typeface.NORMAL)) + Text( + text = placeholderText, + maxLines = 1, + style = TextStyle( + fontSize = textPrimarySize, + fontWeight = FontWeight.Normal, + fontFamily = fontFamily, + color = textColor, + ), + ) + } + } + } + + companion object { + private const val MAX_TABLET_SCALE = 1.2f + private const val LARGE_SCALE_MULTIPLIER = 1.8f + private const val TEXT_AREA_DP = 56f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt new file mode 100644 index 0000000000000..a7866c0ccd160 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Typeface +import android.util.AttributeSet +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.customization.clocks.R as clocksR +import java.util.Locale + +class OldQuickLookClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = "OLDQuickLookClockView" + + private val primaryTextSize = 80.sp + private val secondaryTextSize = 18.sp + private val dateTextSize = 14.sp + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val density = context.resources.displayMetrics.density + val scaledDensity = context.resources.displayMetrics.scaledDensity + val timePx = PRIMARY_TEXT_SP * scaledDensity * scaleRatio + val infoPx = INFO_AREA_DP * density * scaleRatio + return (timePx + infoPx).toInt() + } + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark, icon, tintIcon, display) = rememberClockState() + + val textColor = tintColor(isDoze, screenOff, regionDark) + val horizontalAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + val isJpLang = Locale.getDefault().language == "ja" + val infoFontName = if (isJpLang) "NDot77JPExtended" else "nothingdot" + val infoFontFamily = FontFamily(Typeface.create(infoFontName, Typeface.NORMAL)) + val clockFontFamily = FontFamily(Typeface.create("nothingdot57", Typeface.NORMAL)) + + val placeholderText = config?.placeholderTextRes?.let { context.getString(it) } + val hasSpecialContent = display !is DateDisplay.DateOnly + val bottomText = when (display) { + is DateDisplay.Weather -> (display as DateDisplay.Weather).temp + else -> date + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = horizontalAlign, + ) { + Text( + text = time, + maxLines = 1, + style = TextStyle( + fontSize = primaryTextSize, + fontWeight = FontWeight.Bold, + fontFamily = clockFontFamily, + color = textColor, + ), + modifier = Modifier.then(fidgetTapModifier), + ) + + Text( + text = dateStr, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = dateTextSize, + fontWeight = FontWeight.Normal, + fontFamily = infoFontFamily, + color = textColor, + ), + ) + + Column( + horizontalAlignment = horizontalAlign, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 6.dp), + ) { + if (hasSpecialContent) { + Row(verticalAlignment = Alignment.CenterVertically) { + icon?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.width(7.dp)) + } + Text( + text = bottomText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = secondaryTextSize, + fontWeight = FontWeight.Normal, + fontFamily = infoFontFamily, + color = textColor, + ), + ) + } + } else if (placeholderText != null) { + Text( + text = placeholderText, + maxLines = 1, + style = TextStyle( + fontSize = secondaryTextSize, + fontWeight = FontWeight.Normal, + fontFamily = infoFontFamily, + color = textColor, + ), + ) + } + } + } + } + + @Composable + private fun LargeContent() { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + + val textColor = tintColor(isDoze, screenOff, regionDark) + val clockFontFamily = FontFamily(Typeface.create("nothingdot57", Typeface.NORMAL)) + + val (hours, minutes) = splitTimeLines(time) + + val largeFontSize = with(LocalDensity.current) { + context.resources.getDimensionPixelSize( + clocksR.dimen.large_clock_text_size + ).toSp() + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.then(fidgetTapModifier), + ) { + Text( + text = hours, + maxLines = 1, + style = TextStyle( + fontSize = largeFontSize, + fontWeight = FontWeight.Bold, + fontFamily = clockFontFamily, + color = textColor, + lineHeight = largeFontSize, + ), + ) + Text( + text = minutes, + maxLines = 1, + style = TextStyle( + fontSize = largeFontSize, + fontWeight = FontWeight.Bold, + fontFamily = clockFontFamily, + color = textColor, + lineHeight = largeFontSize, + ), + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + EnhancedDateArea( + textColor = textColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + } + + companion object { + private const val PRIMARY_TEXT_SP = 80f + private const val INFO_AREA_DP = 60f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookController.kt new file mode 100644 index 0000000000000..bf50b66378e19 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookController.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.app.PendingIntent +import android.text.format.DateFormat +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.android.systemui.plugins.keyguard.data.model.AlarmData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import kotlinx.coroutines.flow.MutableStateFlow +import java.util.concurrent.TimeUnit + +class QuickLookController(private val view: AxClockView) { + + val clockDataFlow = MutableStateFlow(ClockData.EMPTY) + val mediaStateFlow = MutableStateFlow(ClockMediaState()) + val nowPlayingFlow = MutableStateFlow("") + val nowPlayingTapActionFlow = MutableStateFlow(null) + val nextAlarmFlow = MutableStateFlow("") + + fun onClockDataChanged(data: ClockData) { + clockDataFlow.value = data + } + + fun onPlaybackStateChanged(playing: Boolean) { + mediaStateFlow.value = mediaStateFlow.value.copy(isPlaying = playing) + } + + fun onMetadataChanged(track: String, artist: String, packageName: String) { + mediaStateFlow.value = mediaStateFlow.value.copy( + trackTitle = track, + artistName = artist, + packageName = packageName, + ) + } + + fun onNowPlayingUpdate(text: String) { + nowPlayingFlow.value = text + } + + fun onAlarmDataChanged(data: AlarmData) { + val nextAlarmMillis = data.nextAlarmMillis ?: 0L + val now = System.currentTimeMillis() + val futureLimit = now + TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS) + val withinHours = nextAlarmMillis in now..futureLimit + nextAlarmFlow.value = if (withinHours) { + val fmt = if (DateFormat.is24HourFormat(view.context)) "HH:mm" else "h:mm" + DateFormat.format(fmt, nextAlarmMillis).toString() + } else "" + } + + @Composable + fun rememberResolvedDisplay(dateStr: String): DateDisplay { + val clockData by clockDataFlow.collectAsState() + val media by mediaStateFlow.collectAsState() + val nowPlaying by nowPlayingFlow.collectAsState() + val nowPlayingTapAction by nowPlayingTapActionFlow.collectAsState() + val alarm by nextAlarmFlow.collectAsState() + + return remember(clockData, media, nowPlaying, nowPlayingTapAction, alarm, dateStr) { + view.resolveDisplay(clockData, media, nowPlaying, nowPlayingTapAction, alarm, dateStr) + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt new file mode 100644 index 0000000000000..333021737f26c --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.app.PendingIntent +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +private const val TAG = "QuickLookDateArea" + +@Composable +fun QuickLookDateArea( + modifier: Modifier = Modifier, + display: DateDisplay, + dateStr: String, + sizeScale: Float, + textColor: Color, + textSize: TextUnit = 18.sp, + fontFamily: FontFamily, + fontWeight: FontWeight = FontWeight.Medium, + letterSpacing: TextUnit = 0.sp, + iconSize: Dp = 16.dp, + uppercase: Boolean = false, + rowArrangement: Arrangement.Horizontal = Arrangement.Center, +) { + val style = TextStyle( + fontSize = textSize, + fontWeight = fontWeight, + fontFamily = fontFamily, + color = textColor, + letterSpacing = letterSpacing, + lineHeight = 24.sp, + ) + + val inverseScale = if (sizeScale > 0f) 1f / sizeScale else 1f + val tapModifier = display.tapAction?.let { action -> + Modifier.pointerInput(action) { + detectTapGestures { + fireTapAction(action) + } + } + } ?: Modifier + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = rowArrangement, + modifier = modifier + .widthIn(max = 280.dp) + .graphicsLayer { scaleY = inverseScale } + .then(tapModifier), + ) { + if (display is DateDisplay.Weather) { + val dateText = if (uppercase) dateStr.uppercase() else dateStr + Text(text = dateText, maxLines = 1, style = style, modifier = Modifier.basicMarquee()) + display.icon?.let { + Spacer(modifier = Modifier.width(6.dp)) + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (display.tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(iconSize), + ) + } + if (display.temp.isNotEmpty()) { + Spacer(modifier = Modifier.width(6.dp)) + val tempText = if (uppercase) display.temp.uppercase() else display.temp + Text(text = tempText, maxLines = 1, style = style) + } + } else { + display.icon?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (display.tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(iconSize), + ) + Spacer(modifier = Modifier.width(6.dp)) + } + val text = when (display) { + is DateDisplay.IconText -> display.text + is DateDisplay.DateOnly -> dateStr + else -> dateStr + } + val displayText = if (uppercase) text.uppercase() else text + Text( + text = displayText, + maxLines = 1, + style = style, + modifier = Modifier.basicMarquee(), + ) + } + } +} + +internal fun fireTapAction(action: PendingIntent) { + try { + action.send() + } catch (e: PendingIntent.CanceledException) { + Log.w(TAG, "Tap action cancelled", e) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt new file mode 100644 index 0000000000000..6c7474881a403 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.shared.clocks.view + +import android.graphics.Typeface +import androidx.compose.ui.text.font.FontFamily + +fun splitTimeLines(time: String): Pair = when (time.length) { + 4 -> time.substring(0, 2) to time.substring(2, 4) + 3 -> time.substring(0, 1) to time.substring(1, 3) + else -> "" to "" +} + +fun resolveBodyFontFamily(): FontFamily { + val typeface = Typeface.create(FONT_FAMILY_BODY, Typeface.NORMAL) + return FontFamily(typeface) +} + +fun resolveDateFontFamily(): FontFamily { + val typeface = Typeface.create(FONT_FAMILY_DATE, Typeface.NORMAL) + return FontFamily(typeface) +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/CalendarSimpleData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/CalendarSimpleData.kt new file mode 100644 index 0000000000000..caff6a70d6887 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/CalendarSimpleData.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.keyguard.ui.clocks + +import android.app.PendingIntent +import android.text.TextUtils +import java.util.Objects + +class CalendarSimpleData( + val id: Long = 0L, + val title: String? = null, + val startTime: Long = 0L, + val endTime: Long = 0L, + val location: String? = null, + val description: String = "", + val formattedTime: String = "", + val eventStatus: Int = EVENT_STATUS_TO_SCHEDULE, + val tapAction: PendingIntent? = null, +) { + + fun isEventVisible(): Boolean = + eventStatus == EVENT_STATUS_TO_BEGIN || eventStatus == EVENT_STATUS_NOW + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CalendarSimpleData) return false + + return id == other.id && + startTime == other.startTime && + endTime == other.endTime && + eventStatus == other.eventStatus && + TextUtils.equals(title, other.title) && + TextUtils.equals(location, other.location) + } + + override fun hashCode(): Int { + return Objects.hash(id, title, startTime, endTime, location, eventStatus) + } + + companion object { + const val EVENT_STATUS_TO_SCHEDULE = 0 + const val EVENT_STATUS_TO_BEGIN = 1 + const val EVENT_STATUS_NOW = 2 + const val EVENT_STATUS_END = 3 + + val EMPTY = CalendarSimpleData() + } +} + diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt index 21a0990f999b8..e95720355f504 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt @@ -19,31 +19,31 @@ import com.android.systemui.plugins.annotations.ProtectedInterface @ProtectedInterface interface ClockAnimations { /** Runs an enter animation (if any) */ - fun enter() + fun enter() {} /** Sets how far into AOD the device currently is. */ - fun doze(fraction: Float) + fun doze(fraction: Float) {} /** Sets how far into the folding animation the device is. */ - fun fold(fraction: Float) + fun fold(fraction: Float) {} /** Runs the battery animation (if any). */ - fun charge() + fun charge() {} /** Runs when the clock's position changed during the move animation. */ - fun onPositionAnimated(anim: ClockPositionAnimationArgs) + fun onPositionAnimated(anim: ClockPositionAnimationArgs) {} /** * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview, * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize */ - fun onPickerCarouselSwiping(swipingFraction: Float) + fun onPickerCarouselSwiping(swipingFraction: Float) {} /** Runs when an animation when the view is tapped on the lockscreen */ - fun onFidgetTap(x: Float, y: Float) + fun onFidgetTap(x: Float, y: Float) {} /** Update reactive axes for this clock */ - fun onFontAxesChanged(style: ClockAxisStyle) + fun onFontAxesChanged(style: ClockAxisStyle) {} } data class ClockPositionAnimationArgs(val fromLeft: Int, val direction: Int, val fraction: Float) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockData.kt new file mode 100644 index 0000000000000..eb76a846bc5e5 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockData.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.keyguard.ui.clocks + +data class ClockData( + val weather: ClockWeatherData = ClockWeatherData.EMPTY, + val calendar: CalendarSimpleData = CalendarSimpleData.EMPTY, + val smartspace: List = emptyList() +) { + companion object { + val EMPTY = ClockData() + } +} + diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt index 3bd443c5b8ae5..77ef47ceb66cd 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt @@ -69,6 +69,14 @@ interface ClockEvents { /** Call with zen/dnd information */ fun onZenDataChanged(data: ZenData) + fun onUiModeChanged(isDarkTheme: Boolean) {} + fun onDateChanged() {} + fun onClockDataChanged(data: ClockData) {} + fun onMetadataChanged(track: String, artist: String, packageName: String) {} + fun onPlaybackStateChanged(playing: Boolean) {} + fun onNowPlayingUpdate(nowPlayingText: String) {} + fun onClockLayoutChanged(isCentered: Boolean, isLargeClockVisible: Boolean) {} + fun onDepthEffectVisibilityChanged(visible: Boolean) {} } class ClockEventListeners { diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt index f0699f4298c59..3e8aff35d2ee5 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt @@ -15,6 +15,7 @@ package com.android.systemui.plugins.keyguard.ui.clocks import android.content.Context import android.graphics.Color +import android.graphics.Point import android.graphics.Rect import com.android.systemui.monet.ColorScheme import com.android.systemui.plugins.annotations.ProtectedInterface @@ -53,6 +54,14 @@ interface ClockFaceEvents { /** Called to notify the clock about its display. */ fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) + fun onStartedWakingUp() {} + fun onStartedGoingToSleep(isKeyguardVisible: Boolean) {} + fun onWakefulnessStateChanged(isWakingUp: Boolean, tapPosition: Point?) {} + fun onRegionDarknessChanged(isRegionDark: Boolean) {} + fun onScreenOff(screenOff: Boolean) {} + fun onDozeChanged(dozing: Boolean) {} + fun onDozeAmountChanged(linear: Float, eased: Float) {} + fun onPulsingChanged(pulsing: Boolean) {} } /** Contains Theming information for the clock face */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockWeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockWeatherData.kt new file mode 100644 index 0000000000000..4c6f9ab816fa7 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockWeatherData.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.keyguard.ui.clocks + +import android.app.PendingIntent + +data class ClockWeatherData( + val temp: String = "", + val condition: String = "", + val conditionCode: Int = 0, + val city: String? = null, + val humidity: String? = null, + val wind: String? = null, + val windDirection: String? = null, + val tempUnit: String? = null, + val windUnit: String? = null, + val pinWheel: String? = null, + val timestamp: Long = 0L, + val iconBytes: ByteArray? = null, + val tintIcon: Boolean = true, + val tapAction: PendingIntent? = null, +) { + val formattedTemp: String + get() = if (temp.isNotEmpty()) "$temp${tempUnit ?: "°"}" else "" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ClockWeatherData) return false + return temp == other.temp && + condition == other.condition && + conditionCode == other.conditionCode && + city == other.city && + timestamp == other.timestamp && + tintIcon == other.tintIcon && + iconBytes.contentEquals(other.iconBytes) + } + + override fun hashCode(): Int { + var result = temp.hashCode() + result = 31 * result + condition.hashCode() + result = 31 * result + conditionCode + result = 31 * result + (city?.hashCode() ?: 0) + result = 31 * result + timestamp.hashCode() + result = 31 * result + tintIcon.hashCode() + result = 31 * result + (iconBytes?.contentHashCode() ?: 0) + return result + } + + companion object { + val EMPTY = ClockWeatherData() + } +} + diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/SmartspaceData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/SmartspaceData.kt new file mode 100644 index 0000000000000..d3d2d35bd02bb --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/SmartspaceData.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.keyguard.ui.clocks + +import android.app.PendingIntent + +data class SmartspaceData( + val id: String, + val title: String, + val subtitle: String, + val featureType: Int, + val iconBytes: ByteArray?, + val componentName: String?, + val isSensitive: Boolean, + val sourceType: Int, + val creationTime: Long, + val score: Float, + val tapAction: PendingIntent? = null, +) { + val isFromSmartspacer: Boolean + get() = sourceType == SOURCE_SMARTSPACER + + val isFromGoogleSmartspace: Boolean + get() = sourceType == SOURCE_GOOGLE_SMARTSPACE + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SmartspaceData) return false + return id == other.id && + title == other.title && + subtitle == other.subtitle && + featureType == other.featureType && + sourceType == other.sourceType + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + subtitle.hashCode() + result = 31 * result + featureType + result = 31 * result + sourceType + return result + } + + companion object { + const val SOURCE_SMARTSPACER = 100 + const val SOURCE_GOOGLE_SMARTSPACE = 200 + + val EMPTY = SmartspaceData( + id = "", + title = "", + subtitle = "", + featureType = 0, + iconBytes = null, + componentName = null, + isSensitive = false, + sourceType = 0, + creationTime = 0L, + score = 0f + ) + } +} + diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 1ff18673b1805..c5c56617c1cc4 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -16,6 +16,7 @@ package com.android.keyguard import android.R +import android.app.PendingIntent import android.app.NotificationManager.zenModeFromInterruptionFilter import android.content.BroadcastReceiver import android.content.Context @@ -62,9 +63,12 @@ import com.android.systemui.plugins.keyguard.ui.clocks.ClockController import com.android.systemui.plugins.keyguard.ui.clocks.ClockEventListener import com.android.systemui.plugins.keyguard.ui.clocks.ClockFaceController import com.android.systemui.plugins.keyguard.ui.clocks.ClockFaceController.Companion.updateTheme +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import com.android.systemui.shared.clocks.view.AxClockView import com.android.systemui.plugins.keyguard.ui.clocks.ClockMessageBuffers import com.android.systemui.plugins.keyguard.ui.clocks.ClockTickRate import com.android.systemui.plugins.keyguard.ui.clocks.TimeFormatKind +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R as SysuiR import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.settings.UserTracker @@ -74,6 +78,7 @@ import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChang import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ZenModeController import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.util.ScrimUtils import com.android.systemui.util.annotations.DeprecatedSysuiVisibleForTesting import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy @@ -102,6 +107,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, // TODO b/362719719 - We should use the configuration controller associated with the display. private val configurationController: ConfigurationController, + private val statusBarStateController: StatusBarStateController, @DisplaySpecific private val resources: Resources, @DisplaySpecific val context: Context, @Main private val mainExecutor: DelayableExecutor, @@ -132,6 +138,8 @@ constructor( if (clock == null) { return } + (clock.smallClock.view as? AxClockView)?.onFidgetTapListener = null + (clock.largeClock.view as? AxClockView)?.onFidgetTapListener = null smallClockOnAttachStateChangeListener?.let { clock.smallClock.view.removeOnAttachStateChangeListener(it) smallClockFrame?.viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener) @@ -151,6 +159,9 @@ constructor( clock.eventListeners.attach(clockListener) clock.initialize(isDarkTheme(), dozeAmount.value, 0f) + (clock.smallClock.view as? AxClockView)?.onFidgetTapListener = ::handleFidgetTap + (clock.largeClock.view as? AxClockView)?.onFidgetTapListener = ::handleFidgetTap + if (!regionSamplingEnabled) { updateColors() } else { @@ -187,6 +198,16 @@ constructor( } zenData?.let { clock.events.onZenDataChanged(it) } alarmData?.let { clock.events.onAlarmDataChanged(it) } + quickLookClockData?.let { clock.events.onClockDataChanged(it) } + if (qlMediaPlaying || qlTrack.isNotEmpty()) { + clock.events.onPlaybackStateChanged(qlMediaPlaying) + clock.events.onMetadataChanged(qlTrack, qlArtist, qlMediaPackage) + } + if (qlNowPlaying.isNotEmpty()) { + clock.events.onNowPlayingUpdate(qlNowPlaying) + (clock.smallClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = qlNowPlayingAction + (clock.largeClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = qlNowPlayingAction + } smallClockOnAttachStateChangeListener = object : OnAttachStateChangeListener { @@ -257,6 +278,9 @@ constructor( context.theme.resolveAttribute(R.attr.isLightTheme, isLightTheme, true) return isLightTheme.data == 0 } + private fun onClockUiModeChanged() { + clock?.run { events.onUiModeChanged(isDarkTheme()) } + } private fun updateColors() { val isDarkTheme = isDarkTheme() @@ -277,6 +301,10 @@ constructor( smallClock.updateTheme { it.copy(isDarkTheme = isDarkTheme) } largeClock.updateTheme { it.copy(isDarkTheme = isDarkTheme) } } + clock?.run { + smallClock.events.onRegionDarknessChanged(isDarkTheme) + largeClock.events.onRegionDarknessChanged(isDarkTheme) + } } protected open fun createRegionSampler( @@ -312,6 +340,13 @@ constructor( private var weatherData: WeatherData? = null private var zenData: ZenData? = null private var alarmData: AlarmData? = null + private var quickLookClockData: ClockData? = null + private var qlMediaPlaying: Boolean = false + private var qlTrack: String = "" + private var qlArtist: String = "" + private var qlMediaPackage: String = "" + private var qlNowPlaying: String = "" + private var qlNowPlayingAction: PendingIntent? = null private val clockListener = object : ClockEventListener { @@ -337,6 +372,9 @@ constructor( logger.i("onDensityOrFontScaleChanged") updateFontSizes() } + override fun onUiModeChanged() { + onClockUiModeChanged() + } } private val batteryCallback = @@ -355,9 +393,16 @@ constructor( private val localeBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - clock?.run { - events.onLocaleChanged(Locale.getDefault()) - events.onTimeFormatChanged(getTimeFormatKind()) + when (intent.action) { + Intent.ACTION_LOCALE_CHANGED -> { + clock?.run { + events.onLocaleChanged(Locale.getDefault()) + events.onTimeFormatChanged(getTimeFormatKind()) + } + } + Intent.ACTION_DATE_CHANGED -> { + clock?.run { events.onDateChanged() } + } } } } @@ -392,6 +437,42 @@ constructor( weatherData = data clock?.run { events.onWeatherDataChanged(data) } } + override fun onClockDataChanged(data: ClockData) { + quickLookClockData = data + clock?.run { events.onClockDataChanged(data) } + } + override fun onQLPlaybackStateChanged(play: Boolean) { + qlMediaPlaying = play + clock?.run { events.onPlaybackStateChanged(play) } + } + override fun onQLMetadataChanged(track: String, artist: String, packageName: String) { + qlTrack = track + qlArtist = artist + qlMediaPackage = packageName + clock?.run { + events.onMetadataChanged(track, artist, packageName) + } + } + override fun onNowPlayingUpdate(nowPlayingText: String, tapAction: PendingIntent?) { + qlNowPlaying = nowPlayingText + qlNowPlayingAction = tapAction + clock?.run { + events.onNowPlayingUpdate(nowPlayingText) + (smallClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = tapAction + (largeClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = tapAction + } + } + override fun onStartedWakingUp() { + clock?.smallClock?.events?.onStartedWakingUp() + clock?.largeClock?.events?.onStartedWakingUp() + } + override fun onStartedGoingToSleep(why: Int) { + clock?.smallClock?.events?.onScreenOff(true) + clock?.largeClock?.events?.onScreenOff(true) + val keyguardVisible = ScrimUtils.get().isKeyguardShowing() + clock?.smallClock?.events?.onStartedGoingToSleep(keyguardVisible) + clock?.largeClock?.events?.onStartedGoingToSleep(keyguardVisible) + } override fun onTimeChanged() { refreshTime() @@ -436,6 +517,61 @@ constructor( } } + private val dozeCallback = + object : StatusBarStateController.StateListener { + override fun onDozingChanged(isDozing: Boolean) { + clock?.smallClock?.events?.onDozeChanged(isDozing) + clock?.largeClock?.events?.onDozeChanged(isDozing) + } + override fun onDozeAmountChanged(linear: Float, eased: Float) { + clock?.smallClock?.events?.onDozeAmountChanged(linear, eased) + clock?.largeClock?.events?.onDozeAmountChanged(linear, eased) + } + override fun onPulsingChanged(pulsing: Boolean) { + clock?.smallClock?.events?.onPulsingChanged(pulsing) + clock?.largeClock?.events?.onPulsingChanged(pulsing) + } + } + private var depthBlockedByFading = false + private var depthBlockedByGoingAway = false + private var depthBlockedByBouncer = false + private var depthBlockedByAlpha = false + private var depthBlockedByDozeAmount = false + private val depthScrimListener = object : ScrimUtils.ScrimEventListener { + override fun onKeyguardFadingAwayChanged(fadingAway: Boolean) { + depthBlockedByFading = fadingAway + updateDepthVisibility() + } + override fun onKeyguardGoingAwayChanged(goingAway: Boolean) { + depthBlockedByGoingAway = goingAway + updateDepthVisibility() + } + override fun onPrimaryBouncerShowingChanged(showing: Boolean) { + depthBlockedByBouncer = showing + updateDepthVisibility() + } + override fun onDozingChanged() { + updateDepthVisibility() + } + override fun onKeyguardShowingChanged(showing: Boolean) { + updateDepthVisibility() + } + override fun onKeyguardAlphaChanged(alpha: Float) { + depthBlockedByAlpha = alpha < 1f + updateDepthVisibility() + } + } + private fun updateDepthVisibility() { + val scrim = ScrimUtils.get() + val visible = scrim.isKeyguardShowing() + && !scrim.isDozing() + && !depthBlockedByFading + && !depthBlockedByGoingAway + && !depthBlockedByBouncer + && !depthBlockedByAlpha + && !depthBlockedByDozeAmount + clock?.events?.onDepthEffectVisibilityChanged(visible) + } private fun handleZenMode(zen: Int) { val mode = ZenMode.fromInt(zen) if (mode == null) { @@ -478,18 +614,24 @@ constructor( isRegistered = true logger.i("registerListeners(isPreview = $isPreview)") + val filter = IntentFilter().apply { + addAction(Intent.ACTION_LOCALE_CHANGED) + addAction(Intent.ACTION_DATE_CHANGED) + } broadcastDispatcher.registerReceiver( localeBroadcastReceiver, - IntentFilter(Intent.ACTION_LOCALE_CHANGED), + filter, ) // Proactively update timezone on listener registration to avoid race conditions on startup. clock?.events?.onTimeZoneChanged(IcuTimeZone.getDefault()) + statusBarStateController.addCallback(dozeCallback) configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) zenModeController.addCallback(zenModeCallback) + ScrimUtils.get().addListener(depthScrimListener) if (SceneContainerFlag.isEnabled) { handleDoze( when (AOD) { @@ -514,10 +656,12 @@ constructor( logger.i("unregisterListeners(isPreview = $isPreview)") broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) + statusBarStateController.removeCallback(dozeCallback) configurationController.removeCallback(configListener) batteryController.removeCallback(batteryCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) zenModeController.removeCallback(zenModeCallback) + ScrimUtils.get().removeListener(depthScrimListener) smallRegionSampler?.stopRegionSampler() largeRegionSampler?.stopRegionSampler() smallTimeListener?.stop() @@ -583,7 +727,7 @@ constructor( } fun handleFidgetTap(x: Float, y: Float) { - if (isPreview) return + if (isPreview || isCharging) return clock?.run { smallClock.animations.onFidgetTap(x, y) largeClock.animations.onFidgetTap(x, y) @@ -607,6 +751,12 @@ constructor( smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD) largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD) dozeAmount.value = doze + + val blocked = doze > 0f && doze < 1f + if (depthBlockedByDozeAmount != blocked) { + depthBlockedByDozeAmount = blocked + updateDepthVisibility() + } } @DeprecatedSysuiVisibleForTesting diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 0fa8a359561d8..d5cf8304c185e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -132,6 +132,7 @@ import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.quicklook.QuickLookClient; import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -155,6 +156,7 @@ import com.android.systemui.keyguard.shared.constants.TrustAgentUiEvent; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.keyguard.data.model.WeatherData; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.scene.domain.interactor.SceneInteractor; @@ -330,6 +332,7 @@ public void onExpandedChanged(boolean isExpanded) { }; private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; private final Provider mDozeParameters; + private final QuickLookClient mQuickLookClient; private final Object mSimDataLockObject = new Object(); HashMap mSimDatasBySlotId = new HashMap<>(); @@ -476,6 +479,44 @@ public boolean isPocketLockVisible(){ return mPocketManager.isPocketLockVisible(); } + private final QuickLookClient.Callback mQuickLookCallback = new QuickLookClient.Callback() { + @Override + public void onClockDataChanged(ClockData data) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onClockDataChanged(data); + } + } + } + @Override + public void onQLPlaybackStateChanged(boolean play) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onQLPlaybackStateChanged(play); + } + } + } + @Override + public void onQLMetadataChanged(String track, String artist, String packageName) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onQLMetadataChanged(track, artist, packageName); + } + } + } + @Override + public void onNowPlayingUpdate(String nowPlayingText, android.app.PendingIntent tapAction) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onNowPlayingUpdate(nowPlayingText, tapAction); + } + } + } + }; private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback = new IBiometricEnabledOnKeyguardCallback.Stub() { @Override @@ -861,6 +902,12 @@ public void setKeyguardShowing(boolean showing, boolean occluded) { if (occlusionChanged || showingChanged) { updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } + if (!showingChanged) return; + if (mKeyguardShowing) { + mQuickLookClient.addCallback(mQuickLookCallback); + } else { + mQuickLookClient.removeCallback(mQuickLookCallback); + } } /** @@ -945,7 +992,6 @@ private boolean isSecureLockDeviceStrongBiometricAuthFlagSet(int userId) { STRONG_BIOMETRIC_AUTH_REQUIRED_FOR_SECURE_LOCK_DEVICE); } - @VisibleForTesting public void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) { try { @@ -1523,7 +1569,6 @@ public boolean getUserUnlockedWithBiometric(int userId) { return fingerprintAllowed || unlockedByFace; } - /** * Returns whether the user is unlocked with face. * @deprecated Use {@link DeviceEntryFaceAuthInteractor#isAuthenticated()} instead @@ -2304,7 +2349,8 @@ protected KeyguardUpdateMonitor( Provider communalSceneInteractor, Provider keyguardServiceShowLockscreenInteractor, - Provider dozeParameters) { + Provider dozeParameters, + QuickLookClient quickLookClient) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2358,6 +2404,7 @@ protected KeyguardUpdateMonitor( mSceneInteractor = sceneInteractor; mCommunalSceneInteractor = communalSceneInteractor; mKeyguardServiceShowLockscreenInteractor = keyguardServiceShowLockscreenInteractor; + mQuickLookClient = quickLookClient; mPocketManager = (PocketManager) context.getSystemService(Context.POCKET_SERVICE); if (mPocketManager != null) { @@ -2706,7 +2753,6 @@ void onAlternateBouncerVisibilityChange(boolean isAlternateBouncerVisible) { setAlternateBouncerShowing(isAlternateBouncerVisible); } - @VisibleForTesting void onTransitionStateChanged(ObservableTransitionState transitionState) { int primaryBouncerFullyShown = isPrimaryBouncerFullyShown(transitionState) ? 1 : 0; @@ -2965,7 +3011,6 @@ private void requestActiveUnlock( } } - /** * Attempts to trigger active unlock from trust agent. * Only dismisses the keyguard under certain conditions. @@ -3249,7 +3294,6 @@ public boolean shouldListenForFace() { return getFaceAuthInteractor() != null && getFaceAuthInteractor().canFaceAuthRun(); } - private void logListenerModelData(@NonNull KeyguardListenModel model) { mLogger.logKeyguardListenerModel(model); if (model instanceof KeyguardFingerprintListenModel) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index a0a7f153ec613..e337235de8a66 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -25,6 +25,7 @@ import com.android.internal.annotations.WeaklyReferencedCallback; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.plugins.keyguard.data.model.WeatherData; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData; import com.android.systemui.statusbar.KeyguardIndicationController; import java.util.TimeZone; @@ -342,4 +343,11 @@ public void onBiometricEnrollmentStateChanged(BiometricSourceType biometricSourc * On force is dismissible state changed. */ public void onForceIsDismissibleChanged(boolean forceIsDismissible) { } + public void onClockDataChanged(ClockData data) { } + public void onQLPlaybackStateChanged(boolean play) { } + public void onQLMetadataChanged(String track, String artist, String packageName) { } + public void onNowPlayingUpdate(String nowPlayingText) { } + public void onNowPlayingUpdate(String nowPlayingText, android.app.PendingIntent tapAction) { + onNowPlayingUpdate(nowPlayingText); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index b9e0aa98e8b60..3e76304980450 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -34,7 +34,7 @@ import com.android.systemui.res.R; import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shared.clocks.ClockRegistry; -import com.android.systemui.shared.clocks.DefaultClockProvider; +import com.android.systemui.shared.clocks.AxClockProvider; import com.android.systemui.util.ThreadAssert; import dagger.Module; @@ -69,7 +69,7 @@ public static ClockRegistry getClockRegistry( com.android.systemui.shared.Flags.lockscreenCustomClocks() || featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), /* handleAllUsers= */ true, - new DefaultClockProvider( + new AxClockProvider( layoutInflater, resources, com.android.systemui.shared.Flags.clockReactiveVariants(), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 7f11f5332d99e..a9a7b5a458eec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -111,15 +111,20 @@ object KeyguardClockViewBinder { } launch { - viewModel.clockShouldBeCentered.collect { - viewModel.currentClock.value?.let { - if (it.largeClock.config.hasCustomPositionUpdatedAnimation) { - blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping) - } else { - blueprintInteractor.refreshBlueprint(Type.DefaultTransition) + combine( + viewModel.clockShouldBeCentered, + viewModel.isLargeClockVisible + ) { centered, large -> centered to large } + .collect { (isCentered, isLargeVisible) -> + viewModel.currentClock.value?.let { clock -> + clock.events.onClockLayoutChanged(isCentered, isLargeVisible) + if (clock.largeClock.config.hasCustomPositionUpdatedAnimation) { + blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping) + } else { + blueprintInteractor.refreshBlueprint(Type.DefaultTransition) + } } } - } } launch { @@ -275,7 +280,7 @@ object KeyguardClockViewBinder { clockSection.applyConstraints(constraintSet) if (animated) { set?.let { TransitionManager.beginDelayedTransition(rootView, it) } - ?: run { TransitionManager.beginDelayedTransition(rootView, defaultTransition) } + ?: run { TransitionManager.beginDelayedTransition(rootView) } } constraintSet.applyTo(rootView) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index 99a8e8585566c..b4b7452848845 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -69,8 +69,8 @@ constructor( aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, clockSection: ClockSection, - smartspaceSection: SmartspaceSection, mediaSection: SplitShadeMediaSection, + smartspaceSection: SmartspaceSection, keyguardWeatherViewSection: KeyguardWeatherViewSection, keyguardSliceViewSection: KeyguardSliceViewSection, axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, @@ -92,9 +92,9 @@ constructor( splitShadeGuidelines, aodPromotedNotificationSection, aodNotificationIconsSection, - smartspaceSection, aodBurnInSection, clockSection, + smartspaceSection, keyguardWeatherViewSection, keyguardSliceViewSection, mediaSection, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index 234dee7871e0f..71e81cb0706d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -18,6 +18,10 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -45,6 +49,7 @@ import com.android.systemui.statusbar.ui.SystemBarUtilsState import com.android.systemui.util.ui.value import javax.inject.Inject import kotlinx.coroutines.DisposableHandle +import com.android.systemui.shared.clocks.ClockSettingsRepository class AodNotificationIconsSection @Inject @@ -62,13 +67,36 @@ constructor( private var nicBindingDisposable: DisposableHandle? = null private val nicId = R.id.aod_notification_icon_container private lateinit var nic: NotificationIconContainer + private var shouldCenterNic = true + private val clockSettingsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + updateShouldCenterNic() + } + } + private fun updateShouldCenterNic() { + val newShouldCenter = ClockSettingsRepository.shouldCenterIcons(context) + if (shouldCenterNic != newShouldCenter) { + shouldCenterNic = newShouldCenter + if (::nic.isInitialized) { + nic.setPaddingRelative( + if (shouldCenterNic) 0 + else context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons), + 0, + 0, + 0, + ) + } + } + } override fun addViews(constraintLayout: ConstraintLayout) { + updateShouldCenterNic() nic = NotificationIconContainer(context, null).apply { id = nicId setPaddingRelative( - resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons), + if (shouldCenterNic) 0 + else resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons), 0, resources.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding), 0, @@ -77,6 +105,17 @@ constructor( } constraintLayout.addView(nic) + ClockSettingsRepository.init(context) + context.contentResolver.registerContentObserver( + ClockSettingsRepository.clockFaceUri, + false, + clockSettingsObserver + ) + context.contentResolver.registerContentObserver( + ClockSettingsRepository.alignmentUri, + false, + clockSettingsObserver + ) } override fun bindData(constraintLayout: ConstraintLayout) { @@ -111,12 +150,14 @@ constructor( setGoneMargin(nicId, BOTTOM, bottomMargin) setVisibility(nicId, if (isVisible.value) VISIBLE else GONE) + clear(nicId, START) + clear(nicId, END) if (PromotedNotificationUi.isEnabled && !isFullWidthShade) { // Don't create a start constraint, so the icons can hopefully right-align. } else { - connect(nicId, START, PARENT_ID, START, horizontalMargin) + connect(nicId, START, PARENT_ID, START) } - connect(nicId, END, PARENT_ID, END, horizontalMargin) + connect(nicId, END, PARENT_ID, END) constrainHeight(nicId, height) } @@ -125,5 +166,6 @@ constructor( override fun removeViews(constraintLayout: ConstraintLayout) { constraintLayout.removeView(nicId) nicBindingDisposable?.dispose() + context.contentResolver.unregisterContentObserver(clockSettingsObserver) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 6b278212c50a3..cc5f7081c2b53 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -29,7 +29,9 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.VISIBLE +import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT +import com.android.systemui.customization.R as custR import com.android.systemui.customization.clocks.R as clocksR import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor @@ -47,6 +49,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shared.R as sharedR +import com.android.systemui.shared.clocks.useAxClocks import com.android.systemui.util.ui.value import dagger.Lazy import javax.inject.Inject @@ -109,33 +112,31 @@ constructor( clock: ClockController, constraintSet: ConstraintSet, ): ConstraintSet { + val isLargeVisible = keyguardClockViewModel.isLargeClockVisible.value + val targetFace = getTargetClockFace(clock) + val nonTargetFace = getNonTargetClockFace(clock) + val targetViews = targetFace.views + val nonTargetViews = nonTargetFace.views // Add constraint between rootView and clockContainer applyDefaultConstraints(constraintSet) - getNonTargetClockFace(clock).applyConstraints(constraintSet) - getTargetClockFace(clock).applyConstraints(constraintSet) + nonTargetFace.applyConstraints(constraintSet) + targetFace.applyConstraints(constraintSet) // Add constraint between elements in clock and clock container return constraintSet.apply { - setVisibility(getTargetClockFace(clock).views, VISIBLE) - setVisibility(getNonTargetClockFace(clock).views, GONE) - setAlpha(getTargetClockFace(clock).views, 1F) - setAlpha(getNonTargetClockFace(clock).views, 0F) + setVisibility(targetViews, VISIBLE) + setVisibility(nonTargetViews, GONE) + setAlpha(targetViews, 1F) + setAlpha(nonTargetViews, 0F) if (!keyguardClockViewModel.isLargeClockVisible.value) { - if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value) { + if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value && !useAxClocks) { connect( sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM, ) - } else { - connect( - sharedR.id.bc_smartspace_view, - TOP, - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - BOTTOM, - ) } } else { if (aodBurnInViewModel.movement.value.scaleClockOnly) { @@ -193,6 +194,7 @@ constructor( } fun applyDefaultConstraints(constraints: ConstraintSet) { + val isLargeVisible = keyguardClockViewModel.isLargeClockVisible.value val guideline = if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID else R.id.split_shade_guideline @@ -201,7 +203,8 @@ constructor( connect(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, END, guideline, END) if ( com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout() && - !com.android.systemui.shared.Flags.clockReactiveVariants() + !com.android.systemui.shared.Flags.clockReactiveVariants() + && !useAxClocks ) { connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, @@ -221,7 +224,9 @@ constructor( ) } val largeClockTopMargin = - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + if (useAxClocks) { + keyguardClockViewModel.getLargeClockTopMargin() + } else if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { keyguardClockViewModel.getLargeClockTopMargin() + context.resources.getDimensionPixelSize( clocksR.dimen.enhanced_smartspace_height @@ -238,30 +243,33 @@ constructor( connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, TOP, - PARENT_ID, - TOP, + R.id.smart_space_barrier_bottom, + BOTTOM, largeClockTopMargin, ) - constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, WRAP_CONTENT) + constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, MATCH_CONSTRAINT) // The following two lines make LOCKSCREEN_CLOCK_VIEW_LARGE is constrained to available // height when it goes beyond constraints; otherwise, it use WRAP_CONTENT constrainHeight(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, WRAP_CONTENT) constrainMaxHeight(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, 0) - constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, WRAP_CONTENT) + constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, MATCH_CONSTRAINT) constrainHeight( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - context.resources.getDimensionPixelSize(clocksR.dimen.small_clock_height), + context.resources.getDimensionPixelSize(custR.dimen.clock_height), ) connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, START, PARENT_ID, START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize( - clocksR.dimen.status_view_margin_horizontal - ), + 0 + ) + connect( + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, + END, + guideline, + END, ) val smallClockTopMargin = keyguardClockViewModel.getSmallClockTopMargin() + context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) @@ -291,7 +299,7 @@ constructor( 0 } - if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value) { + if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value && !useAxClocks) { val dateWeatherSmartspaceHeight = context.resources .getDimensionPixelSize(clocksR.dimen.date_weather_view_height) @@ -310,6 +318,6 @@ constructor( } } - constrainWeatherClockDateIconsBarrier(constraints) + if (!useAxClocks) constrainWeatherClockDateIconsBarrier(constraints) } } diff --git a/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt b/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt index 271c35717b5be..07e3b7eb50d96 100644 --- a/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt +++ b/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt @@ -28,9 +28,17 @@ import com.android.axion.quicklook.IAxQuickLookService import com.android.axion.quicklook.IQuickLookCallback import com.android.axion.quicklook.QuickLookTarget import com.android.axion.quicklook.SportsData +import com.android.axion.quicklook.calendarData +import com.android.axion.quicklook.mediaData import com.android.axion.quicklook.nowPlayingData +import com.android.axion.quicklook.smartspaceData import com.android.axion.quicklook.sportsData +import com.android.axion.quicklook.weatherData import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.keyguard.ui.clocks.CalendarSimpleData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData +import com.android.systemui.plugins.keyguard.ui.clocks.SmartspaceData import com.android.systemui.util.WeakListenerManager import javax.inject.Inject @@ -40,6 +48,9 @@ class QuickLookClient @Inject constructor( ) { interface Callback { + fun onClockDataChanged(data: ClockData) {} + fun onQLPlaybackStateChanged(play: Boolean) {} + fun onQLMetadataChanged(track: String, artist: String, packageName: String) {} fun onNowPlayingUpdate(nowPlayingText: String, tapAction: PendingIntent?) {} fun onSportsUpdate(sports: List) {} } @@ -49,6 +60,11 @@ class QuickLookClient @Inject constructor( private var isBound = false private var rebindAttempts = 0 + private var cachedClockData: ClockData = ClockData.EMPTY + private var cachedMediaPlaying: Boolean = false + private var cachedTrack: String = "" + private var cachedArtist: String = "" + private var cachedMediaPackage: String = "" private var cachedNowPlaying: String = "" private var cachedNowPlayingAction: PendingIntent? = null private var cachedSports: List = emptyList() @@ -97,6 +113,13 @@ class QuickLookClient @Inject constructor( } private fun deliverCachedData(callback: Callback) { + if (cachedClockData != ClockData.EMPTY) { + callback.onClockDataChanged(cachedClockData) + } + callback.onQLPlaybackStateChanged(cachedMediaPlaying) + if (cachedTrack.isNotEmpty() || cachedArtist.isNotEmpty()) { + callback.onQLMetadataChanged(cachedTrack, cachedArtist, cachedMediaPackage) + } if (cachedNowPlaying.isNotEmpty()) { callback.onNowPlayingUpdate(cachedNowPlaying, cachedNowPlayingAction) } @@ -106,11 +129,63 @@ class QuickLookClient @Inject constructor( } private fun processTargets(targets: List) { + var weather: ClockWeatherData = ClockWeatherData.EMPTY + var calendar: CalendarSimpleData = CalendarSimpleData.EMPTY + var hasMedia = false var hasNowPlaying = false + val smartspaceTargets = mutableListOf() val sportsTargets = mutableListOf() for (target in targets) { when (target.targetType) { + QuickLookTarget.TYPE_WEATHER -> { + val w = target.weatherData ?: continue + weather = ClockWeatherData( + temp = w.temp, + condition = w.condition, + conditionCode = w.conditionCode, + city = w.city, + humidity = w.humidity, + wind = w.wind, + windDirection = w.windDirection, + tempUnit = w.tempUnit, + windUnit = w.windUnit, + pinWheel = w.pinWheel, + timestamp = w.timestamp, + iconBytes = w.iconBytes, + tintIcon = w.iconBytes == null, + tapAction = target.primaryAction?.pendingIntent, + ) + } + QuickLookTarget.TYPE_CALENDAR -> { + val c = target.calendarData ?: continue + calendar = CalendarSimpleData( + id = c.id, + title = c.title, + startTime = c.startTime, + endTime = c.endTime, + location = c.location, + description = c.description, + formattedTime = c.formattedTime, + eventStatus = c.eventStatus, + tapAction = target.primaryAction?.pendingIntent, + ) + } + QuickLookTarget.TYPE_MEDIA -> { + hasMedia = true + val m = target.mediaData ?: continue + + if (m.isPlaying != cachedMediaPlaying) { + cachedMediaPlaying = m.isPlaying + callbacks.notify { it.onQLPlaybackStateChanged(m.isPlaying) } + } + if (m.track != cachedTrack || m.artist != cachedArtist || m.packageName != cachedMediaPackage) { + cachedTrack = m.track + cachedArtist = m.artist + cachedMediaPackage = m.packageName ?: "" + callbacks.notify { it.onQLMetadataChanged(m.track, m.artist, cachedMediaPackage) } + } + } QuickLookTarget.TYPE_NOW_PLAYING -> { hasNowPlaying = true val np = target.nowPlayingData ?: continue @@ -131,15 +206,65 @@ class QuickLookClient @Inject constructor( "${s.team1Name} vs ${s.team2Name}" else -> target.title ?: "" } + if (sportTitle.isNotEmpty()) { + smartspaceTargets.add(SmartspaceData( + id = target.id, + title = sportTitle, + subtitle = s.statusDetail, + featureType = 9, + iconBytes = target.iconBytes, + componentName = null, + isSensitive = false, + sourceType = QuickLookTarget.TYPE_SPORTS, + creationTime = target.creationTime, + score = target.score, + tapAction = target.primaryAction?.pendingIntent, + )) + } + } + QuickLookTarget.TYPE_SMARTSPACER, + QuickLookTarget.TYPE_GOOGLE_SMARTSPACE -> { + val s = target.smartspaceData ?: continue + smartspaceTargets.add(SmartspaceData( + id = s.id, + title = s.title, + subtitle = s.subtitle, + featureType = s.featureType, + iconBytes = s.iconBytes, + componentName = s.componentName, + isSensitive = s.isSensitive, + sourceType = s.sourceType, + creationTime = s.creationTime, + score = s.score, + tapAction = target.primaryAction?.pendingIntent, + )) } } } + val newClockData = ClockData(weather, calendar, smartspaceTargets) + if (newClockData != cachedClockData) { + cachedClockData = newClockData + callbacks.notify { it.onClockDataChanged(newClockData) } + } + if (sportsTargets != cachedSports) { cachedSports = sportsTargets callbacks.notify { it.onSportsUpdate(sportsTargets) } } + if (!hasMedia) { + if (cachedMediaPlaying) { + cachedMediaPlaying = false + callbacks.notify { it.onQLPlaybackStateChanged(false) } + } + if (cachedTrack.isNotEmpty() || cachedArtist.isNotEmpty()) { + cachedTrack = "" + cachedArtist = "" + cachedMediaPackage = "" + callbacks.notify { it.onQLMetadataChanged("", "", "") } + } + } if (!hasNowPlaying && cachedNowPlaying.isNotEmpty()) { cachedNowPlaying = "" cachedNowPlayingAction = null @@ -191,3 +316,4 @@ class QuickLookClient @Inject constructor( private const val MAX_REBIND_DELAY_MS = 30000L } } + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 39a95756b66cb..670cc06107e5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -1007,6 +1007,10 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto mClipRect.set(0, -height, getWidth(), height); if (mShelfIcons != null) { mShelfIcons.setClipBounds(mClipRect); + if (mAmbientState.isDozing() && mShelfIcons.shouldCenterIcons()) { + mShelfIcons.calculateIconXTranslations(); + mShelfIcons.applyIconStates(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 28271e21e23f9..badf63521bc3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -29,6 +29,7 @@ import android.graphics.Rect; import android.graphics.drawable.Icon; import android.os.Handler; +import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.util.AttributeSet; @@ -46,6 +47,7 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.settingslib.Utils; import com.android.systemui.res.R; +import com.android.systemui.shared.clocks.ClockSettingsRepository; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.stack.AnimationFilter; @@ -170,6 +172,8 @@ public AnimationFilter getAnimationFilter() { private int mThemedTextColorPrimaryInverse; @Nullable private Runnable mIsolatedIconAnimationEndRunnable; private boolean mUseIncreasedIconScale; + private boolean mShouldCenterIcons = false; + private final ContentObserver mClockSettingsObserver; private SettingsObserver mSettingsObserver; @@ -177,6 +181,12 @@ public NotificationIconContainer(Context context, AttributeSet attrs) { super(context, attrs); initResources(); setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW)); + mClockSettingsObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + updateShouldCenterIcons(); + } + }; } private void initResources() { @@ -632,7 +642,23 @@ protected float getRightBound() { * @return The left boundary (not the RTL compatible start) of the area that icons can be added. */ protected float getLeftBound() { - return getActualPaddingStart(); + float basePadding = getActualPaddingStart(); + if (mShouldCenterIcons && getChildCount() > 0) { + float iconsWidth = 0; + int childCount = getChildCount(); + int maxVisibleIcons = mMaxIcons; + int showCount = Math.min(childCount, maxVisibleIcons); + for (int i = 0; i < showCount; i++) { + View child = getChildAt(i); + iconsWidth += child.getWidth() * getDrawingScale(child); + } + if (childCount > maxVisibleIcons) { + iconsWidth += mStaticDotDiameter + mDotPadding; + } + float centeredPadding = Math.max(0, (getWidth() - iconsWidth) / 2); + return centeredPadding; + } + return basePadding; } protected float getActualPaddingEnd() { @@ -945,4 +971,35 @@ public void initFrom(View view) { } } } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + ClockSettingsRepository.init(getContext()); + getContext().getContentResolver().registerContentObserver( + ClockSettingsRepository.clockFaceUri, + false, + mClockSettingsObserver + ); + getContext().getContentResolver().registerContentObserver( + ClockSettingsRepository.alignmentUri, + false, + mClockSettingsObserver + ); + updateShouldCenterIcons(); + } + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getContext().getContentResolver().unregisterContentObserver(mClockSettingsObserver); + } + private void updateShouldCenterIcons() { + final boolean shouldCenter = ClockSettingsRepository.shouldCenterIcons(getContext()); + if (mShouldCenterIcons != shouldCenter) { + mShouldCenterIcons = shouldCenter; + requestLayout(); + } + } + public boolean shouldCenterIcons() { + return mShouldCenterIcons; + } } diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index e3c3c8ccd503e..d4a9a761c12e0 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -1579,6 +1579,7 @@ private String checkContentProviderPermission(ProviderInfo cpi, int callingPid, return "ContentProvider access not allowed from sdk sandbox UID. " + "ProviderInfo: " + cpi.toString(); } + if ("com.android.systemui".equals(appName)) return null; boolean checkedGrants = false; if (checkUser) { // Looking for cross-user grants before enforcing the typical cross-users permissions From e0158046781c7b989a783bc323b3e8b3d4cdaa44 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:58:48 +0800 Subject: [PATCH 02/51] [ax_clocks|bugfix] fixing clock issues the enlarge clock settings size scale was getting applied to the large clock (should not be applied) https: //github.com/AxionAOSP/issue_tracker/issues/141 https: //github.com/AxionAOSP/issue_tracker/issues/126 Change-Id: Iaa2794523837c8184079b43e7f42ffb85d2a1abb Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> --- .../clocks/DefaultClockFaceLayout.kt | 2 +- .../customization/res/values/axion_dimens.xml | 1 - .../systemui/shared/clocks/ClockConfigs.kt | 12 +- .../shared/clocks/ClockSettingsRepository.kt | 2 +- .../shared/clocks/view/AxClockHost.kt | 3 +- .../shared/clocks/view/AxClockView.kt | 88 +++++--- .../shared/clocks/view/AxionAgeClockView.kt | 10 +- .../view/BitmapDigitComposeClockView.kt | 212 +++++++++++------- .../shared/clocks/view/ClockCanvasHelpers.kt | 6 + .../shared/clocks/view/CyberpunkClockView.kt | 9 +- .../shared/clocks/view/GeneralClockView.kt | 83 ++++--- .../clocks/view/OldQuickLookClockView.kt | 5 +- .../shared/clocks/view/QuickLookDateArea.kt | 19 +- .../android/keyguard/ClockEventController.kt | 2 +- 14 files changed, 293 insertions(+), 161 deletions(-) diff --git a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt index 514d42b103895..d5696aabd6088 100644 --- a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt +++ b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt @@ -83,7 +83,7 @@ open class DefaultClockFaceLayout(val view: View) : ClockFaceLayout { override fun LockscreenScope.LockscreenElement() { clockView( view, - Modifier.wrapContentSize() + Modifier.wrapContentSize(unbounded = true) .then(contentScope.largeClockModifier()) .then(context.burnInModifier), ) diff --git a/packages/SystemUI/customization/res/values/axion_dimens.xml b/packages/SystemUI/customization/res/values/axion_dimens.xml index b48b06e030854..7539ad1485d45 100644 --- a/packages/SystemUI/customization/res/values/axion_dimens.xml +++ b/packages/SystemUI/customization/res/values/axion_dimens.xml @@ -73,7 +73,6 @@ 36dp - 400dp 4dp 16dp diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt index 055158813a956..0d190f971ac1f 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt @@ -35,13 +35,14 @@ object ClockConfigs { enum class Position { ABOVE, BELOW } enum class Align { LEFT, CENTER, RIGHT } - fun resolveConfig(context: Context, className: String, isLarge: Boolean = false): ClockStyleConfig? { + fun resolveConfig(context: Context, className: String, isLarge: Boolean = false): ClockStyleConfig? = + resolveConfig(className, isLarge, ClockSettingsRepository.alignment.value) + + fun resolveConfig(className: String, isLarge: Boolean, alignValue: String): ClockStyleConfig? { val key = if (isLarge) "${className}_large" else className val base = clockConfigMap[key] ?: clockConfigMap[className] ?: return null if (base.position == Position.BELOW) return base - val alignValue = ClockSettingsRepository.alignment.value - val resolvedAlign = when (alignValue) { ALIGNMENT_LEFT -> Align.LEFT ALIGNMENT_CENTER -> Align.CENTER @@ -80,13 +81,11 @@ object ClockConfigs { Position.BELOW, Align.CENTER, visible = false, - customHeightRes = R.dimen.large_clock_height, ), "GeneralClockView_large" to ClockStyleConfig( Position.BELOW, Align.CENTER, visible = false, - customHeightRes = R.dimen.large_clock_height, ), "BitmapDigitComposeClockView" to ClockStyleConfig( Position.ABOVE, @@ -97,7 +96,6 @@ object ClockConfigs { Position.BELOW, Align.CENTER, visible = false, - customHeightRes = R.dimen.large_clock_height, ), "CyberpunkClockView" to ClockStyleConfig( Position.ABOVE, @@ -108,7 +106,6 @@ object ClockConfigs { Position.BELOW, Align.CENTER, visible = false, - customHeightRes = R.dimen.large_clock_height, ), "AxionAgeClockView" to ClockStyleConfig( Position.ABOVE, @@ -119,7 +116,6 @@ object ClockConfigs { Position.BELOW, Align.CENTER, visible = false, - customHeightRes = R.dimen.large_clock_height, ), ) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt index f577a00b8d827..60a2d8cd62126 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt @@ -44,7 +44,7 @@ object ClockSettingsRepository { const val DATE_POSITION_ABOVE = "above" const val DATE_POSITION_BELOW = "below" - private const val LARGE_SIZE_SCALE = 1.15f + private const val LARGE_SIZE_SCALE = 1.4f @JvmField val clockFaceUri: Uri = Settings.Secure.getUriFor(SETTING_CLOCK_FACE) @JvmField val alignmentUri: Uri = Settings.Secure.getUriFor(SETTING_ALIGNMENT) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt index d7d98b3a0c55f..d323fb69bce76 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt @@ -113,10 +113,9 @@ class AxClockHost(private val clock: AxClockView) { Box( modifier = sizeModifier .graphicsLayer { - val s = clock.sizeScale val a = animScale.value scaleX = a - scaleY = s * a + scaleY = a when (align) { ClockSettingsRepository.ALIGNMENT_LEFT -> transformOrigin = TransformOrigin(0f, 0.5f) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt index 82a33792d691f..f3770a26c1f3f 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt @@ -25,14 +25,21 @@ import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.ViewGroup +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -98,13 +105,17 @@ abstract class AxClockView @JvmOverloads constructor( val clockDateTextSize get() = context.scaledDimen(R.dimen.clock_date_text_size) val clockDateMarginTop get() = context.scaledDimen(R.dimen.clock_date_margin_top) val scaleRatio get() = context.scaleRatio - val sizeScale get() = if (isPreviewMode) 1f else ClockSettingsRepository.sizeScale.value + val sizeScale get() = when { + isPreviewMode -> 1f + isLargeClock -> 1f + else -> ClockSettingsRepository.sizeScale.value + } val iconSize get() = context.scaledDimenInt(R.dimen.clock_icon_secondary_size) protected val config: ClockConfigs.ClockStyleConfig? get() { val className = this::class.simpleName ?: return null - return ClockConfigs.resolveConfig(context, className, isLargeClock) + return ClockConfigs.resolveConfig(className, isLargeClock, state.alignmentState.value) } val isLeftAligned: Boolean get() = config?.align == ClockConfigs.Align.LEFT @@ -258,24 +269,26 @@ abstract class AxClockView @JvmOverloads constructor( override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val w = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec) - if (isLargeClock) { - val maxH = MeasureSpec.getSize(heightMeasureSpec) - val cv = host.view + val cv = host.view + val mode = MeasureSpec.getMode(heightMeasureSpec) + val maxH = MeasureSpec.getSize(heightMeasureSpec) + + cv.measure( + MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + ) + val naturalH = cv.measuredHeight + val floor = if (isLargeClock) 0 else clockHeight + val finalH = when (mode) { + MeasureSpec.EXACTLY -> maxOf(naturalH, maxH, floor) + else -> maxOf(naturalH, floor) + } + setMeasuredDimension(w, finalH) + if (w > 0 && finalH > 0 && finalH != naturalH) { cv.measure( MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(maxH, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(finalH, MeasureSpec.EXACTLY), ) - setMeasuredDimension(w, cv.measuredHeight) - } else { - setMeasuredDimension(w, clockHeight) - val mw = measuredWidth - val mh = measuredHeight - if (mw > 0 && mh > 0) { - host.view.measure( - MeasureSpec.makeMeasureSpec(mw, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(mh, MeasureSpec.EXACTLY), - ) - } } } @@ -315,19 +328,35 @@ abstract class AxClockView @JvmOverloads constructor( protected fun tintColor(isDoze: Boolean, screenOff: Boolean, regionDark: Boolean): Color = viewModel.tintColor(isDoze, screenOff, regionDark) + @Composable + protected fun inverseSizeScaleModifier(): Modifier = Modifier + + @Composable + protected fun digitScaleModifier(): Modifier { + if (isLargeClock || isPreviewMode) return Modifier + val scaleValue by ClockSettingsRepository.sizeScale.collectAsState() + if (scaleValue == 1f) return Modifier + return Modifier.graphicsLayer { + scaleX = scaleValue + scaleY = scaleValue + } + } + protected val fidgetTapModifier: Modifier - @Composable get() { - val display = viewModel.rememberResolvedDisplay() - return Modifier.pointerInput(touchEnabled, display.tapAction) { - if (!touchEnabled) return@pointerInput - detectTapGestures { offset -> - if (!state.isDoze) { - onFidgetTapListener?.invoke(offset.x, offset.y) - display.tapAction?.let { fireTapAction(it) } - } - } + @Composable get() = fidgetTapModifierFor(null) + + @Composable + protected fun fidgetTapModifierFor(boundsProvider: (() -> Rect?)?): Modifier { + return Modifier.pointerInput(touchEnabled) { + if (!touchEnabled) return@pointerInput + detectTapGestures { offset -> + if (state.isDoze) return@detectTapGestures + val bounds = boundsProvider?.invoke() + if (bounds != null && !bounds.contains(offset)) return@detectTapGestures + onFidgetTapListener?.invoke(offset.x, offset.y) } } + } @Composable protected fun DigitArea( @@ -357,11 +386,12 @@ abstract class AxClockView @JvmOverloads constructor( }, ) { val display = viewModel.rememberResolvedDisplay() + val inverseModifier = inverseSizeScaleModifier() QuickLookDateArea( - modifier = modifier, + modifier = modifier.then(inverseModifier), display = display, dateStr = state.dateStr, - sizeScale = sizeScale, + sizeScale = 1f, textColor = textColor, textSize = textSize, fontFamily = fontFamily, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt index 82879b405e4ca..78490738bcc75 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -52,6 +53,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.android.systemui.shared.clocks.ClockSettingsRepository import kotlinx.coroutines.launch class AxionAgeClockView @JvmOverloads constructor( @@ -99,9 +101,11 @@ class AxionAgeClockView @JvmOverloads constructor( val fidgetValue = weightFidget.value val textColor = tintColor(isDoze, screenOff, regionDark) val isDark = isDoze || regionDark - val digitW = if (large) 90.dp else 48.dp - val digitH = if (large) 150.dp else 108.dp - val digitSpacing = if (large) 8.dp else 2.dp + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val sz = if (large) 1f else dynSizeScale + val digitW = (if (large) 90.dp else 48.dp) * sz + val digitH = (if (large) 150.dp else 108.dp) * sz + val digitSpacing = (if (large) 8.dp else 2.dp) * sz val digitStroke = if (large) 1.5.dp else 1.dp val glowExtra = if (large) 4f else 3f val infoTextSize = if (large) 18.sp else 14.sp diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt index d67e8c23797fd..7b71381b57bf1 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt @@ -55,10 +55,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Rect as ComposeRect import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -73,11 +75,19 @@ import androidx.compose.foundation.basicMarquee import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.ClockSettingsRepository import com.android.systemui.shared.clocks.extensions.createBitmaps import com.android.systemui.shared.clocks.extensions.scaledDimen import com.android.systemui.shared.clocks.extensions.scaleRatio import kotlin.math.min +private data class LargeFontLayout( + val visualTopOffset: Float, + val visualHeight: Float, + val cellWidth: Float, + val canvasHeightDp: Float, +) + class BitmapDigitComposeClockView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -105,7 +115,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( private var maxRadius = -1 private var tapPos = Point(0, 0) private var dozingAmountChanging = false - private var fidgetWeightAnimator: ValueAnimator? = null + private var fidgetWeightAnimator: AnimatorSet? = null private val weightVersion = mutableIntStateOf(0) private val screenWidth = context.resources.displayMetrics.widthPixels private val screenHeight = context.resources.displayMetrics.heightPixels @@ -226,17 +236,18 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( config.digitLightResIds?.let { loadDigitBitmaps(it) } } - val scale = context.scaleRatio * config.smallScaleMultiplier + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val scale = context.scaleRatio * config.smallScaleMultiplier * dynSizeScale val spacing = if (config.negativeSpacing) { - -context.scaledDimen(config.digitSpacingRes) + -context.scaledDimen(config.digitSpacingRes) * dynSizeScale } else { - context.scaledDimen(config.digitSpacingRes) + context.scaledDimen(config.digitSpacingRes) * dynSizeScale } - val overlapPadding = if (config.overlapSpacingRes != 0) context.scaledDimen(config.overlapSpacingRes) else 0f + val overlapPadding = if (config.overlapSpacingRes != 0) context.scaledDimen(config.overlapSpacingRes) * dynSizeScale else 0f - val dotSize = context.scaledDimen(config.dotSizeRes) - val dotMargin = context.scaledDimen(config.dotMarginRes) - val dotCenterMargin = context.scaledDimen(config.dotCenterMarginRes) + val dotSize = context.scaledDimen(config.dotSizeRes) * dynSizeScale + val dotMargin = context.scaledDimen(config.dotMarginRes) * dynSizeScale + val dotCenterMargin = context.scaledDimen(config.dotCenterMarginRes) * dynSizeScale val tintColor = androidColorToComposeColor( config.clockColor(isDoze, screenOff, regionDark) @@ -252,28 +263,39 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + val bitmapDigitBounds = remember { mutableStateOf(null) } SmallShell(config, dateColor) { Canvas( modifier = Modifier .fillMaxWidth() .height(canvasHeightDp) - .then(fidgetTapModifier), + .then(fidgetTapModifierFor { bitmapDigitBounds.value }), ) { state.clockColorOverrideState.value if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas - val totalWidth = computeTotalWidth( + val naturalWidth = computeTotalWidth( time, bitmaps, scale, spacing, overlapPadding, config, dotCenterMargin ) - if (totalWidth <= 0f) return@Canvas - - val finalWidth = minOf(totalWidth, size.width) + if (naturalWidth <= 0f) return@Canvas + + val fitScale = fitToWidth(naturalWidth) + val drawScale = scale * fitScale + val drawSpacing = spacing * fitScale + val drawOverlap = overlapPadding * fitScale + val drawDotSize = dotSize * fitScale + val drawDotMargin = dotMargin * fitScale + val drawDotCenterMargin = dotCenterMargin * fitScale + val finalWidth = naturalWidth * fitScale val startX = when { isLeftAligned -> clockPaddingStart isRightAligned -> size.width - clockPaddingStart - finalWidth else -> (size.width - finalWidth) / 2f } + bitmapDigitBounds.value = ComposeRect( + startX, 0f, startX + finalWidth, size.height + ) val minuteStartIndex = if (time.length == 4) 2 else 1 val minuteTint = if (config.minuteAlpha < 1f && !isDoze && !screenOff) { @@ -288,21 +310,21 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val bitmapMap = if (useLightVariant && lightBitmaps != null) lightBitmaps else bitmaps val bitmap = bitmapMap[char] ?: return@forEachIndexed - val yOffset = (size.height - bitmap.height * scale) / 2f + val yOffset = (size.height - bitmap.height * drawScale) / 2f val digitTint = if (index >= minuteStartIndex) minuteTint else tintColor - drawScaledBitmap(bitmap, x, yOffset, scale, digitTint) + drawScaledBitmap(bitmap, x, yOffset, drawScale, digitTint) - x += bitmap.width * scale - x += getCustomSpacing(config, time, index, spacing, overlapPadding) + x += bitmap.width * drawScale + x += getCustomSpacing(config, time, index, drawSpacing, drawOverlap) if (shouldDrawSeparator(config, time, index)) { x += drawDotSeparator( - x, yOffset, bitmap.height * scale, - dotSize, dotMargin, dotCenterMargin, tintColor + x, yOffset, bitmap.height * drawScale, + drawDotSize, drawDotMargin, drawDotCenterMargin, tintColor ) } else if (index < time.lastIndex) { - x += spacing + x += drawSpacing } } } @@ -313,7 +335,8 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( private fun SmallFontContent(config: BitmapFaceConfig, mode: RenderMode.FontDigit) { val (time, _, isDoze, screenOff, regionDark) = rememberClockState() - val scale = context.scaleRatio + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val scale = context.scaleRatio * dynSizeScale val canvasHeight = remember(scale) { fontPaint.textSize = mode.fontSize * scale val metrics = fontPaint.fontMetrics @@ -322,30 +345,42 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + val fontDigitBounds = remember { mutableStateOf(null) } SmallShell(config, dateColor) { Canvas( modifier = Modifier .fillMaxWidth() .height(canvasHeightDp) - .then(fidgetTapModifier), + .then(fidgetTapModifierFor { fontDigitBounds.value }), ) { weightVersion.intValue state.clockColorOverrideState.value if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas val nativeCanvas = drawContext.canvas.nativeCanvas - fontPaint.textSize = mode.fontSize * scale - fontPaint.color = config.clockColor(isDoze, screenOff, regionDark) - val displayTime = formatDisplayTime(time) if (displayTime.isEmpty()) return@Canvas - val digitWidths = FloatArray(displayTime.length) { i -> + fontPaint.textSize = mode.fontSize * scale + fontPaint.color = config.clockColor(isDoze, screenOff, regionDark) + + val naturalWidths = FloatArray(displayTime.length) { i -> fontPaint.typeface = getCachedTypeface( fontWeights.getOrElse(i) { mode.lsFontWeight }, mode.fontPath ) fontPaint.measureText(displayTime[i].toString()) } + val naturalWidth = naturalWidths.sum() + val fitScale = fitToWidth(naturalWidth) + if (fitScale < 1f) fontPaint.textSize = mode.fontSize * scale * fitScale + val digitWidths = if (fitScale < 1f) { + FloatArray(displayTime.length) { i -> + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(i) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.measureText(displayTime[i].toString()) + } + } else naturalWidths val totalWidth = digitWidths.sum() val metrics = fontPaint.fontMetrics @@ -362,12 +397,17 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( fontPaint.getTextBounds(displayTime[0].toString(), 0, 1, firstBounds) val leftBearing = firstBounds.left.toFloat() - var x = when { + val startX = when { isLeftAligned -> clockPaddingStart - leftBearing isRightAligned -> size.width - clockPaddingStart - totalWidth isWakingUp -> (size.width / 2f) + (totalWidth / 2f) else -> (size.width - totalWidth) / 2f } + fontDigitBounds.value = ComposeRect( + (size.width - totalWidth) / 2f, 0f, + (size.width + totalWidth) / 2f, size.height, + ) + var x = startX for (i in indices) { if (isWakingUp && !isSideAligned) { @@ -407,6 +447,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( .weight(1f) .analogDrawModifier(config, time, isDoze, screenOff, regionDark), ) + Spacer(modifier = Modifier.height(ANALOG_DATE_GAP_DP.dp)) } } @@ -496,6 +537,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val canvasHeightDp = with(LocalDensity.current) { (digitHeightPx * 2 + lineSpacing).toDp() } + val largeBitmapBounds = remember { mutableStateOf(null) } Column( modifier = Modifier.fillMaxWidth().wrapContentHeight(), @@ -505,7 +547,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( modifier = Modifier .fillMaxWidth() .height(canvasHeightDp) - .then(fidgetTapModifier), + .then(fidgetTapModifierFor { largeBitmapBounds.value }), ) { state.clockColorOverrideState.value if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas @@ -519,6 +561,12 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val sampleBitmap = bitmaps['0'] ?: return@Canvas val digitHeight = sampleBitmap.height * scale + val maxW = maxOf(hoursWidth, minutesWidth) + largeBitmapBounds.value = ComposeRect( + (size.width - maxW) / 2f, 0f, + (size.width + maxW) / 2f, size.height, + ) + val hoursX = (size.width - hoursWidth) / 2f val minutesX = (size.width - minutesWidth) / 2f @@ -546,16 +594,33 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val scale = context.scaleRatio val largeFontSize = mode.fontSize * scale * mode.largeScale - val canvasHeightDp = remember(largeFontSize, mode.lineSpacing, scale) { + val fontLayout = remember(largeFontSize, mode.lineSpacing, scale, mode.fontPath, mode.lsFontWeight) { val tempPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { textSize = largeFontSize typeface = getCachedTypeface(mode.lsFontWeight, mode.fontPath) } - val metrics = tempPaint.fontMetrics - val lineHeight = metrics.descent - metrics.ascent - val totalPx = lineHeight * 2 + mode.lineSpacing * scale - totalPx / context.resources.displayMetrics.density + val bounds = Rect() + var topMin = Int.MAX_VALUE + var bottomMax = Int.MIN_VALUE + var advanceMax = 0f + for (d in '0'..'9') { + tempPaint.getTextBounds(d.toString(), 0, 1, bounds) + if (bounds.top < topMin) topMin = bounds.top + if (bounds.bottom > bottomMax) bottomMax = bounds.bottom + val adv = tempPaint.measureText(d.toString()) + if (adv > advanceMax) advanceMax = adv + } + val visualHeight = (bottomMax - topMin).toFloat() + val canvasPx = visualHeight * 2f + mode.lineSpacing * scale + LargeFontLayout( + visualTopOffset = topMin.toFloat(), + visualHeight = visualHeight, + cellWidth = advanceMax, + canvasHeightDp = canvasPx / context.resources.displayMetrics.density, + ) } + val canvasHeightDp = fontLayout.canvasHeightDp + val largeFontBounds = remember { mutableStateOf(null) } Column( modifier = Modifier.fillMaxWidth().wrapContentHeight(), @@ -565,7 +630,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( modifier = Modifier .fillMaxWidth() .height(canvasHeightDp.dp) - .then(fidgetTapModifier), + .then(fidgetTapModifierFor { largeFontBounds.value }), ) { weightVersion.intValue state.clockColorOverrideState.value @@ -573,6 +638,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val nativeCanvas = drawContext.canvas.nativeCanvas fontPaint.textSize = largeFontSize + fontPaint.typeface = getCachedTypeface(mode.lsFontWeight, mode.fontPath) fontPaint.color = config.clockColor(isDoze, screenOff, regionDark) val displayTime = formatDisplayTime(time) @@ -581,45 +647,34 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val (hours, minutes) = splitTimeLines(displayTime) if (hours.isEmpty()) return@Canvas - val metrics = fontPaint.fontMetrics - val lineHeight = metrics.descent - metrics.ascent - val startY = -metrics.ascent - - var globalIdx = 0 - var hx = 0f - val hourWidths = FloatArray(hours.length) { i -> - fontPaint.typeface = getCachedTypeface( - fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath - ) - fontPaint.measureText(hours[i].toString()) - } - val hoursWidth = hourWidths.sum() - hx = (size.width - hoursWidth) / 2f - for (i in hours.indices) { - fontPaint.typeface = getCachedTypeface( - fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath - ) - nativeCanvas.drawText(hours[i].toString(), hx, startY, fontPaint) - hx += hourWidths[i] + val cellWidth = fontLayout.cellWidth + val hoursBaselineY = -fontLayout.visualTopOffset + val minutesBaselineY = hoursBaselineY + fontLayout.visualHeight + mode.lineSpacing * scale + + fun drawCenteredLine(digits: String, baselineY: Float, weightStart: Int) { + if (digits.isEmpty()) return + val lineWidth = cellWidth * digits.length + var cellX = (size.width - lineWidth) / 2f + for (i in digits.indices) { + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(weightStart + i) { mode.lsFontWeight }, + mode.fontPath, + ) + val glyphW = fontPaint.measureText(digits[i].toString()) + val glyphX = cellX + (cellWidth - glyphW) / 2f + nativeCanvas.drawText(digits[i].toString(), glyphX, baselineY, fontPaint) + cellX += cellWidth + } } - globalIdx += hours.length - val minWidths = FloatArray(minutes.length) { i -> - fontPaint.typeface = getCachedTypeface( - fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath - ) - fontPaint.measureText(minutes[i].toString()) - } - val minutesWidth = minWidths.sum() - var mx = (size.width - minutesWidth) / 2f - val minY = startY + lineHeight + mode.lineSpacing * scale - for (i in minutes.indices) { - fontPaint.typeface = getCachedTypeface( - fontWeights.getOrElse(globalIdx + i) { mode.lsFontWeight }, mode.fontPath - ) - nativeCanvas.drawText(minutes[i].toString(), mx, minY, fontPaint) - mx += minWidths[i] - } + drawCenteredLine(hours, hoursBaselineY, 0) + drawCenteredLine(minutes, minutesBaselineY, hours.length) + + val maxLineW = cellWidth * maxOf(hours.length, minutes.length) + largeFontBounds.value = ComposeRect( + (size.width - maxLineW) / 2f, 0f, + (size.width + maxLineW) / 2f, size.height, + ) } Spacer(modifier = Modifier.height(16.dp)) @@ -689,14 +744,17 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val cy = size.height / 2f val color = config.clockColor(isDoze, screenOff, regionDark) val highlightColor = ContextCompat.getColor(context, R.color.clock_dot_color) - val scale = context.scaleRatio - val dotSize = context.scaledDimen(R.dimen.dot_size) - val handSize = context.scaledDimen(R.dimen.clock_hand_size) - val bitmaps = createBitmaps(context, config.tickResIds) val tick = bitmaps.getOrNull(0) val tickLight = bitmaps.getOrNull(1) + val baseScale = context.scaleRatio * sizeScale + val tickNaturalW = (tick?.width ?: 0) * baseScale + val fitScale = fitToWidth(tickNaturalW) + val scale = baseScale * fitScale + val dotSize = context.scaledDimen(R.dimen.dot_size) * sizeScale * fitScale + val handSize = context.scaledDimen(R.dimen.clock_hand_size) * sizeScale * fitScale + if (tick != null && tickLight != null) { tickPaint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) val tw = tick.width * scale @@ -876,8 +934,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( } } - fidgetWeightAnimator = squeeze - AnimatorSet().apply { + fidgetWeightAnimator = AnimatorSet().apply { playSequentially(squeeze, restore) start() } @@ -896,6 +953,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( companion object { private const val MAX_TABLET_SCALE = 1.2f private const val DATE_ROW_DP = 24f + private const val ANALOG_DATE_GAP_DP = 16f private const val DIGIT_COUNT = 4 private const val WEIGHT_SNAP = 10 private const val RIPPLE_ANIM_MS = 500L diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt index a454ba0ebc61c..9635440ad9a94 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt @@ -31,6 +31,12 @@ import androidx.compose.ui.unit.IntSize import kotlin.math.cos import kotlin.math.sin +internal fun DrawScope.fitToWidth(naturalWidth: Float, marginDp: Float = 16f): Float { + val marginPx = marginDp * density + val available = (size.width - 2f * marginPx).coerceAtLeast(1f) + return if (naturalWidth > available) available / naturalWidth else 1f +} + internal fun DrawScope.drawScaledBitmap( bitmap: Bitmap, x: Float, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt index 725c94d97ab71..9992e48cbf9f5 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt @@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding @@ -37,8 +39,10 @@ import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import com.android.systemui.shared.clocks.ClockSettingsRepository import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -201,7 +205,7 @@ class CyberpunkClockView @JvmOverloads constructor( ) { Column( modifier = Modifier - .fillMaxWidth() + .wrapContentSize() .padding(horizontal = 16.dp) .then(fidgetTapModifier) .graphicsLayer { @@ -389,10 +393,11 @@ class CyberpunkClockView @JvmOverloads constructor( .padding(horizontal = 24.dp, vertical = 6.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() Text( text = time, style = TextStyle( - fontSize = 80.sp, + fontSize = 80.sp * dynSizeScale, fontWeight = FontWeight.Black, fontFamily = FontFamily.Monospace, color = primaryColor, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt index d380630872dab..a6a3884dfa21a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt @@ -35,11 +35,14 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color @@ -57,6 +60,7 @@ import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.ClockSettingsRepository import com.android.systemui.shared.clocks.extensions.scaledDimen import com.android.systemui.shared.clocks.extensions.scaleRatio import java.util.Locale @@ -140,6 +144,7 @@ class GeneralClockView @JvmOverloads constructor( val canvasHeightDp = with(LocalDensity.current) { (digitH * 2 + lineSpacing).toDp() } + val digitBoundsState = remember { mutableStateOf(null) } Column( modifier = Modifier.fillMaxWidth().wrapContentHeight(), @@ -149,7 +154,7 @@ class GeneralClockView @JvmOverloads constructor( modifier = Modifier .fillMaxWidth() .height(canvasHeightDp) - .then(fidgetTapModifier), + .then(fidgetTapModifierFor { digitBoundsState.value }), ) { if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas @@ -190,6 +195,14 @@ class GeneralClockView @JvmOverloads constructor( drawLine(hours, (size.width - hoursW) / 2f, 0f) drawLine(minutes, (size.width - minutesW) / 2f, dh + lineSpacing) + + val maxW = maxOf(hoursW, minutesW) + digitBoundsState.value = Rect( + left = (size.width - maxW) / 2f, + top = 0f, + right = (size.width + maxW) / 2f, + bottom = dh * 2 + lineSpacing, + ) } Spacer(modifier = Modifier.height(12.dp)) @@ -207,8 +220,9 @@ class GeneralClockView @JvmOverloads constructor( private fun SmallContent() { val (time, date, isDoze, screenOff, regionDark, icon, tintIcon, display) = rememberClockState() - val scale = context.scaleRatio - val paddingV = context.scaledDimen(R.dimen.clock_padding) + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val scale = context.scaleRatio * dynSizeScale + val paddingV = context.scaledDimen(R.dimen.clock_padding) * dynSizeScale val dotSz = context.scaledDimen(R.dimen.dot_small_size) val dotMgn = context.scaledDimen(R.dimen.dot_margin) @@ -238,6 +252,9 @@ class GeneralClockView @JvmOverloads constructor( } val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } + val inverseModifier = inverseSizeScaleModifier() + val smallDigitBounds = remember { mutableStateOf(null) } + Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, @@ -253,70 +270,79 @@ class GeneralClockView @JvmOverloads constructor( color = tintColor.copy(alpha = if (isDoze) 0.6f else 0.8f), letterSpacing = 0.5.sp, ), - modifier = Modifier.padding( - start = if (isRightAligned) 0.dp else sidePadding, - end = if (isRightAligned) sidePadding else 0.dp, - top = 4.dp, - bottom = 4.dp, - ), + modifier = Modifier + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + top = 4.dp, + bottom = 4.dp, + ) + .then(inverseModifier), ) Canvas( modifier = Modifier .fillMaxWidth() .height(canvasHeightDp) - .then(fidgetTapModifier), + .then(fidgetTapModifierFor { smallDigitBounds.value }), ) { if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas val bitmapMap = if (useLight) lightBitmaps else bitmaps - var totalWidth = 0f + var naturalWidth = 0f for ((i, char) in time.withIndex()) { val bmp = bitmapMap[char] ?: continue - totalWidth += bmp.width * scale - if (time.length - i == 3) totalWidth += 2 * paddingV + naturalWidth += bmp.width * scale + if (time.length - i == 3) naturalWidth += 2 * paddingV } - if (totalWidth <= 0f) return@Canvas - if (totalWidth > size.width) totalWidth = size.width + if (naturalWidth <= 0f) return@Canvas + + val fitScale = fitToWidth(naturalWidth) + val drawScale = scale * fitScale + val drawPaddingV = paddingV * fitScale + val totalWidth = naturalWidth * fitScale - var x = when { + val startX = when { isLeftAligned -> clockPaddingStart isRightAligned -> size.width - clockPaddingStart - totalWidth else -> (size.width - totalWidth) / 2f } + smallDigitBounds.value = Rect(startX, 0f, startX + totalWidth, size.height) + var x = startX for ((i, char) in time.withIndex()) { val bmp = bitmapMap[char] ?: continue - val yOffset = (size.height - bmp.height * scale) / 2f + val yOffset = (size.height - bmp.height * drawScale) / 2f drawImage( image = bmp.asImageBitmap(), srcOffset = IntOffset.Zero, srcSize = IntSize(bmp.width, bmp.height), dstOffset = IntOffset(x.toInt(), yOffset.toInt()), - dstSize = IntSize((bmp.width * scale).toInt(), (bmp.height * scale).toInt()), + dstSize = IntSize((bmp.width * drawScale).toInt(), (bmp.height * drawScale).toInt()), colorFilter = ColorFilter.tint(tintColor, BlendMode.SrcIn), ) - x += bmp.width * scale + x += bmp.width * drawScale if (time.length - i == 3) { - val centerX = x + paddingV + val centerX = x + drawPaddingV val centerY = size.height / 2f - val dotRadius = dotSz / 2 - val topDotY = centerY - (dotMgn / 2 + dotRadius) - val bottomDotY = centerY + (dotMgn / 2 + dotRadius) + val dotRadius = dotSz * fitScale / 2 + val topDotY = centerY - (dotMgn * fitScale / 2 + dotRadius) + val bottomDotY = centerY + (dotMgn * fitScale / 2 + dotRadius) + val adjDotSz = dotSz * fitScale drawOval( color = tintColor, topLeft = Offset(centerX - dotRadius, topDotY - dotRadius), - size = Size(dotSz, dotSz), + size = Size(adjDotSz, adjDotSz), ) drawOval( color = tintColor, topLeft = Offset(centerX - dotRadius, bottomDotY - dotRadius), - size = Size(dotSz, dotSz), + size = Size(adjDotSz, adjDotSz), ) - x += 2 * paddingV + x += 2 * drawPaddingV } } } @@ -330,6 +356,7 @@ class GeneralClockView @JvmOverloads constructor( textColor = tintColor, startPadding = sidePadding, horizontalAlign = horizontalAlign, + modifier = inverseModifier, ) } } @@ -344,6 +371,7 @@ class GeneralClockView @JvmOverloads constructor( textColor: Color, startPadding: androidx.compose.ui.unit.Dp, horizontalAlign: Alignment.Horizontal, + modifier: Modifier = Modifier, ) { Column( horizontalAlignment = horizontalAlign, @@ -354,7 +382,8 @@ class GeneralClockView @JvmOverloads constructor( start = if (isRightAligned) 0.dp else startPadding, end = if (isRightAligned) startPadding else 0.dp, bottom = 4.dp, - ), + ) + .then(modifier), ) { if (hasSpecialContent) { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt index a7866c0ccd160..ea8b103bd079a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,6 +48,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.android.systemui.customization.clocks.R as clocksR +import com.android.systemui.shared.clocks.ClockSettingsRepository import java.util.Locale class OldQuickLookClockView @JvmOverloads constructor( @@ -81,6 +83,7 @@ class OldQuickLookClockView @JvmOverloads constructor( private fun SmallContent() { val (time, date, isDoze, screenOff, regionDark, icon, tintIcon, display) = rememberClockState() + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() val textColor = tintColor(isDoze, screenOff, regionDark) val horizontalAlign = when { isLeftAligned -> Alignment.Start @@ -119,7 +122,7 @@ class OldQuickLookClockView @JvmOverloads constructor( text = time, maxLines = 1, style = TextStyle( - fontSize = primaryTextSize, + fontSize = primaryTextSize * dynSizeScale, fontWeight = FontWeight.Bold, fontFamily = clockFontFamily, color = textColor, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt index 333021737f26c..161f51f737b4d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt @@ -20,7 +20,8 @@ import android.app.PendingIntent import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -29,13 +30,13 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -72,11 +73,10 @@ fun QuickLookDateArea( val inverseScale = if (sizeScale > 0f) 1f / sizeScale else 1f val tapModifier = display.tapAction?.let { action -> - Modifier.pointerInput(action) { - detectTapGestures { - fireTapAction(action) - } - } + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { fireTapAction(action) } } ?: Modifier Row( @@ -84,7 +84,10 @@ fun QuickLookDateArea( horizontalArrangement = rowArrangement, modifier = modifier .widthIn(max = 280.dp) - .graphicsLayer { scaleY = inverseScale } + .graphicsLayer { + scaleX = inverseScale + scaleY = inverseScale + } .then(tapModifier), ) { if (display is DateDisplay.Weather) { diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index c5c56617c1cc4..13805cc26624c 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -727,7 +727,7 @@ constructor( } fun handleFidgetTap(x: Float, y: Float) { - if (isPreview || isCharging) return + if (isPreview) return clock?.run { smallClock.animations.onFidgetTap(x, y) largeClock.animations.onFidgetTap(x, y) From 587911bf32b449f56b9a1618cb01cacfe8ae5651 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 20 Apr 2026 05:41:21 +0800 Subject: [PATCH 03/51] fixup! [ax_clocks|bugfix] fixing clock issues --- .../shared/clocks/AxClockAnimations.kt | 4 +- .../shared/clocks/view/AxClockAnimator.kt | 13 ------ .../shared/clocks/view/AxClockConstants.kt | 6 --- .../shared/clocks/view/AxClockView.kt | 37 ---------------- .../shared/clocks/view/AxionAgeClockView.kt | 2 - .../view/BitmapDigitComposeClockView.kt | 44 ++----------------- .../clocks/view/ClockDepthController.kt | 38 ++++++++++++++++ .../shared/clocks/view/CyberpunkClockView.kt | 2 - .../shared/clocks/view/GeneralClockView.kt | 20 +-------- .../clocks/view/OldQuickLookClockView.kt | 4 +- .../android/keyguard/ClockEventController.kt | 13 ------ .../interactor/KeyguardClockInteractor.kt | 2 - 12 files changed, 46 insertions(+), 139 deletions(-) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt index 2fa1dc1ec8e02..f93511bb0b86d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt @@ -50,9 +50,7 @@ class AxClockAnimations( clockView.translationY = 0.5f * clockView.bottom * (1 - swipingFraction) } - override fun onFidgetTap(x: Float, y: Float) { - clockView.animateFidgetTap(x, y) - } + override fun onFidgetTap(x: Float, y: Float) {} override fun onPositionAnimated(args: ClockPositionAnimationArgs) {} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt index 2c4b0d09e66b8..1c8a48986de97 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt @@ -17,9 +17,6 @@ package com.android.systemui.shared.clocks.view import android.animation.ValueAnimator -import android.content.Context -import android.os.VibrationEffect -import android.os.Vibrator import android.util.Log import androidx.compose.ui.geometry.Offset import com.android.app.animation.Interpolators @@ -45,13 +42,3 @@ fun AxClockView.animateCharge() { onChargeAnimation() } -fun AxClockView.animateFidgetTapDefault(x: Float, y: Float) { - state.fidgetPosition.value = Offset(x, y) - state.fidgetTrigger.value = System.currentTimeMillis() - val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator - if (useGlitchInteraction) { - vibrator?.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)) - } else { - vibrator?.vibrate(FIDGET_HAPTICS) - } -} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt index 280e709536d16..f88a41a36fd34 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt @@ -16,7 +16,6 @@ package com.android.systemui.shared.clocks.view -import android.os.VibrationEffect import androidx.compose.animation.core.CubicBezierEasing internal const val FONT_FAMILY_BODY = "variable-title-small" @@ -34,11 +33,6 @@ internal const val CLOCK_PATTERN_ALL = "hhmmss" internal const val APPEAR_DURATION = 400L -internal val FIDGET_HAPTICS: VibrationEffect = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, 0) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43) - .compose() - const val PREVIEW_TIME_12 = "1008" const val PREVIEW_TIME_12_STANDARD = "10:08" const val PREVIEW_TIME_24 = "1008" diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt index f3770a26c1f3f..8a61175a94b64 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt @@ -25,22 +25,14 @@ import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.ViewGroup -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -92,7 +84,6 @@ abstract class AxClockView @JvmOverloads constructor( } var touchEnabled: Boolean = true - var onFidgetTapListener: ((Float, Float) -> Unit)? = null private val depthController = ClockDepthController(this) var depthEffectEnabled: Boolean @@ -219,8 +210,6 @@ abstract class AxClockView @JvmOverloads constructor( fun refreshDate() { interactor.refreshDate() } - open fun animateFidgetTap(x: Float, y: Float) = animateFidgetTapDefault(x, y) - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { if (!touchEnabled) return false return super.dispatchTouchEvent(ev) @@ -342,32 +331,6 @@ abstract class AxClockView @JvmOverloads constructor( } } - protected val fidgetTapModifier: Modifier - @Composable get() = fidgetTapModifierFor(null) - - @Composable - protected fun fidgetTapModifierFor(boundsProvider: (() -> Rect?)?): Modifier { - return Modifier.pointerInput(touchEnabled) { - if (!touchEnabled) return@pointerInput - detectTapGestures { offset -> - if (state.isDoze) return@detectTapGestures - val bounds = boundsProvider?.invoke() - if (bounds != null && !bounds.contains(offset)) return@detectTapGestures - onFidgetTapListener?.invoke(offset.x, offset.y) - } - } - } - - @Composable - protected fun DigitArea( - modifier: Modifier = Modifier, - content: @Composable () -> Unit, - ) { - Box(modifier = modifier.then(fidgetTapModifier)) { - content() - } - } - @Composable protected fun EnhancedDateArea( modifier: Modifier = Modifier, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt index 78490738bcc75..edacd475cd4cd 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt @@ -144,7 +144,6 @@ class AxionAgeClockView @JvmOverloads constructor( Column( verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.then(fidgetTapModifier), ) { Row(horizontalArrangement = Arrangement.spacedBy(digitSpacing)) { AxDigit(time[0], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) @@ -188,7 +187,6 @@ class AxionAgeClockView @JvmOverloads constructor( if (time.length >= 4) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.then(fidgetTapModifier), ) { AxDigit(time[0], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) Spacer(modifier = Modifier.width(digitSpacing)) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt index 7b71381b57bf1..0b9532026ca2c 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt @@ -55,12 +55,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Rect as ComposeRect import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -263,13 +261,11 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) - val bitmapDigitBounds = remember { mutableStateOf(null) } SmallShell(config, dateColor) { Canvas( modifier = Modifier .fillMaxWidth() - .height(canvasHeightDp) - .then(fidgetTapModifierFor { bitmapDigitBounds.value }), + .height(canvasHeightDp), ) { state.clockColorOverrideState.value if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas @@ -293,10 +289,6 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( isRightAligned -> size.width - clockPaddingStart - finalWidth else -> (size.width - finalWidth) / 2f } - bitmapDigitBounds.value = ComposeRect( - startX, 0f, startX + finalWidth, size.height - ) - val minuteStartIndex = if (time.length == 4) 2 else 1 val minuteTint = if (config.minuteAlpha < 1f && !isDoze && !screenOff) { tintColor.copy(alpha = config.minuteAlpha) @@ -345,13 +337,11 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) - val fontDigitBounds = remember { mutableStateOf(null) } SmallShell(config, dateColor) { Canvas( modifier = Modifier .fillMaxWidth() - .height(canvasHeightDp) - .then(fidgetTapModifierFor { fontDigitBounds.value }), + .height(canvasHeightDp), ) { weightVersion.intValue state.clockColorOverrideState.value @@ -403,10 +393,6 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( isWakingUp -> (size.width / 2f) + (totalWidth / 2f) else -> (size.width - totalWidth) / 2f } - fontDigitBounds.value = ComposeRect( - (size.width - totalWidth) / 2f, 0f, - (size.width + totalWidth) / 2f, size.height, - ) var x = startX for (i in indices) { @@ -537,8 +523,6 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val canvasHeightDp = with(LocalDensity.current) { (digitHeightPx * 2 + lineSpacing).toDp() } - val largeBitmapBounds = remember { mutableStateOf(null) } - Column( modifier = Modifier.fillMaxWidth().wrapContentHeight(), horizontalAlignment = Alignment.CenterHorizontally, @@ -546,8 +530,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( Canvas( modifier = Modifier .fillMaxWidth() - .height(canvasHeightDp) - .then(fidgetTapModifierFor { largeBitmapBounds.value }), + .height(canvasHeightDp), ) { state.clockColorOverrideState.value if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas @@ -561,12 +544,6 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val sampleBitmap = bitmaps['0'] ?: return@Canvas val digitHeight = sampleBitmap.height * scale - val maxW = maxOf(hoursWidth, minutesWidth) - largeBitmapBounds.value = ComposeRect( - (size.width - maxW) / 2f, 0f, - (size.width + maxW) / 2f, size.height, - ) - val hoursX = (size.width - hoursWidth) / 2f val minutesX = (size.width - minutesWidth) / 2f @@ -620,7 +597,6 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( ) } val canvasHeightDp = fontLayout.canvasHeightDp - val largeFontBounds = remember { mutableStateOf(null) } Column( modifier = Modifier.fillMaxWidth().wrapContentHeight(), @@ -629,8 +605,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( Canvas( modifier = Modifier .fillMaxWidth() - .height(canvasHeightDp.dp) - .then(fidgetTapModifierFor { largeFontBounds.value }), + .height(canvasHeightDp.dp), ) { weightVersion.intValue state.clockColorOverrideState.value @@ -669,12 +644,6 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( drawCenteredLine(hours, hoursBaselineY, 0) drawCenteredLine(minutes, minutesBaselineY, hours.length) - - val maxLineW = cellWidth * maxOf(hours.length, minutes.length) - largeFontBounds.value = ComposeRect( - (size.width - maxLineW) / 2f, 0f, - (size.width + maxLineW) / 2f, size.height, - ) } Spacer(modifier = Modifier.height(16.dp)) @@ -902,11 +871,6 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( tapPos = pos } - override fun animateFidgetTap(x: Float, y: Float) { - super.animateFidgetTap(x, y) - animateWeightPulse() - } - override fun onChargeAnimation() { animateWeightPulse() } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt index a444892e508f0..b12e0fdb41cfa 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt @@ -8,6 +8,8 @@ import com.android.systemui.shared.clocks.DepthWallpaperProvider import android.content.res.Resources import android.provider.Settings import android.util.DisplayMetrics +import kotlin.math.ceil +import kotlin.math.floor class ClockDepthController(private val view: View) { @@ -17,12 +19,16 @@ class ClockDepthController(private val view: View) { private var pathAspect: Float = 1f private var depthActive = false private var depthVisible = true + private var depthSuppressed = false private var maskAlpha = 0f private var revealProgress = 0f private var maskAnimator: ValueAnimator? = null private val transformedPath = Path() private val pathMatrix = Matrix() + private val coverageRegion = Region() + private val coverageClip = Region() + private val coverageDiff = Region() private val location = IntArray(2) private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE @@ -82,6 +88,7 @@ class ClockDepthController(private val view: View) { DepthWallpaperProvider.removeListener(listener) subjectPath = null depthActive = false + depthSuppressed = false } fun setDepthVisible(visible: Boolean) { @@ -168,6 +175,24 @@ class ClockDepthController(private val view: View) { return } + val suppressed = pathBounds.contains(layerRect) && pathFullyCovers(layerRect) + if (suppressed) { + if (maskAnimator?.isRunning == true) { + maskAnimator?.cancel() + } + maskAlpha = 0f + revealProgress = 0f + depthSuppressed = true + drawSuper(canvas) + return + } + if (depthSuppressed) { + depthSuppressed = false + if (depthActive && depthVisible) { + animateReveal() + } + } + if (revealProgress >= 1f && maskAlpha >= 1f) { canvas.save() canvas.clipOutPath(transformedPath) @@ -197,6 +222,19 @@ class ClockDepthController(private val view: View) { } } + private fun pathFullyCovers(layerRect: RectF): Boolean { + coverageClip.set( + floor(layerRect.left).toInt(), + floor(layerRect.top).toInt(), + ceil(layerRect.right).toInt(), + ceil(layerRect.bottom).toInt() + ) + coverageRegion.setPath(transformedPath, coverageClip) + coverageDiff.set(coverageClip) + coverageDiff.op(coverageRegion, Region.Op.DIFFERENCE) + return coverageDiff.isEmpty + } + private fun animateReveal() { maskAnimator?.cancel() maskAnimator = ValueAnimator.ofFloat(maskAlpha, 1f).apply { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt index 9992e48cbf9f5..22b60203788ba 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt @@ -121,7 +121,6 @@ class CyberpunkClockView @JvmOverloads constructor( ) { Box( modifier = Modifier - .then(fidgetTapModifier) .graphicsLayer { if (progress > 0f) { translationX = (if (glitchSeed > 0.5f) 8f else -8f) * progress @@ -207,7 +206,6 @@ class CyberpunkClockView @JvmOverloads constructor( modifier = Modifier .wrapContentSize() .padding(horizontal = 16.dp) - .then(fidgetTapModifier) .graphicsLayer { if (progress > 0f) { translationX = (if (glitchSeed > 0.5f) 15f else -15f) * progress diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt index a6a3884dfa21a..4c2c3ff34feb0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt @@ -37,12 +37,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color @@ -144,8 +142,6 @@ class GeneralClockView @JvmOverloads constructor( val canvasHeightDp = with(LocalDensity.current) { (digitH * 2 + lineSpacing).toDp() } - val digitBoundsState = remember { mutableStateOf(null) } - Column( modifier = Modifier.fillMaxWidth().wrapContentHeight(), horizontalAlignment = Alignment.CenterHorizontally, @@ -153,8 +149,7 @@ class GeneralClockView @JvmOverloads constructor( Canvas( modifier = Modifier .fillMaxWidth() - .height(canvasHeightDp) - .then(fidgetTapModifierFor { digitBoundsState.value }), + .height(canvasHeightDp), ) { if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas @@ -195,14 +190,6 @@ class GeneralClockView @JvmOverloads constructor( drawLine(hours, (size.width - hoursW) / 2f, 0f) drawLine(minutes, (size.width - minutesW) / 2f, dh + lineSpacing) - - val maxW = maxOf(hoursW, minutesW) - digitBoundsState.value = Rect( - left = (size.width - maxW) / 2f, - top = 0f, - right = (size.width + maxW) / 2f, - bottom = dh * 2 + lineSpacing, - ) } Spacer(modifier = Modifier.height(12.dp)) @@ -253,7 +240,6 @@ class GeneralClockView @JvmOverloads constructor( val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } val inverseModifier = inverseSizeScaleModifier() - val smallDigitBounds = remember { mutableStateOf(null) } Column( modifier = Modifier.fillMaxSize(), @@ -283,8 +269,7 @@ class GeneralClockView @JvmOverloads constructor( Canvas( modifier = Modifier .fillMaxWidth() - .height(canvasHeightDp) - .then(fidgetTapModifierFor { smallDigitBounds.value }), + .height(canvasHeightDp), ) { if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas @@ -308,7 +293,6 @@ class GeneralClockView @JvmOverloads constructor( isRightAligned -> size.width - clockPaddingStart - totalWidth else -> (size.width - totalWidth) / 2f } - smallDigitBounds.value = Rect(startX, 0f, startX + totalWidth, size.height) var x = startX for ((i, char) in time.withIndex()) { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt index ea8b103bd079a..2b145914fc64c 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt @@ -127,7 +127,6 @@ class OldQuickLookClockView @JvmOverloads constructor( fontFamily = clockFontFamily, color = textColor, ), - modifier = Modifier.then(fidgetTapModifier), ) Text( @@ -213,8 +212,7 @@ class OldQuickLookClockView @JvmOverloads constructor( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.then(fidgetTapModifier), - ) { + ) { Text( text = hours, maxLines = 1, diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 13805cc26624c..2d783058f7094 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -138,8 +138,6 @@ constructor( if (clock == null) { return } - (clock.smallClock.view as? AxClockView)?.onFidgetTapListener = null - (clock.largeClock.view as? AxClockView)?.onFidgetTapListener = null smallClockOnAttachStateChangeListener?.let { clock.smallClock.view.removeOnAttachStateChangeListener(it) smallClockFrame?.viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener) @@ -159,9 +157,6 @@ constructor( clock.eventListeners.attach(clockListener) clock.initialize(isDarkTheme(), dozeAmount.value, 0f) - (clock.smallClock.view as? AxClockView)?.onFidgetTapListener = ::handleFidgetTap - (clock.largeClock.view as? AxClockView)?.onFidgetTapListener = ::handleFidgetTap - if (!regionSamplingEnabled) { updateColors() } else { @@ -726,14 +721,6 @@ constructor( } } - fun handleFidgetTap(x: Float, y: Float) { - if (isPreview) return - clock?.run { - smallClock.animations.onFidgetTap(x, y) - largeClock.animations.onFidgetTap(x, y) - } - } - private fun handleDoze(doze: Float) { if (isPreview) { dozeAmount.value = doze diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index babf7de0f6583..0481b095a3ea8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -220,8 +220,6 @@ constructor( if (wallpaperFocalAreaInteractor.hasFocalArea.value) { wallpaperFocalAreaInteractor.sendTapPosition(x, y) - } else { - clockEventController.handleFidgetTap(x, y) } } From e54f85231a6b3ae4eb3547ff45609f76a57a271c Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 6 May 2026 16:09:23 +0800 Subject: [PATCH 04/51] [ax_clocks|bugfix] fixing time update regressions same with charging computations, this one also burns cpu outside keyguard, skip time updates since the appear animation will invalidate and update the clock anyway. 56.72% 0.00% com.android.systemui 1913 __libc_init | -- __libc_init | -- main |--0.99%-- [hit in function] | |--93.98%-- android.os.Handler.dispatchMessage | | | |--33.02%-- com.android.keyguard.KeyguardUpdateMonitor$14.handleMessage | | | | | |--58.50%-- com.android.keyguard.ClockEventController$keyguardUpdateMonitorCallback$1.onTimeChanged | | | com.android.keyguard.ClockEventController$keyguardUpdateMonitorCallback$1.refreshTime | | | com.android.systemui.shared.clocks.AxClockFaceController$events$1.onTimeTick | | | com.android.systemui.shared.clocks.view.AxClockView.refreshTime | | | | | | | |--75.22%-- com.android.systemui.shared.clocks.view.AxClockInteractor.refreshTime | | | | | | | | | |--58.85%-- androidx.compose.runtime.ParcelableSnapshotMutableState.setValue | | | | | androidx.compose.runtime.snapshots.GlobalSnapshot$$ExternalSyntheticLambda0.invoke | | | | | androidx.compose.ui.platform.GlobalSnapshotManager$ensureStarted$2.invoke | | | | | kotlinx.coroutines.channels.BufferedChannel.trySend-JP2dKIU | | | | | kotlinx.coroutines.channels.BufferedChannel.access$updateCellSend | | | | | kotlinx.coroutines.channels.BufferedChannel.tryResumeReceiver | | | | | kotlinx.coroutines.CancellableContinuationImpl.dispatchResume | | | | | androidx.compose.ui.platform.AndroidUiDispatcher.dispatch | | | | | android.view.Choreographer.postCallbackDelayedInternal | | | | | android.view.Choreographer.scheduleFrameLocked | | | | | art_jni_trampoline | | | | | android::nativeScheduleVsync(_JNIEnv*, _jclass*, long) | | | | | android::DisplayEventDispatcher::scheduleVsync() Change-Id: I258c56a36d3d3b79e4340f0787a071e16b5090ee --- .../shared/clocks/view/AxClockInteractor.kt | 18 +++++++++++++++--- .../android/keyguard/ClockEventController.kt | 4 +++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt index bfb6e0e0dae25..61c2d6f847f3a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt @@ -37,6 +37,10 @@ class AxClockInteractor( internal set var locale: Locale = Locale.getDefault() + private var cachedSdf: SimpleDateFormat? = null + private var cachedDateFormat: SimpleDateFormat? = null + private var cachedDateLocale: Locale = locale + var needsSeconds: Boolean = false var useStandardFormat: Boolean = false @@ -70,13 +74,18 @@ class AxClockInteractor( if (format == newFormat && locale == newLocale) return format = newFormat locale = newLocale + cachedSdf = null + cachedDateFormat = null refreshTime() } fun refreshTime(): Boolean { format ?: return false calendar.timeInMillis = System.currentTimeMillis() - val newTime = SimpleDateFormat(format, Locale.ENGLISH).format(calendar.time) + if (cachedSdf == null) { + cachedSdf = SimpleDateFormat(format, Locale.ENGLISH) + } + val newTime = cachedSdf!!.format(calendar.time) refreshDate() val changed = timeStr != newTime if (changed) { @@ -87,8 +96,11 @@ class AxClockInteractor( } fun refreshDate() { - val dateFormat = SimpleDateFormat("EEE, dd MMM", locale) - state.dateStrFlow.value = dateFormat.format(calendar.time) + if (cachedDateFormat == null || cachedDateLocale != locale) { + cachedDateFormat = SimpleDateFormat("EEE, dd MMM", locale) + cachedDateLocale = locale + } + state.dateStrFlow.value = cachedDateFormat!!.format(calendar.time) } fun setupPreview(setPreviewMode: () -> Unit) { diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 2d783058f7094..40921c109314b 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -470,7 +470,9 @@ constructor( } override fun onTimeChanged() { - refreshTime() + if (ScrimUtils.get().isKeyguardShowing()) { + refreshTime() + } } private fun refreshTime() { From ba79a153bb474a624351305bc54a9b6d41f09bd9 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:23:15 +0800 Subject: [PATCH 05/51] [feature] adding lockscreen widgets feature Change-Id: I2229b7d79d10061269c4ffc9d942303beb60c7d1 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> --- packages/SystemUI/AndroidManifest.xml | 5 + .../layout/keyguard_widgets_area.xml | 23 + packages/SystemUI/res/values/axion_dimens.xml | 29 + packages/SystemUI/res/values/ids.xml | 4 + .../repository/KeyguardClockRepository.kt | 16 + .../interactor/KeyguardClockInteractor.kt | 6 +- .../ui/binder/KeyguardRootViewBinder.kt | 5 + .../blueprints/DefaultKeyguardBlueprint.kt | 3 + .../blueprints/SplitShadeKeyguardBlueprint.kt | 3 + .../sections/KeyguardWidgetViewSection.kt | 113 +++ .../layout/sections/SplitShadeMediaSection.kt | 6 +- .../transitions/ClockSizeTransition.kt | 1 + .../lockscreen/KeyguardAppWidgetHost.kt | 77 +++ .../lockscreen/KeyguardAppWidgetHostView.kt | 77 +++ .../KeyguardWidgetConfigActivity.kt | 186 +++++ .../KeyguardWidgetHostController.kt | 654 ++++++++++++++++++ .../KeyguardWidgetInteractionHandler.kt | 50 ++ .../ui/controller/KeyguardMediaController.kt | 3 + .../NotificationPanelViewController.java | 16 +- .../repository/FakeKeyguardClockRepository.kt | 1 + .../appwidget/AppWidgetServiceImpl.java | 3 + 21 files changed, 1273 insertions(+), 8 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_widgets_area.xml create mode 100644 packages/SystemUI/res/values/axion_dimens.xml create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHost.kt create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHostView.kt create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetConfigActivity.kt create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetHostController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetInteractionHandler.kt diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 322eaaf48b158..cfe7025474062 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -923,6 +923,11 @@ + + + diff --git a/packages/SystemUI/res/values/axion_dimens.xml b/packages/SystemUI/res/values/axion_dimens.xml new file mode 100644 index 0000000000000..15ff6e99d6af0 --- /dev/null +++ b/packages/SystemUI/res/values/axion_dimens.xml @@ -0,0 +1,29 @@ + + + + + 9.2989dp + 9.2989dp + 37.1956dp + + + 4dp + 14dp + 16dp + 16dp + diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 37f09c1b79527..e443d6b0587f8 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -315,4 +315,8 @@ + + + + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index d1d7da50a69df..a03b705d9d6e2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -76,6 +76,7 @@ interface KeyguardClockRepository { val forcedClockSize: Flow + val areLockscreenWidgetsEnabled: Boolean fun setClockSize(size: ClockSize) } @@ -162,6 +163,21 @@ constructor( initialValue = null, ) + override val areLockscreenWidgetsEnabled: Boolean + get() { + val isEnabled = Settings.System.getIntForUser( + context.contentResolver, + "lockscreen_widgets_enabled", + 0, + UserHandle.USER_CURRENT + ) == 1 + val widgetConfig = Settings.System.getStringForUser( + context.contentResolver, + "lockscreen_widgets_config", + UserHandle.USER_CURRENT + ) ?: "" + return isEnabled && widgetConfig.isNotEmpty() + } private fun getClockSize(): ClockSizeSetting { val isDoubleLineClock = secureSettings.getIntForUser( Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index 0481b095a3ea8..c90d0ba744ab7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -92,6 +92,8 @@ constructor( var clock: ClockController? by keyguardClockRepository.clockEventController::clock + val areLockscreenWidgetsEnabled: Boolean + get() = keyguardClockRepository.areLockscreenWidgetsEnabled val isAodPromotedNotificationPresent: Flow = if (PromotedNotificationUi.isEnabled) { aodPromotedNotificationInteractor.isPresent @@ -135,7 +137,9 @@ constructor( val clockSize: StateFlow = selectedClockSize .flatMapLatestConflated { selectedSize -> - if (selectedSize == ClockSizeSetting.SMALL) { + if (selectedSize == ClockSizeSetting.SMALL || + keyguardClockRepository.areLockscreenWidgetsEnabled + ) { flowOf(ClockSize.SMALL) } else { dynamicClockSize diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 50c4ec3b8a49d..90806b3e5d643 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -211,6 +211,7 @@ object KeyguardRootViewBinder { childViews[burnInLayerId]?.alpha = alpha childViews[sliceViewId]?.alpha = alpha childViews[weatherAreaId]?.alpha = alpha + childViews[widgetArea]?.alpha = alpha } } @@ -241,6 +242,7 @@ object KeyguardRootViewBinder { childViews[burnInLayerId]?.translationY = y childViews[sliceViewId]?.translationY = y childViews[weatherAreaId]?.translationY = y + childViews[widgetArea]?.translationY = y childViews[largeClockId]?.translationY = y if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { childViews[largeClockDateId]?.translationY = y @@ -259,6 +261,7 @@ object KeyguardRootViewBinder { childViews[burnInLayerId]?.translationX = px childViews[sliceViewId]?.translationX = px childViews[weatherAreaId]?.translationX = px + childViews[widgetArea]?.translationX = px childViews[aodPromotedNotificationId]?.translationX = px childViews[aodNotificationIconContainerId]?.translationX = px } @@ -309,6 +312,7 @@ object KeyguardRootViewBinder { childViews[burnInLayerId]?.visibility = visibility childViews[sliceViewId]?.visibility = visibility childViews[weatherAreaId]?.visibility = visibility + childViews[widgetArea]?.visibility = visibility } } @@ -614,6 +618,7 @@ object KeyguardRootViewBinder { private val endButton = R.id.end_button private val deviceEntryIcon = R.id.device_entry_icon_view private val nsslPlaceholderId = R.id.nssl_placeholder + private val widgetArea = R.id.keyguard_widgets_area private val authInteractionProperties = AuthInteractionProperties() private val authUiIds = setOf(deviceEntryIcon, indicationArea) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt index 4940123ac0908..4c0d3708fd3dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt @@ -37,6 +37,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguar import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import java.util.Optional import javax.inject.Inject @@ -70,6 +71,7 @@ constructor( smartspaceSection: SmartspaceSection, keyguardWeatherViewSection: KeyguardWeatherViewSection, keyguardSliceViewSection: KeyguardSliceViewSection, + keyguardWidgetViewSection: KeyguardWidgetViewSection, axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, ) : KeyguardBlueprint { @@ -93,6 +95,7 @@ constructor( clockSection, keyguardWeatherViewSection, keyguardSliceViewSection, + keyguardWidgetViewSection, defaultDeviceEntrySection, udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index b4b7452848845..ca46ee5ce54ab 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -35,6 +35,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguar import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection @@ -69,6 +70,7 @@ constructor( aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, clockSection: ClockSection, + keyguardWidgetViewSection: KeyguardWidgetViewSection, mediaSection: SplitShadeMediaSection, smartspaceSection: SmartspaceSection, keyguardWeatherViewSection: KeyguardWeatherViewSection, @@ -97,6 +99,7 @@ constructor( smartspaceSection, keyguardWeatherViewSection, keyguardSliceViewSection, + keyguardWidgetViewSection, mediaSection, defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views. ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt new file mode 100644 index 0000000000000..4326e5d1ce213 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.lockscreen.KeyguardWidgetHostController +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.keyguard.ui.clocks.ClockViewIds +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ConfigurationController +import javax.inject.Inject + +class KeyguardWidgetViewSection +@Inject +constructor( + private val context: Context, + private val layoutInflater: LayoutInflater, + private val activityStarter: ActivityStarter, + private val configurationController: ConfigurationController, + private val broadcastDispatcher: BroadcastDispatcher, + private val keyguardClockViewModel: KeyguardClockViewModel, +) : KeyguardSection() { + private lateinit var widgetsView: ViewGroup + private lateinit var controller: KeyguardWidgetHostController + + private val isSplitShade: Boolean + get() = context.resources.getBoolean(R.bool.config_use_split_notification_shade) + + override fun addViews(constraintLayout: ConstraintLayout) { + widgetsView = + layoutInflater.inflate(R.layout.keyguard_widgets_area, null, false) as ViewGroup + constraintLayout.addView(widgetsView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + controller = KeyguardWidgetHostController(context, widgetsView, activityStarter, configurationController, broadcastDispatcher) + controller.init() + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + connect( + R.id.keyguard_widgets_area, + ConstraintSet.TOP, + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, + ConstraintSet.BOTTOM, + ) + connect( + R.id.keyguard_widgets_area, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + ) + connect( + R.id.keyguard_widgets_area, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END, + ) + constrainWidth(R.id.keyguard_widgets_area, ConstraintSet.MATCH_CONSTRAINT) + constrainMaxWidth( + R.id.keyguard_widgets_area, + context.resources.getDimensionPixelSize(R.dimen.shade_panel_width), + ) + val centered = !isSplitShade || keyguardClockViewModel.clockShouldBeCentered.value + setHorizontalBias(R.id.keyguard_widgets_area, if (centered) 0.5f else 0f) + if (::controller.isInitialized) { + controller.clockCentered = centered + } + constrainHeight(R.id.keyguard_widgets_area, ConstraintSet.WRAP_CONTENT) + val density = context.resources.displayMetrics.density + constrainMaxHeight( + R.id.keyguard_widgets_area, + (KeyguardWidgetHostController.MAX_CONTAINER_HEIGHT_DP * density).toInt(), + ) + + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf(R.id.keyguard_widgets_area) + ) + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + controller.dispose() + } + +} + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt index 34e3d9364c12f..381881fb29f06 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt @@ -70,11 +70,7 @@ constructor( constraintSet.apply { constrainWidth(mediaContainerId, MATCH_CONSTRAINT) constrainHeight(mediaContainerId, WRAP_CONTENT) - if (smartspaceViewModel.isSmartspaceEnabled) { - connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM) - } else { - connect(mediaContainerId, TOP, R.id.keyguard_slice_view, BOTTOM) - } + connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM) connect(mediaContainerId, START, PARENT_ID, START) connect(mediaContainerId, END, R.id.split_shade_guideline, END) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 87e48ff27a3c9..9d7347aca43a6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -419,6 +419,7 @@ class ClockSizeTransition( // Notifications normally and media on split shade needs to be moved addTarget(R.id.aod_notification_icon_container) addTarget(R.id.status_view_media_container) + addTarget(R.id.keyguard_widgets_area) } override fun initTargets(from: Target, to: Target) { diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHost.kt new file mode 100644 index 0000000000000..145212b3e7127 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHost.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen + +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.os.DeadObjectException +import android.os.Looper +import android.os.TransactionTooLargeException +import android.util.Log +import android.widget.RemoteViews + +class KeyguardAppWidgetHost( + context: Context, + hostId: Int, + private val interactionHandler: RemoteViews.InteractionHandler, +) : AppWidgetHost(context, hostId, interactionHandler, Looper.getMainLooper()) { + + var onWidgetRemovedListener: ((Int) -> Unit)? = null + + override fun onCreateView( + context: Context, + appWidgetId: Int, + appWidget: AppWidgetProviderInfo?, + ): AppWidgetHostView { + return KeyguardAppWidgetHostView(context, interactionHandler) + } + + override fun onAppWidgetRemoved(appWidgetId: Int) { + Log.i(TAG, "Widget removed from system: id=$appWidgetId") + onWidgetRemovedListener?.invoke(appWidgetId) + } + + override fun startListening() { + try { + super.startListening() + } catch (e: Exception) { + if (!e.isBinderSizeError()) { + Log.e(TAG, "Error starting widget host listening", e) + } + } + } + + override fun stopListening() { + try { + super.stopListening() + } catch (e: Exception) { + Log.e(TAG, "Error stopping widget host listening", e) + } + } + + companion object { + private const val TAG = "KeyguardAppWidgetHost" + const val HOST_ID = 1027 + + private fun Exception.isBinderSizeError(): Boolean { + return cause is TransactionTooLargeException || cause is DeadObjectException + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHostView.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHostView.kt new file mode 100644 index 0000000000000..c10d771128489 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHostView.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen + +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.graphics.Outline +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.RemoteViews +import com.android.systemui.util.ScrimUtils + +class KeyguardAppWidgetHostView( + context: Context, + interactionHandler: RemoteViews.InteractionHandler, +) : AppWidgetHostView(context, interactionHandler) { + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (!ScrimUtils.get().isKeyguardShowing()) return false + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + parent?.requestDisallowInterceptTouchEvent(true) + } + return super.dispatchTouchEvent(event) + } + + var cornerRadius: Float = 0f + set(value) { + field = value + if (value > 0f) { + outlineProvider = roundedOutlineProvider + clipToOutline = true + } else { + outlineProvider = ViewOutlineProvider.BACKGROUND + clipToOutline = false + } + invalidateOutline() + } + + private val roundedOutlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, view.width, view.height, cornerRadius) + } + } + + override fun setAppWidget(appWidgetId: Int, info: AppWidgetProviderInfo?) { + super.setAppWidget(appWidgetId, info) + super.setPadding(0, 0, 0, 0) + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + + super.setPadding(0, 0, 0, 0) + } + + override fun updateAppWidget(remoteViews: RemoteViews?) { + super.updateAppWidget(remoteViews) + super.setPadding(0, 0, 0, 0) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetConfigActivity.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetConfigActivity.kt new file mode 100644 index 0000000000000..0734f3418d157 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetConfigActivity.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen + +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Intent +import android.os.Bundle +import android.os.UserHandle +import android.provider.Settings +import android.util.Log +import org.json.JSONArray +import org.json.JSONObject + +class KeyguardWidgetConfigActivity : Activity() { + + private var widgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val providerStr = intent.getStringExtra(EXTRA_PROVIDER) + if (providerStr.isNullOrEmpty()) { + finish() + return + } + + val cn = ComponentName.unflattenFromString(providerStr) + if (cn == null) { + finish() + return + } + + if (savedInstanceState != null) { + widgetId = + savedInstanceState.getInt(STATE_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + return + } + + val host = AppWidgetHost(this, KeyguardAppWidgetHost.HOST_ID) + widgetId = host.allocateAppWidgetId() + + val options = + Bundle().apply { + putInt( + AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD, + ) + } + val bound = + AppWidgetManager.getInstance(this).bindAppWidgetIdIfAllowed(widgetId, cn, options) + + if (!bound) { + Log.w(TAG, "Failed to bind widget $cn, id=$widgetId") + host.deleteAppWidgetId(widgetId) + widgetId = AppWidgetManager.INVALID_APPWIDGET_ID + finish() + return + } + + val info = AppWidgetManager.getInstance(this).getAppWidgetInfo(widgetId) + if (info != null && requiresConfiguration(info)) { + val configIntent = + Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE).apply { + component = info.configure + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + } + try { + startActivityForResult(configIntent, REQUEST_CONFIGURE) + } catch (e: Exception) { + Log.e(TAG, "Failed to start configure activity for widget $widgetId", e) + cleanup() + finish() + } + } else { + + saveWidgetToSettings() + finish() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(STATE_WIDGET_ID, widgetId) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CONFIGURE) { + if (resultCode == RESULT_OK) { + saveWidgetToSettings() + } else { + cleanup() + } + finish() + } + } + + private fun saveWidgetToSettings() { + try { + val json = + Settings.System.getStringForUser( + contentResolver, + KeyguardWidgetHostController.SETTING_CONFIG, + UserHandle.USER_CURRENT, + ) ?: "" + + val arr = if (json.isNotBlank()) JSONArray(json) else JSONArray() + arr.put( + JSONObject().apply { + put("appWidgetId", widgetId) + put("provider", intent.getStringExtra(EXTRA_PROVIDER) ?: "") + put("cellX", intent.getIntExtra(EXTRA_CELL_X, 0)) + put("cellY", intent.getIntExtra(EXTRA_CELL_Y, 0)) + put("spanX", intent.getIntExtra(EXTRA_SPAN_X, 1)) + put("spanY", intent.getIntExtra(EXTRA_SPAN_Y, 1)) + } + ) + + Settings.System.putStringForUser( + contentResolver, + KeyguardWidgetHostController.SETTING_CONFIG, + arr.toString(), + UserHandle.USER_CURRENT, + ) + + Settings.System.putIntForUser( + contentResolver, + KeyguardWidgetHostController.SETTING_ENABLED, + 1, + UserHandle.USER_CURRENT, + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to save widget to settings", e) + } + } + + private fun cleanup() { + if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + try { + AppWidgetHost(this, KeyguardAppWidgetHost.HOST_ID).deleteAppWidgetId(widgetId) + } catch (e: Exception) { + Log.e(TAG, "Failed to delete widget ID $widgetId", e) + } + widgetId = AppWidgetManager.INVALID_APPWIDGET_ID + } + } + + private fun requiresConfiguration(info: AppWidgetProviderInfo): Boolean { + if (info.configure == null) return false + val features = info.widgetFeatures + val configOptional = + (features and AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0) && + (features and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0) + return !configOptional + } + + companion object { + private const val TAG = "KgWidgetConfigActivity" + const val EXTRA_PROVIDER = "extra_provider" + const val EXTRA_CELL_X = "extra_cell_x" + const val EXTRA_CELL_Y = "extra_cell_y" + const val EXTRA_SPAN_X = "extra_span_x" + const val EXTRA_SPAN_Y = "extra_span_y" + private const val STATE_WIDGET_ID = "state_widget_id" + private const val REQUEST_CONFIGURE = 1 + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetHostController.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetHostController.kt new file mode 100644 index 0000000000000..f14328ded2d60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetHostController.kt @@ -0,0 +1,654 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen + +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.database.ContentObserver +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import com.android.axion.compose.host.AxComposeView +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.ScrimUtils +import kotlin.math.min +import org.json.JSONArray +import org.json.JSONObject + +data class WidgetEntry( + val appWidgetId: Int, + val provider: ComponentName?, + val cellX: Int, + val cellY: Int, + val spanX: Int, + val spanY: Int, + var hostView: AppWidgetHostView? = null, +) + +class KeyguardWidgetHostController( + private val context: Context, + private val container: ViewGroup, + private val activityStarter: ActivityStarter, + private val configurationController: ConfigurationController, + private val broadcastDispatcher: BroadcastDispatcher, +) { + private val appWidgetManager = AppWidgetManager.getInstance(context) + private val interactionHandler = KeyguardWidgetInteractionHandler(activityStarter) + private val widgetHost = + KeyguardAppWidgetHost(context, KeyguardAppWidgetHost.HOST_ID, interactionHandler) + + private val contentResolver: ContentResolver = context.contentResolver + private val userManager = context.getSystemService(UserManager::class.java)!! + private val handler = Handler(Looper.getMainLooper()) + + private val _widgets = mutableStateListOf() + private val _enabled = mutableStateOf(false) + private val _viewGeneration = mutableStateOf(0) + private val _clockCentered = mutableStateOf(false) + + private var listening = false + private var hostListening = false + private var initialized = false + private var pendingSelfUpdates = 0 + private var configDirty = false + + var clockCentered: Boolean + get() = _clockCentered.value + set(value) { _clockCentered.value = value } + + private val cornerRadius: Float + get() = context.resources.getDimension(R.dimen.kg_widget_corner_radius) + + private val sidePadding: Int + get() = context.resources.getDimensionPixelSize(R.dimen.kg_widget_side_padding) + + private val cellGap: Int + get() = context.resources.getDimensionPixelSize(R.dimen.kg_widget_cell_gap) + + private val isSplitShade: Boolean + get() { + val resId = + context.resources.getIdentifier( + "config_use_split_notification_shade", + "bool", + "com.android.systemui", + ) + return if (resId != 0) context.resources.getBoolean(resId) else false + } + + val cellSize: Int + get() { + val dm = context.resources.displayMetrics + val maxWidth = context.resources.getDimensionPixelSize(R.dimen.shade_panel_width) + val baseWidth = + if (isSplitShade) { + min(dm.widthPixels / 2, maxWidth) + } else { + min(min(dm.widthPixels, dm.heightPixels), maxWidth) + } + val widthCell = (baseWidth - 2 * sidePadding - (GRID_COLUMNS - 1) * cellGap) / GRID_COLUMNS + val density = dm.density + val maxContainerHeight = (MAX_CONTAINER_HEIGHT_DP * density).toInt() + val composePadding = (COMPOSE_TOP_PADDING_DP * density).toInt() + val availableHeight = maxContainerHeight - composePadding + val heightCell = (availableHeight - (MAX_ROWS - 1) * cellGap) / MAX_ROWS + return min(widthCell, heightCell) + } + + val cellHeight: Int + get() = cellSize + + private val hostView by lazy { WidgetHostFrameLayout(context) } + + private inner class WidgetHostFrameLayout(context: Context) : + FrameLayout(context), LaunchableView { + private val composeView = + AxComposeView(context).apply { + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ) + setContent { WidgetGridContent() } + } + + init { + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ) + addView(composeView) + } + + private val delegate = + LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) }) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!ScrimUtils.get().isKeyguardShowing()) return false + composeView.onTouchEvent(event) + return super.onTouchEvent(event) + } + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + if (!ScrimUtils.get().isKeyguardShowing()) return false + composeView.onInterceptTouchEvent(event) + return super.onInterceptTouchEvent(event) + } + } + + private val settingsObserver = + object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean) { + if (pendingSelfUpdates > 0) { + pendingSelfUpdates-- + return + } + loadConfig() + } + } + + private val scrimListener = + object : ScrimUtils.ScrimEventListener { + override fun onKeyguardShowingChanged(showing: Boolean) { + if (showing) { + startHostListening() + } else { + stopHostListening() + hostView.visibility = View.GONE + container.visibility = View.GONE + } + } + } + + private val userUnlockReceiver = + object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + if (intent.action == Intent.ACTION_USER_UNLOCKED && !initialized) { + initWidgets() + } + } + } + + private val configListener = + object : ConfigurationController.ConfigurationListener { + override fun onUiModeChanged() { + scheduleResetWidgets() + } + + override fun onThemeChanged() { + scheduleResetWidgets() + } + + override fun onConfigChanged(newConfig: Configuration) { + updateWidgetSizes() + } + } + + private var resetPending = false + + private fun scheduleResetWidgets() { + if (resetPending) return + resetPending = true + handler.postDelayed( + { + resetPending = false + if (hostListening) { + performFullReset() + } else { + configDirty = true + } + }, + RESET_DELAY_MS, + ) + } + + private fun performFullReset() { + configDirty = false + + cleanupWidgetViews() + _widgets.clear() + loadConfig() + } + + fun init() { + container.addView(hostView) + ScrimUtils.get().addListener(scrimListener) + startObserving() + configurationController.addCallback(configListener) + widgetHost.onWidgetRemovedListener = { appWidgetId -> onWidgetRemoved(appWidgetId) } + + if (userManager.isUserUnlocked) { + initWidgets() + } else { + broadcastDispatcher.registerReceiver( + userUnlockReceiver, + IntentFilter(Intent.ACTION_USER_UNLOCKED), + ) + } + } + + private fun initWidgets() { + if (initialized) return + initialized = true + broadcastDispatcher.unregisterReceiver(userUnlockReceiver) + startHostListening() + loadConfig() + } + + fun dispose() { + widgetHost.onWidgetRemovedListener = null + ScrimUtils.get().removeListener(scrimListener) + configurationController.removeCallback(configListener) + handler.removeCallbacksAndMessages(null) + broadcastDispatcher.unregisterReceiver(userUnlockReceiver) + stopObserving() + stopHostListening() + cleanupWidgetViews() + container.removeAllViews() + initialized = false + } + + private fun startObserving() { + if (listening) return + contentResolver.registerContentObserver( + ENABLED_URI, + false, + settingsObserver, + UserHandle.USER_CURRENT, + ) + contentResolver.registerContentObserver( + CONFIG_URI, + false, + settingsObserver, + UserHandle.USER_CURRENT, + ) + listening = true + } + + private fun stopObserving() { + if (!listening) return + contentResolver.unregisterContentObserver(settingsObserver) + listening = false + } + + private fun startHostListening() { + if (hostListening) return + widgetHost.startListening() + hostListening = true + + if (configDirty) { + performFullReset() + return + } + + var needsSave = false + _widgets.forEachIndexed { idx, entry -> + if (entry.hostView == null && entry.provider != null) { + val resolved = resolveWidget(entry) + if (resolved != null) { + if (resolved.appWidgetId != entry.appWidgetId) { + needsSave = true + } + _widgets[idx] = resolved + } + } + } + if (needsSave) saveConfig() + updateVisibility() + } + + private fun stopHostListening() { + if (!hostListening) return + widgetHost.stopListening() + hostListening = false + } + + private fun loadConfig() { + if (!userManager.isUserUnlocked) return + + val isEnabled = + Settings.System.getIntForUser( + contentResolver, + SETTING_ENABLED, + 0, + UserHandle.USER_CURRENT, + ) == 1 + + val configJson = + Settings.System.getStringForUser( + contentResolver, + SETTING_CONFIG, + UserHandle.USER_CURRENT, + ) ?: "" + + _enabled.value = isEnabled && configJson.isNotEmpty() + + val entries = parseConfig(configJson) + updateWidgetEntries(entries) + updateVisibility() + } + + private fun parseConfig(json: String): List { + if (json.isBlank()) return emptyList() + return try { + val arr = JSONArray(json) + (0 until arr.length()).mapNotNull { i -> + val obj = arr.getJSONObject(i) + val providerStr = obj.optString("provider", "") + val provider = + if (providerStr.isNotEmpty()) { + ComponentName.unflattenFromString(providerStr) + } else null + + WidgetEntry( + appWidgetId = obj.optInt("appWidgetId", -1), + provider = provider, + cellX = obj.optInt("cellX", 0), + cellY = obj.optInt("cellY", 0), + spanX = obj.optInt("spanX", 1).coerceIn(1, GRID_COLUMNS), + spanY = obj.optInt("spanY", 1).coerceIn(1, MAX_ROWS), + ) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse widget config", e) + emptyList() + } + } + + private fun updateWidgetEntries(entries: List) { + + _widgets.forEach { existing -> + if (existing.appWidgetId >= 0 && existing.provider != null) { + val stillPresent = + entries.any { + it.appWidgetId == existing.appWidgetId || + (it.provider?.flattenToString() == + existing.provider.flattenToString() && + it.cellX == existing.cellX && + it.cellY == existing.cellY) + } + if (!stillPresent) { + widgetHost.deleteAppWidgetId(existing.appWidgetId) + } + } + } + + cleanupWidgetViews() + _widgets.clear() + + if (!hostListening) { + _widgets.addAll(entries) + return + } + + var needsSave = false + entries.forEach { entry -> + val resolved = resolveWidget(entry) + if (resolved != null) { + if (resolved.appWidgetId != entry.appWidgetId) { + needsSave = true + } + _widgets.add(resolved) + } + } + if (needsSave) saveConfig() + + _viewGeneration.value++ + } + + private fun resolveWidget(entry: WidgetEntry): WidgetEntry? { + var widgetId = entry.appWidgetId + val provider = entry.provider ?: return null + + if (widgetId < 0) { + widgetId = allocateAndBind(provider) ?: return null + } else { + val info = appWidgetManager.getAppWidgetInfo(widgetId) + if (info == null) { + widgetHost.deleteAppWidgetId(widgetId) + widgetId = allocateAndBind(provider) ?: return null + } + } + + val hostView = + try { + widgetHost.createView( + context, + widgetId, + appWidgetManager.getAppWidgetInfo(widgetId), + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to create widget view for id=$widgetId", e) + return null + } + + (hostView as? KeyguardAppWidgetHostView)?.cornerRadius = cornerRadius + + val cs = cellSize + val ch = cellHeight + val gap = cellGap + val w = entry.spanX * cs + (entry.spanX - 1).coerceAtLeast(0) * gap + val h = entry.spanY * ch + (entry.spanY - 1).coerceAtLeast(0) * gap + hostView.updateAppWidgetSize(Bundle(), pxToDp(w), pxToDp(h), pxToDp(w), pxToDp(h)) + + return entry.copy(appWidgetId = widgetId, hostView = hostView) + } + + private fun allocateAndBind(provider: ComponentName): Int? { + val widgetId = widgetHost.allocateAppWidgetId() + val options = + Bundle().apply { + putInt( + AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD, + ) + } + val bound = appWidgetManager.bindAppWidgetIdIfAllowed(widgetId, provider, options) + if (!bound) { + Log.w(TAG, "Failed to bind widget $provider, id=$widgetId") + widgetHost.deleteAppWidgetId(widgetId) + return null + } + return widgetId + } + + private fun cleanupWidgetViews() { + _widgets.forEach { + it.hostView?.let { view -> (view.parent as? ViewGroup)?.removeView(view) } + } + } + + private fun saveConfig() { + val arr = JSONArray() + _widgets.forEach { entry -> + arr.put( + JSONObject().apply { + put("appWidgetId", entry.appWidgetId) + put("provider", entry.provider?.flattenToString() ?: "") + put("cellX", entry.cellX) + put("cellY", entry.cellY) + put("spanX", entry.spanX) + put("spanY", entry.spanY) + } + ) + } + pendingSelfUpdates++ + Settings.System.putStringForUser( + contentResolver, + SETTING_CONFIG, + arr.toString(), + UserHandle.USER_CURRENT, + ) + } + + private fun updateWidgetSizes() { + val cs = cellSize + val ch = cellHeight + val gap = cellGap + _widgets.forEach { entry -> + val view = entry.hostView ?: return@forEach + val w = entry.spanX * cs + (entry.spanX - 1).coerceAtLeast(0) * gap + val h = entry.spanY * ch + (entry.spanY - 1).coerceAtLeast(0) * gap + view.updateAppWidgetSize(Bundle(), pxToDp(w), pxToDp(h), pxToDp(w), pxToDp(h)) + } + _viewGeneration.value++ + } + + private fun updateVisibility() { + val hasViews = _enabled.value && _widgets.any { it.hostView != null } + val vis = if (hasViews) View.VISIBLE else View.GONE + hostView.visibility = vis + container.visibility = vis + } + + private fun onWidgetRemoved(appWidgetId: Int) { + val idx = _widgets.indexOfFirst { it.appWidgetId == appWidgetId } + if (idx < 0) return + val entry = _widgets[idx] + entry.hostView?.let { view -> (view.parent as? ViewGroup)?.removeView(view) } + _widgets.removeAt(idx) + saveConfig() + updateVisibility() + _viewGeneration.value++ + } + + private fun pxToDp(px: Int): Int { + return (px / context.resources.displayMetrics.density).toInt() + } + + @Composable + private fun WidgetGridContent() { + val widgets = _widgets.toList() + val density = LocalDensity.current + val gen = _viewGeneration.value + + if (widgets.isEmpty()) return + + val cs = cellSize + val ch = cellHeight + val gap = cellGap + val maxRow = widgets.maxOf { it.cellY + it.spanY } + val gridHeight = maxRow * ch + (maxRow - 1).coerceAtLeast(0) * gap + val gridWidth = GRID_COLUMNS * cs + (GRID_COLUMNS - 1) * gap + + Box( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .padding( + start = with(density) { sidePadding.toDp() }, + end = with(density) { sidePadding.toDp() }, + top = 8.dp, + ), + contentAlignment = if (isSplitShade && !_clockCentered.value) Alignment.TopStart else Alignment.TopCenter, + ) { + Box( + modifier = + Modifier.size( + width = with(density) { gridWidth.toDp() }, + height = with(density) { gridHeight.toDp() }, + ) + ) { + widgets.forEach { entry -> + val view = entry.hostView ?: return@forEach + val x = entry.cellX * (cs + gap) + val y = entry.cellY * (ch + gap) + val w = entry.spanX * cs + (entry.spanX - 1).coerceAtLeast(0) * gap + val h = entry.spanY * ch + (entry.spanY - 1).coerceAtLeast(0) * gap + + key(entry.appWidgetId, gen) { + AndroidView( + factory = { _ -> + (view.parent as? ViewGroup)?.removeView(view) + view + }, + modifier = + Modifier.offset { IntOffset(x, y) } + .size( + width = with(density) { w.toDp() }, + height = with(density) { h.toDp() }, + ), + ) + } + } + } + } + } + + companion object { + private const val TAG = "KeyguardWidgetHostCtrl" + private const val RESET_DELAY_MS = 300L + const val MAX_CONTAINER_HEIGHT_DP = 220 + private const val COMPOSE_TOP_PADDING_DP = 8 + const val GRID_COLUMNS = 4 + const val MAX_ROWS = 2 + + const val SETTING_ENABLED = "lockscreen_widgets_enabled" + const val SETTING_CONFIG = "lockscreen_widgets_config" + + private val ENABLED_URI: Uri = Settings.System.getUriFor(SETTING_ENABLED) + private val CONFIG_URI: Uri = Settings.System.getUriFor(SETTING_CONFIG) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetInteractionHandler.kt new file mode 100644 index 0000000000000..55836f94febca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetInteractionHandler.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen + +import android.app.PendingIntent +import android.view.View +import android.widget.RemoteViews +import com.android.systemui.plugins.ActivityStarter + +class KeyguardWidgetInteractionHandler(private val activityStarter: ActivityStarter) : + RemoteViews.InteractionHandler { + + override fun onInteraction( + view: View, + pendingIntent: PendingIntent, + response: RemoteViews.RemoteResponse, + ): Boolean { + val launchOptions = response.getLaunchOptions(view) + if (pendingIntent.isActivity) { + activityStarter.startPendingIntentMaybeDismissingKeyguard( + pendingIntent, + false, + null, + null, + launchOptions.first, + launchOptions.second?.toBundle(), + null, + ) + } else { + + RemoteViews.startPendingIntent(view, pendingIntent, launchOptions) + } + return true + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index 857a061a17677..18e1f964e9e01 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -27,6 +27,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Dumpable +import com.android.systemui.keyguard.data.repository.KeyguardClockRepository import com.android.systemui.classifier.Classifier import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -84,6 +85,7 @@ constructor( private val mediaViewModelFactory: MediaViewModel.Factory, private val mediaCarouselInteractor: MediaCarouselInteractor, private val falsingSystem: MediaFalsingSystem, + private val keyguardClockRepository: KeyguardClockRepository, ) : Dumpable { private var lastUsedStatusBarState = -1 @@ -177,6 +179,7 @@ constructor( private fun updateResources() { useSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources) + && !keyguardClockRepository.areLockscreenWidgetsEnabled } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 2eb937d5c2617..1763c406efc64 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -71,6 +71,7 @@ import android.os.Handler; import android.os.PowerManager; import android.os.Trace; +import android.os.UserHandle; import android.provider.Settings; import android.util.IndentingPrintWriter; import android.util.Log; @@ -1252,13 +1253,25 @@ private ClockSize computeDesiredClockSize() { } private ClockSize computeDesiredClockSizeForSingleShade() { - if (hasVisibleNotifications()) { + if (hasVisibleNotifications() || areLockscreenWidgetsEnabled()) { return ClockSize.SMALL; } return ClockSize.LARGE; } + private boolean areLockscreenWidgetsEnabled() { + int enabled = Settings.System.getIntForUser( + mContentResolver, "lockscreen_widgets_enabled", 0, + UserHandle.USER_CURRENT); + String config = Settings.System.getStringForUser( + mContentResolver, "lockscreen_widgets_config", + UserHandle.USER_CURRENT); + return enabled == 1 && config != null && !config.isEmpty(); + } private ClockSize computeDesiredClockSizeForSplitShade() { + if (areLockscreenWidgetsEnabled()) { + return ClockSize.SMALL; + } // Media is not visible to the user on AOD. boolean isMediaVisibleToUser = mMediaDataManager.hasActiveMedia() && !isOnAod(); @@ -3338,7 +3351,6 @@ private void updateVisibility() { mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); } - @Override public void updateExpansionAndVisibility() { if (!SceneContainerFlag.isEnabled()) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt index 9144de92514cc..df4c540858775 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt @@ -52,6 +52,7 @@ class FakeKeyguardClockRepository() : KeyguardClockRepository { override val clockEventController: ClockEventController = mock() + override val areLockscreenWidgetsEnabled: Boolean = false override fun setClockSize(size: ClockSize) { _clockSize.value = size _forcedClockSize.value = size diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index b8fda0504b9a5..6063a0830cae2 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -5965,6 +5965,9 @@ public boolean canAccessAppWidget(Widget widget, int uid, String packageName) { && isDifferentPackageFromProvider(widget.provider, packageName) && widget.host != null && isDifferentPackageFromHost(widget.host, packageName)) { + if (widget.host.id != null && widget.host.id.uid == uid) { + return true; + } // An AppWidget can only be accessed by either // 1. The package that provides the AppWidget. // 2. The package that hosts the AppWidget. From 6aac4f691e7a3a4549eb88412d6e565f305736c6 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:55:56 +0800 Subject: [PATCH 06/51] [ax_themepicker] adding system color hooks Change-Id: Idb82eb87493dc8820ce0753e5d9b866c85f9a965 --- .../theme/ThemeOverlayController.java | 84 +++++++++++++++---- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 3f996c35e41f3..07cb8fe4b0f75 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -68,6 +68,8 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; +import com.android.internal.graphics.cam.Cam; import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -168,6 +170,8 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { protected int mMainWallpaperColor = Color.TRANSPARENT; // UI contrast as reported by UiModeManager private double mContrast = 0.0; + private double mChromaBoost = 0.0; + private boolean mIsFidelityEnabled = true; // Theme variant: Vibrant, Tonal, Expressive, etc @VisibleForTesting @ThemeStyle.Type @@ -682,6 +686,7 @@ private void reevaluateSystemTheme(boolean forceReload) { mMainWallpaperColor = mainColor; if (mIsMonetEnabled) { + fetchCustomThemeSettings(); mThemeStyle = fetchThemeStyleFromSetting(); mMonetParams = fetchMonetParamsFromSetting(); createOverlays(mMainWallpaperColor); @@ -696,6 +701,27 @@ private void reevaluateSystemTheme(boolean forceReload) { updateThemeOverlays(); } + private void fetchCustomThemeSettings() { + final String overlayPackageJson = mSecureSettings.getStringForUser( + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, + mUserTracker.getUserId()); + if (!TextUtils.isEmpty(overlayPackageJson)) { + try { + JSONObject object = new JSONObject(overlayPackageJson); + mContrast = object.optDouble("_contrast_level", 0.0); + mChromaBoost = object.optDouble("_chroma_boost", 0.0); + mIsFidelityEnabled = object.optBoolean("_fidelity_enabled", true); + if (DEBUG) { + Log.d(TAG, "Custom theme settings: contrast=" + mContrast + + " chromaBoost=" + mChromaBoost + + " fidelity=" + mIsFidelityEnabled); + } + } catch (JSONException e) { + Log.w(TAG, "Failed to parse custom theme settings.", e); + } + } + } + /** * Return the main theme color from a given {@link WallpaperColors} instance. */ @@ -725,19 +751,23 @@ protected boolean isPrivateProfile(UserHandle userHandle) { } private void createOverlays(int color) { + int style = mThemeStyle; + if (mIsFidelityEnabled) { + style = ThemeStyle.CONTENT; + } // Keep SystemUI resilient: if custom Monet params trigger an unexpected runtime issue, // fall back to default params instead of crashing the process. try { - mDarkColorScheme = new ColorScheme(color, true /* isDark */, mThemeStyle, mContrast, + mDarkColorScheme = new ColorScheme(color, true /* isDark */, style, mContrast, mMonetParams); - mLightColorScheme = new ColorScheme(color, false /* isDark */, mThemeStyle, mContrast, + mLightColorScheme = new ColorScheme(color, false /* isDark */, style, mContrast, mMonetParams); } catch (RuntimeException e) { Log.w(TAG, "Failed to build ColorScheme with custom MonetParams, falling back", e); mMonetParams = MonetParams.DEFAULT; - mDarkColorScheme = new ColorScheme(color, true /* isDark */, mThemeStyle, mContrast, + mDarkColorScheme = new ColorScheme(color, true /* isDark */, style, mContrast, mMonetParams); - mLightColorScheme = new ColorScheme(color, false /* isDark */, mThemeStyle, mContrast, + mLightColorScheme = new ColorScheme(color, false /* isDark */, style, mContrast, mMonetParams); } mColorScheme = isNightMode() ? mDarkColorScheme : mLightColorScheme; @@ -759,20 +789,46 @@ private void createOverlays(int color) { private void assignColorsToOverlay(FabricatedOverlay overlay, List> colors, Boolean isFixed) { - colors.forEach(p -> { + for (Pair p : colors) { + String prefix = "android:color/system_" + p.first; if (isFixed) { - overlay.setResourceValue(prefix, TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mLightColorScheme.getMaterialScheme()), null); - return; + int original = p.second.getArgb(mLightColorScheme.getMaterialScheme()); + int boosted = boostChroma(original); + overlay.setResourceValue(prefix, TYPE_INT_COLOR_ARGB8, boosted, null); + continue; } - overlay.setResourceValue(prefix + "_light", TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mLightColorScheme.getMaterialScheme()), null); - overlay.setResourceValue(prefix + "_dark", TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mDarkColorScheme.getMaterialScheme()), null); - }); + int lightOriginal = p.second.getArgb(mLightColorScheme.getMaterialScheme()); + int darkOriginal = p.second.getArgb(mDarkColorScheme.getMaterialScheme()); + + int boostedLight = boostChroma(lightOriginal); + int boostedDark = boostChroma(darkOriginal); + + overlay.setResourceValue(prefix + "_light", + TYPE_INT_COLOR_ARGB8, boostedLight, null); + + overlay.setResourceValue(prefix + "_dark", + TYPE_INT_COLOR_ARGB8, boostedDark, null); + } + } + + private int boostChroma(int argb) { + Cam cam = Cam.fromInt(argb); + + final float chromaBoost = (float) mChromaBoost; + + float boostedChroma = cam.getChroma() * (1f + chromaBoost/ 100f); + boostedChroma = Math.min(boostedChroma, 150f); + + int boosted = ColorUtils.CAMToColor( + cam.getHue(), + boostedChroma, + cam.getJ() + ); + + return ColorUtils.setAlphaComponent(boosted, Color.alpha(argb)); } /** @@ -980,7 +1036,7 @@ private int fetchThemeStyleFromSetting() { try { JSONObject object = new JSONObject(overlayPackageJson); style = ThemeStyle.valueOf( - object.getString(OVERLAY_CATEGORY_THEME_STYLE)); + object.optString(OVERLAY_CATEGORY_THEME_STYLE, ThemeStyle.name(ThemeStyle.TONAL_SPOT))); if (!validStyles.contains(style)) { style = ThemeStyle.TONAL_SPOT; } From 6221ac9d36a43d4472c0c89a0efe6306ef58775d Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:02:58 +0100 Subject: [PATCH 07/51] core: Catch IndexOutOfBoundsException in TypedArray This catches a known race condition during rapid theme and RRO overlay switches where the AssetManager string pool is mid-rebuild while SystemUI tries to reinflate views. Instead of crashing SystemUI with an InflateException, this gracefully returns null to allow fallback to default fonts until the next layout pass. --- core/java/android/content/res/TypedArray.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index e1b07858d2d84..4b3f1a633de3f 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -1388,7 +1388,12 @@ private boolean getValueAt(int index, TypedValue outValue) { outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava( data[index + STYLE_CHANGING_CONFIGURATIONS]); outValue.density = data[index + STYLE_DENSITY]; - outValue.string = (type == TypedValue.TYPE_STRING) ? loadStringValueAt(index) : null; + try { + outValue.string = (type == TypedValue.TYPE_STRING) ? loadStringValueAt(index) : null; + } catch (IndexOutOfBoundsException e) { + android.util.Log.e("TypedArray", "Caught IndexOutOfBoundsException loading string value (likely due to rapid theme/overlay changes). Returning null.", e); + outValue.string = null; + } outValue.sourceResourceId = data[index + STYLE_SOURCE_RESOURCE_ID]; return true; } From d9697de5bd9b407aaf2821744b68bd201d8f34ca Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:05:40 +0100 Subject: [PATCH 08/51] core: settings: Add new custom secure settings keys --- core/java/android/provider/Settings.java | 26 +++++++++++++++++++ .../settings/backup/SecureSettings.java | 1 + .../validators/SecureSettingsValidators.java | 4 +++ 3 files changed, 31 insertions(+) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fe142d13cc1cb..313fadb72bf40 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -15569,6 +15569,32 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String LOCKSCREEN_SMARTSPACE_ENABLED = "lockscreen_smartspace_enabled"; + /** + * Selects the lockscreen smartspace data source. + * + *
    + *
  • {@code 0} ({@link #LOCKSCREEN_SMARTSPACE_SOURCE_AXION}): AxQuickLook only — + * smartspace data is rendered inside the active Axion clock face.
  • + *
  • {@code 1} ({@link #LOCKSCREEN_SMARTSPACE_SOURCE_GOOGLE}): Google BC only — + * smartspace data is rendered in the BC carousel below the clock.
  • + *
  • {@code 2} ({@link #LOCKSCREEN_SMARTSPACE_SOURCE_NONE}): no smartspace, falls + * back to the OmniJaws weather chip when {@code LOCKSCREEN_WEATHER_ENABLED} is + * set.
  • + *
+ * + * @hide + */ + public static final String LOCKSCREEN_SMARTSPACE_SOURCE = "lockscreen_smartspace_source"; + + /** @hide */ + public static final int LOCKSCREEN_SMARTSPACE_SOURCE_AXION = 0; + + /** @hide */ + public static final int LOCKSCREEN_SMARTSPACE_SOURCE_GOOGLE = 1; + + /** @hide */ + public static final int LOCKSCREEN_SMARTSPACE_SOURCE_NONE = 2; + /** * Whether the feature that the device will fire a haptic when users scroll and hit * the edge of the screen is enabled. diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 07ce4fe84ecd3..42634ced71278 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -281,6 +281,7 @@ public class SecureSettings { Settings.Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, Settings.Secure.HINGE_ANGLE_LIDEVENT_ENABLED, Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED, + Settings.Secure.LOCKSCREEN_SMARTSPACE_SOURCE, Settings.Secure.HEARING_AID_RINGTONE_ROUTING, Settings.Secure.HEARING_AID_CALL_ROUTING, Settings.Secure.HEARING_AID_MEDIA_ROUTING, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 678d3b94fde88..4bb0c96e4f69c 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -448,6 +448,10 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_APP, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.LOCKSCREEN_SMARTSPACE_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.LOCKSCREEN_SMARTSPACE_SOURCE, + new InclusiveIntegerRangeValidator( + Secure.LOCKSCREEN_SMARTSPACE_SOURCE_AXION, + Secure.LOCKSCREEN_SMARTSPACE_SOURCE_NONE)); VALIDATORS.put(Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.HINGE_ANGLE_LIDEVENT_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.HEARING_AID_RINGTONE_ROUTING, TRI_STATE_VALIDATOR); From 3283abd4d4016ed161a9a6bf5b45c8c7b8487aab Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:05:40 +0100 Subject: [PATCH 09/51] SystemUI: Implement QS Weather Tile tap dialog --- .../res/drawable/ic_humidity_symbol_small.xml | 8 + .../ic_wind_direction_symbol_small.xml | 8 + .../res/drawable/ic_wind_symbol_small.xml | 8 + .../res/layout/weather_forecast_day_item.xml | 50 ++++ .../res/layout/weather_tile_dialog.xml | 192 ++++++++++++++ packages/SystemUI/res/values/cr_strings.xml | 4 + .../tiles/dialog/WeatherContentManager.kt | 180 +++++++++++++ .../tiles/dialog/WeatherDialogController.kt | 16 ++ .../tiles/dialog/WeatherDialogDelegate.kt | 94 +++++++ .../tiles/dialog/WeatherDialogManager.kt | 79 ++++++ .../systemui/qs/tiles/WeatherTile.java | 238 +++++++++--------- 11 files changed, 754 insertions(+), 123 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_humidity_symbol_small.xml create mode 100644 packages/SystemUI/res/drawable/ic_wind_direction_symbol_small.xml create mode 100644 packages/SystemUI/res/drawable/ic_wind_symbol_small.xml create mode 100644 packages/SystemUI/res/layout/weather_forecast_day_item.xml create mode 100644 packages/SystemUI/res/layout/weather_tile_dialog.xml create mode 100644 packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherContentManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogDelegate.kt create mode 100644 packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogManager.kt diff --git a/packages/SystemUI/res/drawable/ic_humidity_symbol_small.xml b/packages/SystemUI/res/drawable/ic_humidity_symbol_small.xml new file mode 100644 index 0000000000000..7551c2c56c7bb --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_humidity_symbol_small.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_wind_direction_symbol_small.xml b/packages/SystemUI/res/drawable/ic_wind_direction_symbol_small.xml new file mode 100644 index 0000000000000..807485f162d28 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_wind_direction_symbol_small.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_wind_symbol_small.xml b/packages/SystemUI/res/drawable/ic_wind_symbol_small.xml new file mode 100644 index 0000000000000..120ee713250b2 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_wind_symbol_small.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/weather_forecast_day_item.xml b/packages/SystemUI/res/layout/weather_forecast_day_item.xml new file mode 100644 index 0000000000000..62d0bdfa6644d --- /dev/null +++ b/packages/SystemUI/res/layout/weather_forecast_day_item.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/weather_tile_dialog.xml b/packages/SystemUI/res/layout/weather_tile_dialog.xml new file mode 100644 index 0000000000000..7945e7bd9dbed --- /dev/null +++ b/packages/SystemUI/res/layout/weather_tile_dialog.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/cr_strings.xml b/packages/SystemUI/res/values/cr_strings.xml index 8c663785d0b4f..7c30f088ff5c9 100644 --- a/packages/SystemUI/res/values/cr_strings.xml +++ b/packages/SystemUI/res/values/cr_strings.xml @@ -135,6 +135,10 @@ No weather data Weather + + Current weather condition + Forecast weather condition + Refresh rate Auto diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherContentManager.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherContentManager.kt new file mode 100644 index 0000000000000..8659ca10531ee --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherContentManager.kt @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.android.internal.util.alpha.OmniJawsClient +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val TAG = "WeatherContentManager" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +@SysUISingleton +class WeatherContentManager +@Inject +constructor( + @Background private val bgLooper: Looper, +) { + private lateinit var contentView: View + private lateinit var context: Context + private lateinit var coroutineScope: CoroutineScope + + private lateinit var currentIcon: ImageView + private lateinit var currentTemp: TextView + private lateinit var cityName: TextView + private lateinit var windInfo: TextView + private lateinit var windDirection: TextView + private lateinit var humidityInfo: TextView + private lateinit var forecastContainer: LinearLayout + private lateinit var noDataView: TextView + + private val mainHandler = Handler(Looper.getMainLooper()) + private val bgHandler = Handler(bgLooper) + + private val omniJawsObserver = object : OmniJawsClient.OmniJawsObserver { + override fun weatherUpdated() { + refreshWeatherAsync() + } + + override fun weatherError(errorReason: Int) { + if (DEBUG) Log.d(TAG, "weatherError: $errorReason") + mainHandler.post { showNoData() } + } + } + + fun bind(view: View, scope: CoroutineScope) { + if (DEBUG) Log.d(TAG, "bind") + contentView = view + context = view.context + coroutineScope = scope + + currentIcon = view.findViewById(R.id.weather_current_icon) + currentTemp = view.findViewById(R.id.weather_current_temp) + cityName = view.findViewById(R.id.weather_city_name) + windInfo = view.findViewById(R.id.weather_wind_info) + windDirection = view.findViewById(R.id.weather_wind_direction) + humidityInfo = view.findViewById(R.id.weather_humidity_info) + forecastContainer = view.findViewById(R.id.weather_forecast_container) + noDataView = view.findViewById(R.id.weather_no_data) + } + + fun start() { + if (DEBUG) Log.d(TAG, "start") + OmniJawsClient.get().addObserver(context, omniJawsObserver) + refreshWeatherAsync() + } + + fun stop() { + if (DEBUG) Log.d(TAG, "stop") + OmniJawsClient.get().removeObserver(context, omniJawsObserver) + } + + private fun refreshWeatherAsync() { + bgHandler.post { + OmniJawsClient.get().queryWeather(context) + val info = OmniJawsClient.get().weatherInfo + mainHandler.post { + if (info != null) { + populateWeather(info) + } else { + showNoData() + } + } + } + } + + private fun populateWeather(info: OmniJawsClient.WeatherInfo) { + noDataView.visibility = View.GONE + currentIcon.visibility = View.VISIBLE + currentTemp.visibility = View.VISIBLE + cityName.visibility = View.VISIBLE + windInfo.visibility = View.VISIBLE + windDirection.visibility = View.VISIBLE + humidityInfo.visibility = View.VISIBLE + forecastContainer.visibility = View.VISIBLE + + val condIcon: Drawable? = OmniJawsClient.get().getWeatherConditionImage(context, info.conditionCode) + if (condIcon != null) { + currentIcon.setImageDrawable(condIcon) + } + + currentTemp.text = "${info.temp}${info.tempUnits}" + cityName.text = info.city ?: "" + + windInfo.text = "${info.windSpeed} ${info.windUnits}" + windDirection.text = info.windDirection ?: "" + + humidityInfo.text = "${info.humidity}%" + + populateForecasts(info.forecasts ?: emptyList()) + } + + private fun populateForecasts(forecasts: List) { + forecastContainer.removeAllViews() + + val inflater = android.view.LayoutInflater.from(context) + val displayForecasts = forecasts.take(5) + + for (day in displayForecasts) { + val dayView = inflater.inflate(R.layout.weather_forecast_day_item, forecastContainer, false) + + val dayLabel = dayView.findViewById(R.id.forecast_day_label) + val dayIcon = dayView.findViewById(R.id.forecast_day_icon) + val dayHigh = dayView.findViewById(R.id.forecast_day_high) + val dayLow = dayView.findViewById(R.id.forecast_day_low) + + dayLabel.text = formatForecastDate(day.date) + + val icon: Drawable? = OmniJawsClient.get().getWeatherConditionImage(context, day.conditionCode) + if (icon != null) { + dayIcon.setImageDrawable(icon) + } + + dayHigh.text = day.high + dayLow.text = day.low + + forecastContainer.addView(dayView) + } + } + + private fun formatForecastDate(dateStr: String?): String { + if (dateStr.isNullOrEmpty()) return "" + return try { + val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) + val date: Date = inputFormat.parse(dateStr) ?: return dateStr + SimpleDateFormat("EEE", Locale.getDefault()).format(date) + } catch (e: Exception) { + dateStr + } + } + + private fun showNoData() { + noDataView.visibility = View.VISIBLE + currentIcon.visibility = View.GONE + currentTemp.visibility = View.GONE + cityName.visibility = View.GONE + windInfo.visibility = View.GONE + windDirection.visibility = View.GONE + humidityInfo.visibility = View.GONE + forecastContainer.visibility = View.GONE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogController.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogController.kt new file mode 100644 index 0000000000000..9433e54dd14df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogController.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import com.android.systemui.animation.DialogTransitionAnimator + +/** Wrapper controller that skips exit animation checks for the weather dialog. */ +class WeatherDialogController( + private val delegate: DialogTransitionAnimator.Controller +) : DialogTransitionAnimator.Controller by delegate { + + override val skipExitAnimationChecks: Boolean = true +} diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogDelegate.kt new file mode 100644 index 0000000000000..9b51404203d48 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogDelegate.kt @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.Window +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import com.android.systemui.statusbar.phone.SystemUIDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope + +private const val TAG = "WeatherDialogDelegate" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +/** Delegate for the weather forecast QS tile dialog. */ +class WeatherDialogDelegate +@AssistedInject +constructor( + private val weatherContentManager: WeatherContentManager, + private val weatherDialogManager: WeatherDialogManager, + private val systemUIDialogFactory: SystemUIDialog.Factory, + private val shadeDialogContextInteractor: ShadeDialogContextInteractor, + @Assisted private val coroutineScope: CoroutineScope, +) : SystemUIDialog.Delegate { + + private lateinit var dialog: SystemUIDialog + private lateinit var lifecycleRegistry: LifecycleRegistry + private lateinit var lifecycleOwner: LifecycleOwner + + @AssistedFactory + interface Factory { + fun create(coroutineScope: CoroutineScope): WeatherDialogDelegate + } + + override fun createDialog(): SystemUIDialog { + if (DEBUG) Log.d(TAG, "createDialog") + + dialog = systemUIDialogFactory.create(this, shadeDialogContextInteractor.context) + + lifecycleOwner = object : LifecycleOwner { + override val lifecycle: Lifecycle + get() = lifecycleRegistry + } + lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + return dialog + } + + override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + if (DEBUG) Log.d(TAG, "onCreate") + + val context = dialog.context + val dialogView = LayoutInflater.from(context).inflate( + R.layout.weather_tile_dialog, null + ) + dialogView.accessibilityPaneTitle = context.getText(R.string.omnijaws_label_default) + + val window: Window = dialog.window!! + window.setContentView(dialogView) + window.setWindowAnimations(R.style.Animation_InternetDialog) + + val params = window.attributes + // ~65% of screen width — compact forecast card + params.width = (context.resources.displayMetrics.widthPixels * 0.65).toInt() + window.attributes = params + + lifecycleRegistry.currentState = Lifecycle.State.CREATED + weatherContentManager.bind(dialogView, coroutineScope) + } + + override fun onStart(dialog: SystemUIDialog) { + if (DEBUG) Log.d(TAG, "onStart") + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + weatherContentManager.start() + } + + override fun onStop(dialog: SystemUIDialog) { + if (DEBUG) Log.d(TAG, "onStop") + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + weatherContentManager.stop() + weatherDialogManager.destroyDialog() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogManager.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogManager.kt new file mode 100644 index 0000000000000..b799ff71c2735 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogManager.kt @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import android.util.Log +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.coroutines.newTracingContext +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.phone.SystemUIDialog +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel + +private const val TAG = "WeatherDialogManager" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +@SysUISingleton +class WeatherDialogManager +@Inject +constructor( + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val dialogFactory: WeatherDialogDelegate.Factory, + @Main private val mainDispatcher: CoroutineDispatcher, +) { + private lateinit var coroutineScope: CoroutineScope + + companion object { + private const val INTERACTION_JANK_TAG = "weather" + var dialog: SystemUIDialog? = null + } + + fun create(expandable: Expandable?) { + if (dialog != null) { + if (DEBUG) Log.d(TAG, "WeatherDialog is already showing.") + return + } + + if (DEBUG) Log.d(TAG, "Creating weather dialog") + + coroutineScope = CoroutineScope(mainDispatcher + newTracingContext("WeatherDialogScope")) + dialog = dialogFactory.create(coroutineScope).createDialog() + + val standardController = expandable?.dialogTransitionController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) + ) + + val controller = if (standardController != null) { + WeatherDialogController(standardController) + } else { + null + } + + if (controller != null) { + dialogTransitionAnimator.show( + dialog!!, + controller, + animateBackgroundBoundsChange = true, + ) + } else { + dialog?.show() + } + } + + fun destroyDialog() { + if (DEBUG) Log.d(TAG, "destroyDialog") + if (::coroutineScope.isInitialized) { + coroutineScope.cancel() + } + dialog = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java index 91b5b7a7831ed..452ee763a574d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2017 The OmniROM project - * Copyright (C) 2022-2025 crDroid Android project + * Copyright (C) 2022-2026 crDroid Android project + * Copyright (C) 2026 AxionOS Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +18,11 @@ package com.android.systemui.qs.tiles; -import android.content.Context; -import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.service.quicksettings.Tile; @@ -32,42 +32,60 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.alpha.OmniJawsClient; -import com.android.internal.util.alpha.Utils; +import com.android.systemui.alpha.tiles.dialog.WeatherDialogManager; import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData; import com.android.systemui.plugins.qs.QSTile.BooleanState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.quicklook.QuickLookClient; import com.android.systemui.res.R; +import com.android.systemui.shared.clocks.WeatherUtils; import javax.inject.Inject; -public class WeatherTile extends QSTileImpl implements OmniJawsClient.OmniJawsObserver { +/** + * Quick Settings weather tile. Data comes from the AxQuickLook privileged service via {@link + * QuickLookClient} (same pipeline as the Axion lockscreen clock), including OmniJaws vs Google + * weather resolution inside AxQuickLook. + */ +public class WeatherTile extends QSTileImpl { public static final String TILE_SPEC = "weather"; - private static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; + private static final String AX_QUICK_LOOK_PACKAGE = "com.android.axion.quicklook"; + private static final String AX_QUICK_LOOK_SETTINGS_ACTION = + "com.android.axion.quicklook.SETTINGS"; private static final String TAG = "WeatherTile"; - private static final boolean DEBUG = false; private Drawable mWeatherImage; - private OmniJawsClient.WeatherInfo mWeatherData; - private boolean mEnabled; - private final ActivityStarter mActivityStarter; - private String mFormattedCondition; + private static final boolean DEBUG = false; + + private static final int WEATHER_ICON_DP = 32; - private static final String[] ALTERNATIVE_WEATHER_APPS = { - "cz.martykan.forecastie", - "com.accuweather.android", - "com.wunderground.android.weather", - "com.samruston.weather", - "jp.miyavi.androiod.gnws", - }; + private final ActivityStarter mActivityStarter; + private final QuickLookClient mQuickLookClient; + private final WeatherDialogManager mWeatherDialogManager; + private final Handler mMainHandler; + + /** Non-null only when AxQuickLook reports usable weather. */ + @Nullable private ClockWeatherData mWeather; + + private final QuickLookClient.Callback mQuickLookCallback = + new QuickLookClient.Callback() { + @Override + public void onClockDataChanged(ClockData data) { + ClockWeatherData w = data.getWeather(); + mWeather = isMeaningfulWeather(w) ? w : null; + refreshState(); + } + }; @Inject public WeatherTile( @@ -79,12 +97,22 @@ public WeatherTile( MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, - QSLogger qsLogger - ) { + QSLogger qsLogger, + QuickLookClient quickLookClient, + WeatherDialogManager weatherDialogManager) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); - mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); mActivityStarter = activityStarter; + mQuickLookClient = quickLookClient; + mWeatherDialogManager = weatherDialogManager; + mMainHandler = mainHandler; + } + + private static boolean isMeaningfulWeather(@Nullable ClockWeatherData w) { + if (w == null) return false; + boolean hasTemp = w.getTemp() != null && !w.getTemp().isEmpty(); + boolean hasCondition = w.getCondition() != null && !w.getCondition().isEmpty(); + return hasTemp || hasCondition || w.getConditionCode() != 0; } @Override @@ -100,138 +128,102 @@ public BooleanState newTileState() { @Override public void handleSetListening(boolean listening) { if (DEBUG) Log.d(TAG, "setListening " + listening); - mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); - if (listening) { - OmniJawsClient.get().addObserver(mContext, this); - queryAndUpdateWeather(); + mQuickLookClient.addCallback(mQuickLookCallback); } else { - OmniJawsClient.get().removeObserver(mContext, this); - } - } - - @Override - public void weatherUpdated() { - if (DEBUG) Log.d(TAG, "weatherUpdated"); - queryAndUpdateWeather(); - } - - @Override - public void weatherError(int errorReason) { - if (DEBUG) Log.d(TAG, "weatherError " + errorReason); - if (errorReason != OmniJawsClient.EXTRA_ERROR_DISABLED) { - mWeatherData = null; - refreshState(); + mQuickLookClient.removeCallback(mQuickLookCallback); } } @Override protected void handleDestroy() { - OmniJawsClient.get().removeObserver(mContext, this); + mQuickLookClient.removeCallback(mQuickLookCallback); super.handleDestroy(); } @Override public boolean isAvailable() { - return OmniJawsClient.get().isOmniJawsServiceInstalled(mContext); + try { + mContext.getPackageManager().getPackageInfo(AX_QUICK_LOOK_PACKAGE, 0); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } } @Override protected void handleClick(@Nullable Expandable expandable) { - if (DEBUG) Log.d(TAG, "handleClick"); - if (!mState.value || mWeatherData == null) { - mActivityStarter.postStartActivityDismissingKeyguard( - OmniJawsClient.get().getSettingsIntent(), 0); - } else { - PackageManager pm = mContext.getPackageManager(); - for (String app: ALTERNATIVE_WEATHER_APPS) { - if (Utils.isPackageInstalled(mContext, app)) { - Intent intent = pm.getLaunchIntentForPackage(app); - if (intent != null) { - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); - } - } - } - if (Utils.isPackageInstalled(mContext, "com.google.android.googlequicksearchbox")) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("dynact://velour/weather/ProxyActivity")); - intent.setComponent(new ComponentName("com.google.android.googlequicksearchbox", - "com.google.android.apps.gsa.velour.DynamicActivityTrampoline")); - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); - } else { - final Intent weatherActivityIntent = new Intent(); - weatherActivityIntent.setAction(Intent.ACTION_MAIN); - weatherActivityIntent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".WeatherActivity"); - mActivityStarter.postStartActivityDismissingKeyguard(weatherActivityIntent, 0); - } - } - mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); - refreshState(); + // handleClick runs on the background looper; dialog must be shown on the main thread. + mMainHandler.post(() -> mWeatherDialogManager.create(expandable)); } @Override public Intent getLongClickIntent() { - if (DEBUG) Log.d(TAG, "getLongClickIntent"); - return OmniJawsClient.get().getSettingsIntent(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName("com.android.settings", + "com.android.settings.Settings$LockScreenSettingsActivity"); + return intent; } @Override protected void handleUpdateState(BooleanState state, Object arg) { - if (DEBUG) Log.d(TAG, "handleUpdateState " + mEnabled); - state.value = mEnabled; - state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + boolean hasWeather = mWeather != null; + state.value = hasWeather; + state.state = hasWeather ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; state.icon = ResourceIcon.get(R.drawable.ic_qs_weather); state.label = mContext.getResources().getString(R.string.omnijaws_label_default); state.secondaryLabel = mContext.getResources().getString(R.string.omnijaws_service_unknown); - if (mEnabled) { - if (mWeatherData == null || mWeatherImage == null) { - state.label = mContext.getResources().getString(R.string.omnijaws_label_default); - state.secondaryLabel = mContext.getResources().getString(R.string.omnijaws_service_error); - } else { - state.icon = new DrawableIcon(mWeatherImage); - state.label = mWeatherData.city; - state.secondaryLabel = mWeatherData.temp + mWeatherData.tempUnits + - " · " + mFormattedCondition; - } + + if (!hasWeather || mWeather == null) { + return; + } + + int iconPx = (int) (mContext.getResources().getDisplayMetrics().density * WEATHER_ICON_DP + 0.5f); + Bitmap bmp = WeatherUtils.INSTANCE.resolveWeatherBitmap(mContext, mWeather, iconPx); + if (bmp != null) { + Drawable d = new BitmapDrawable(mContext.getResources(), bmp); + state.icon = new DrawableIcon(d); + } + + String city = mWeather.getCity(); + if (city != null && !city.isEmpty()) { + state.label = city; + } + + String tempPart = mWeather.getFormattedTemp(); + String cond = localizeCondition(mWeather.getCondition()); + state.secondaryLabel = tempPart + " · " + cond; + } + + /** + * Maps English-ish OmniJaws/Google condition phrases to localized summaries where we have + * matching cr_strings (legacy OmniJaws tile behavior). + */ + private String localizeCondition(String condition) { + if (condition == null || condition.isEmpty()) { + return ""; + } + String lower = condition.toLowerCase(); + if (lower.contains("cloud")) { + return mContext.getResources().getString(R.string.weather_condition_clouds); + } else if (lower.contains("rain")) { + return mContext.getResources().getString(R.string.weather_condition_rain); + } else if (lower.contains("clear")) { + return mContext.getResources().getString(R.string.weather_condition_clear); + } else if (lower.contains("storm")) { + return mContext.getResources().getString(R.string.weather_condition_storm); + } else if (lower.contains("snow")) { + return mContext.getResources().getString(R.string.weather_condition_snow); + } else if (lower.contains("wind")) { + return mContext.getResources().getString(R.string.weather_condition_wind); + } else if (lower.contains("mist")) { + return mContext.getResources().getString(R.string.weather_condition_mist); } + return condition; } @Override public CharSequence getTileLabel() { return mContext.getResources().getString(R.string.omnijaws_label_default); } - - private void queryAndUpdateWeather() { - if (DEBUG) Log.d(TAG, "queryAndUpdateWeather " + mEnabled); - try { - mWeatherData = null; - if (mEnabled) { - OmniJawsClient.get().queryWeather(mContext); - mWeatherData = OmniJawsClient.get().getWeatherInfo(); - mFormattedCondition = mWeatherData.condition; - if (mFormattedCondition.toLowerCase().contains("clouds")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_clouds); - } else if (mFormattedCondition.toLowerCase().contains("rain")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_rain); - } else if (mFormattedCondition.toLowerCase().contains("clear")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_clear); - } else if (mFormattedCondition.toLowerCase().contains("storm")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_storm); - } else if (mFormattedCondition.toLowerCase().contains("snow")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_snow); - } else if (mFormattedCondition.toLowerCase().contains("wind")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_wind); - } else if (mFormattedCondition.toLowerCase().contains("mist")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_mist); - } - if (mWeatherData != null) { - mWeatherImage = OmniJawsClient.get().getWeatherConditionImage(mContext, mWeatherData.conditionCode); - mWeatherImage = mWeatherImage.mutate(); - } - } - } catch(Exception e) { - // Do nothing - } - refreshState(); - } } From dd23378e9e84d4d0ce23d60b89d66cf15e74b357 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:05:41 +0100 Subject: [PATCH 10/51] SystemUI: Nuke AOSP KeyguardSlice and legacy weather views --- .../KeyguardSliceViewControllerTest.java | 126 ----- .../keyguard/KeyguardSliceViewTest.java | 109 ---- .../layout/keyguard_slice_view.xml | 45 -- .../layout/keyguard_weather_area.xml | 39 -- .../android/keyguard/KeyguardSliceView.java | 500 ------------------ .../keyguard/KeyguardSliceViewController.java | 283 ---------- .../KeyguardSmartspaceRepository.kt | 33 +- .../ui/binder/KeyguardRootViewBinder.kt | 10 - .../ui/binder/KeyguardSliceViewBinder.kt | 59 --- .../blueprints/DefaultKeyguardBlueprint.kt | 6 - .../blueprints/SplitShadeKeyguardBlueprint.kt | 6 - .../sections/KeyguardSliceViewSection.kt | 150 ------ .../sections/KeyguardWeatherViewSection.kt | 117 ---- .../systemui/weather/WeatherImageView.kt | 65 --- .../systemui/weather/WeatherInfoView.kt | 59 --- .../systemui/weather/WeatherTextView.kt | 54 -- .../systemui/weather/WeatherViewController.kt | 213 -------- 17 files changed, 7 insertions(+), 1867 deletions(-) delete mode 100644 packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java delete mode 100644 packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java delete mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml delete mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_weather_area.xml delete mode 100644 packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java delete mode 100644 packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java delete mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSliceViewBinder.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/weather/WeatherInfoView.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java deleted file mode 100644 index e6c27d4290ee5..0000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.keyguard; - -import static org.junit.Assume.assumeFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.pm.PackageManager; -import android.os.Handler; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; -import android.view.View; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.settings.FakeDisplayTracker; -import com.android.systemui.statusbar.policy.ConfigurationController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@RunWithLooper(setAsMainLooper = true) -public class KeyguardSliceViewControllerTest extends SysuiTestCase { - @Mock - private KeyguardSliceView mView; - @Mock - private ConfigurationController mConfigurationController; - @Mock - private ActivityStarter mActivityStarter; - private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); - private DumpManager mDumpManager = new DumpManager(); - private Handler mHandler; - private Handler mBgHandler; - private KeyguardSliceViewController mController; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - TestableLooper testableLooper = TestableLooper.get(this); - assert testableLooper != null; - mHandler = new Handler(testableLooper.getLooper()); - mBgHandler = new Handler(testableLooper.getLooper()); - when(mView.isAttachedToWindow()).thenReturn(true); - when(mView.getContext()).thenReturn(mContext); - mController = new KeyguardSliceViewController(mHandler, mBgHandler, mView, - mActivityStarter, mConfigurationController, mDumpManager, - mDisplayTracker, null, null); - mController.setupUri(KeyguardSliceProvider.KEYGUARD_SLICE_URI); - } - - @After - public void tearDown() { - mController.onViewDetached(); - } - - @Test - public void refresh_replacesSliceContentAndNotifiesListener() { - // Skips the test if running on a watch because watches don't have a SliceManager system - // service. - assumeFalse(isWatch()); - - mController.refresh(); - verify(mView).hideSlice(); - } - - @Test - public void onAttachedToWindow_registersListeners() { - // Skips the test if running on a watch because watches don't have a SliceManager system - // service. - assumeFalse(isWatch()); - - mController.init(); - verify(mConfigurationController).addCallback( - any(ConfigurationController.ConfigurationListener.class)); - } - - @Test - public void onDetachedFromWindow_unregistersListeners() { - // Skips the test if running on a watch because watches don't have a SliceManager system - // service. - assumeFalse(isWatch()); - - ArgumentCaptor attachListenerArgumentCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - - mController.init(); - verify(mView).addOnAttachStateChangeListener(attachListenerArgumentCaptor.capture()); - - attachListenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView); - - verify(mConfigurationController).removeCallback( - any(ConfigurationController.ConfigurationListener.class)); - } - - private boolean isWatch() { - final PackageManager pm = mContext.getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_WATCH); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java deleted file mode 100644 index d96518abc0070..0000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -package com.android.keyguard; - -import android.graphics.Color; -import android.net.Uri; -import android.testing.TestableLooper.RunWithLooper; -import android.view.LayoutInflater; - -import androidx.slice.Slice; -import androidx.slice.SliceProvider; -import androidx.slice.SliceSpecs; -import androidx.slice.builders.ListBuilder; -import androidx.slice.widget.RowContent; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.res.R; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockitoAnnotations; - -import java.util.Collections; -import java.util.HashSet; -import java.util.concurrent.atomic.AtomicBoolean; - -@SmallTest -@RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidJUnit4.class) -public class KeyguardSliceViewTest extends SysuiTestCase { - private KeyguardSliceView mKeyguardSliceView; - private Uri mSliceUri; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - LayoutInflater layoutInflater = LayoutInflater.from(getContext()); - mKeyguardSliceView = (KeyguardSliceView) layoutInflater - .inflate(R.layout.keyguard_slice_view, null); - mSliceUri = Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI); - SliceProvider.setSpecs(new HashSet<>(Collections.singletonList(SliceSpecs.LIST))); - } - - @Test - public void showSlice_notifiesListener() { - ListBuilder builder = new ListBuilder(getContext(), mSliceUri, ListBuilder.INFINITY); - builder.setHeader(new ListBuilder.HeaderBuilder().setTitle("header title!")); - Slice slice = builder.build(); - RowContent rowContent = new RowContent(slice.getItemArray()[0], 0); - - AtomicBoolean notified = new AtomicBoolean(); - mKeyguardSliceView.setContentChangeListener(()-> notified.set(true)); - mKeyguardSliceView.showSlice(rowContent, Collections.EMPTY_LIST); - Assert.assertTrue("Listener should be notified about slice changes.", - notified.get()); - } - - @Test - public void showSlice_emptySliceNotifiesListener() { - AtomicBoolean notified = new AtomicBoolean(); - mKeyguardSliceView.setContentChangeListener(()-> notified.set(true)); - mKeyguardSliceView.showSlice(null, Collections.EMPTY_LIST); - Assert.assertTrue("Listener should be notified about slice changes.", - notified.get()); - } - - @Test - public void hasHeader_readsSliceData() { - ListBuilder builder = new ListBuilder(getContext(), mSliceUri, ListBuilder.INFINITY); - mKeyguardSliceView.showSlice(null, Collections.EMPTY_LIST); - Assert.assertFalse("View should not have a header", mKeyguardSliceView.hasHeader()); - - builder.setHeader(new ListBuilder.HeaderBuilder().setTitle("header title!")); - Slice slice = builder.build(); - RowContent rowContent = new RowContent(slice.getItemArray()[0], 0); - mKeyguardSliceView.showSlice(rowContent, Collections.EMPTY_LIST); - Assert.assertTrue("View should have a header", mKeyguardSliceView.hasHeader()); - } - - @Test - public void getTextColor_whiteTextWhenAOD() { - // Set text color to red since the default is white and test would always pass - mKeyguardSliceView.setTextColor(Color.RED); - mKeyguardSliceView.setDarkAmount(0); - Assert.assertEquals("Should be using regular text color", Color.RED, - mKeyguardSliceView.getTextColor()); - mKeyguardSliceView.setDarkAmount(1); - Assert.assertEquals("Should be using AOD text color", Color.WHITE, - mKeyguardSliceView.getTextColor()); - } -} diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml deleted file mode 100644 index e6cba028abd3e..0000000000000 --- a/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_weather_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_weather_area.xml deleted file mode 100644 index 8fa1eaafcf83a..0000000000000 --- a/packages/SystemUI/res-keyguard/layout/keyguard_weather_area.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java deleted file mode 100644 index a1ab0f6248d82..0000000000000 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java +++ /dev/null @@ -1,500 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.keyguard; - -import android.animation.LayoutTransition; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.annotation.ColorInt; -import android.annotation.StyleRes; -import android.app.PendingIntent; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.InsetDrawable; -import android.graphics.text.LineBreaker; -import android.net.Uri; -import android.os.Trace; -import android.text.TextUtils; -import android.text.TextUtils.TruncateAt; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.view.animation.Animation; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.slice.SliceItem; -import androidx.slice.core.SliceQuery; -import androidx.slice.widget.RowContent; -import androidx.slice.widget.SliceContent; - -import com.android.app.animation.Interpolators; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.graphics.ColorUtils; -import com.android.settingslib.Utils; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.res.R; -import com.android.systemui.util.wakelock.KeepAwakeAnimationListener; - -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; - -/** - * View visible under the clock on the lock screen and AoD. - */ -public class KeyguardSliceView extends LinearLayout { - - private static final String TAG = "KeyguardSliceView"; - public static final int DEFAULT_ANIM_DURATION = 550; - - private final LayoutTransition mLayoutTransition; - @VisibleForTesting - TextView mTitle; - private Row mRow; - private int mTextColor; - private float mDarkAmount = 0; - - private int mIconSize; - private int mIconSizeWithHeader; - /** - * Runnable called whenever the view contents change. - */ - private Runnable mContentChangeListener; - private boolean mHasHeader; - private View.OnClickListener mOnClickListener; - - public KeyguardSliceView(Context context, AttributeSet attrs) { - super(context, attrs); - - Resources resources = context.getResources(); - mLayoutTransition = new LayoutTransition(); - mLayoutTransition.setStagger(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION / 2); - mLayoutTransition.setDuration(LayoutTransition.APPEARING, DEFAULT_ANIM_DURATION); - mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 2); - mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); - mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); - mLayoutTransition.setInterpolator(LayoutTransition.APPEARING, - Interpolators.FAST_OUT_SLOW_IN); - mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT); - mLayoutTransition.setAnimateParentHierarchy(false); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTitle = findViewById(R.id.title); - mRow = findViewById(R.id.row); - mTextColor = Utils.getColorAttrDefaultColor(mContext, R.attr.wallpaperTextColor); - mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size); - mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size); - mTitle.setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED); - } - - @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - setLayoutTransition(isVisible ? mLayoutTransition : null); - } - - /** - * Returns whether the current visible slice has a title/header. - */ - public boolean hasHeader() { - return mHasHeader; - } - - void hideSlice() { - mTitle.setVisibility(GONE); - mRow.setVisibility(GONE); - mHasHeader = false; - if (mContentChangeListener != null) { - mContentChangeListener.run(); - } - } - - private LinearLayout createSubRow() { - LinearLayout layout = new LinearLayout(mContext); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - (LinearLayout.LayoutParams) mRow.getLayoutParams()); - lp.setMarginEnd(mContext.getResources().getDimensionPixelSize( - R.dimen.widget_horizontal_padding)); - layout.setLayoutParams(lp); - layout.setOrientation(LinearLayout.HORIZONTAL); - layout.setVisibility(View.VISIBLE); - return layout; - } - - Map showSlice(RowContent header, List subItems) { - Trace.beginSection("KeyguardSliceView#showSlice"); - mHasHeader = header != null; - Map clickActions = new HashMap<>(); - - if (!mHasHeader) { - mTitle.setVisibility(GONE); - } else { - mTitle.setVisibility(VISIBLE); - - SliceItem mainTitle = header.getTitleItem(); - CharSequence title = mainTitle != null ? mainTitle.getText() : null; - mTitle.setText(title); - if (header.getPrimaryAction() != null - && header.getPrimaryAction().getAction() != null) { - clickActions.put(mTitle, header.getPrimaryAction().getAction()); - } - } - - final int subItemsCount = subItems.size(); - final int blendedColor = getTextColor(); - final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it - mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE); - LinearLayout.LayoutParams layoutParams = (LayoutParams) mRow.getLayoutParams(); - layoutParams.gravity = Gravity.START; - mRow.setLayoutParams(layoutParams); - - int rowIndex = 0, colIndex = 0; - LinearLayout subRow = null; - for (SliceContent sc : subItems.subList(startIndex, subItems.size())) { - subRow = (LinearLayout) mRow.getChildAt(rowIndex); - if (subRow == null) { - subRow = createSubRow(); - mRow.addView(subRow, rowIndex); - } - RowContent rc = (RowContent) sc; - SliceItem item = rc.getSliceItem(); - final Uri itemTag = item.getSlice().getUri(); - // Try to reuse the view if already exists in the layout - KeyguardSliceTextView button = mRow.findViewWithTag(itemTag); - if (button == null) { - button = new KeyguardSliceTextView(mContext); - button.setTextColor(blendedColor); - button.setTag(itemTag); - subRow.addView(button, colIndex); - } else { - LinearLayout buttonParent = (LinearLayout) button.getParent(); - if (mRow.indexOfChild(buttonParent) != rowIndex - || buttonParent.indexOfChild(button) != colIndex) { - buttonParent.removeView(button); - subRow.addView(button, colIndex); - } - } - colIndex++; - - PendingIntent pendingIntent = null; - if (rc.getPrimaryAction() != null) { - pendingIntent = rc.getPrimaryAction().getAction(); - } - clickActions.put(button, pendingIntent); - - final SliceItem titleItem = rc.getTitleItem(); - button.setText(titleItem == null ? null : titleItem.getText()); - button.setContentDescription(rc.getContentDescription()); - - Drawable iconDrawable = null; - SliceItem icon = SliceQuery.find(item.getSlice(), - android.app.slice.SliceItem.FORMAT_IMAGE); - if (icon != null) { - final int iconSize = mHasHeader ? mIconSizeWithHeader : mIconSize; - iconDrawable = icon.getIcon().loadDrawable(mContext); - if (iconDrawable != null) { - if (iconDrawable instanceof InsetDrawable) { - // System icons (DnD) use insets which are fine for centered slice content - // but will cause a slight indent for left/right-aligned slice views - iconDrawable = ((InsetDrawable) iconDrawable).getDrawable(); - } - final int width = (int) (iconDrawable.getIntrinsicWidth() - / (float) iconDrawable.getIntrinsicHeight() * iconSize); - iconDrawable.setBounds(0, 0, Math.max(width, 1), iconSize); - } - } - button.setCompoundDrawablesRelative(iconDrawable, null, null, null); - button.setOnClickListener(mOnClickListener); - button.setClickable(pendingIntent != null); - - if (rc.hasBottomDivider()) { - rowIndex++; - colIndex = 0; - } else { - LinearLayout.LayoutParams lp = - (LinearLayout.LayoutParams) button.getLayoutParams(); - lp.setMarginEnd(mContext.getResources().getDimensionPixelSize( - R.dimen.widget_horizontal_padding)); - button.setLayoutParams(lp); - } - } - - // Removing old views - for (int i = 0; i < mRow.getChildCount(); i++) { - subRow = (LinearLayout) mRow.getChildAt(i); - for (int j = 0; j < subRow.getChildCount(); j++) { - View child = subRow.getChildAt(j); - if (!clickActions.containsKey(child)) { - subRow.removeView(child); - j--; - } - } - if (subRow.getChildCount() == 0) { - mRow.removeView(subRow); - i--; - } - } - - if (mContentChangeListener != null) { - mContentChangeListener.run(); - } - Trace.endSection(); - - return clickActions; - } - - public void setDarkAmount(float darkAmount) { - mDarkAmount = darkAmount; - mRow.setDarkAmount(darkAmount); - updateTextColors(); - } - - private void updateTextColors() { - final int blendedColor = getTextColor(); - mTitle.setTextColor(blendedColor); - mRow.performOnChildren(tv -> tv.setTextColor(blendedColor)); - } - - /** - * Runnable that gets invoked every time the title or the row visibility changes. - * @param contentChangeListener The listener. - */ - public void setContentChangeListener(Runnable contentChangeListener) { - mContentChangeListener = contentChangeListener; - } - - @VisibleForTesting - int getTextColor() { - return ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); - } - - @VisibleForTesting - void setTextColor(@ColorInt int textColor) { - mTextColor = textColor; - updateTextColors(); - } - - void onDensityOrFontScaleChanged() { - mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.widget_icon_size); - mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size); - - mRow.performOnChildren(tv -> tv.onDensityOrFontScaleChanged()); - } - - void onOverlayChanged() { - mRow.performOnChildren(tv -> tv.onOverlayChanged()); - } - - public void dump(PrintWriter pw, String[] args) { - pw.println("KeyguardSliceView:"); - pw.println(" mTitle: " + (mTitle == null ? "null" : mTitle.getVisibility() == VISIBLE)); - pw.println(" mRow: " + (mRow == null ? "null" : mRow.getVisibility() == VISIBLE)); - pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); - pw.println(" mDarkAmount: " + mDarkAmount); - pw.println(" mHasHeader: " + mHasHeader); - } - - @Override - public void setOnClickListener(View.OnClickListener onClickListener) { - mOnClickListener = onClickListener; - mTitle.setOnClickListener(onClickListener); - } - - public static class Row extends LinearLayout { - /** - * This view is visible in AOD, which means that the device will sleep if we - * don't hold a wake lock. We want to enter doze only after all views have reached - * their desired positions. - */ - private final Animation.AnimationListener mKeepAwakeListener; - private LayoutTransition mLayoutTransition; - private float mDarkAmount; - - public Row(Context context) { - this(context, null); - } - - public Row(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public Row(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public Row(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - mKeepAwakeListener = new KeepAwakeAnimationListener(mContext); - } - - @Override - protected void onFinishInflate() { - mLayoutTransition = new LayoutTransition(); - mLayoutTransition.setDuration(DEFAULT_ANIM_DURATION); - - PropertyValuesHolder left = PropertyValuesHolder.ofInt("left", 0, 1); - PropertyValuesHolder right = PropertyValuesHolder.ofInt("right", 0, 1); - ObjectAnimator changeAnimator = ObjectAnimator.ofPropertyValuesHolder((Object) null, - left, right); - mLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, changeAnimator); - mLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, changeAnimator); - mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_APPEARING, - Interpolators.ACCELERATE_DECELERATE); - mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING, - Interpolators.ACCELERATE_DECELERATE); - mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_APPEARING, - DEFAULT_ANIM_DURATION); - mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, - DEFAULT_ANIM_DURATION); - - ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); - mLayoutTransition.setAnimator(LayoutTransition.APPEARING, appearAnimator); - mLayoutTransition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN); - - ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); - mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING, - Interpolators.ALPHA_OUT); - mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 4); - mLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator); - - mLayoutTransition.setAnimateParentHierarchy(false); - } - - @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - setLayoutTransition(isVisible ? mLayoutTransition : null); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = MeasureSpec.getSize(widthMeasureSpec); - - performOnChildren(tv -> tv.setMaxWidth(Integer.MAX_VALUE)); - - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - /** - * Set the amount (ratio) that the device has transitioned to doze. - * - * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. - */ - public void setDarkAmount(float darkAmount) { - boolean isDozing = darkAmount != 0; - boolean wasDozing = mDarkAmount != 0; - if (isDozing == wasDozing) { - return; - } - mDarkAmount = darkAmount; - setLayoutAnimationListener(isDozing ? null : mKeepAwakeListener); - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } - - public void performOnChildren(Consumer action) { - for (int i = 0; i < getChildCount(); i++) { - LinearLayout subRow = (LinearLayout) getChildAt(i); - for (int j = 0; j < subRow.getChildCount(); j++) { - View child = subRow.getChildAt(j); - if (child instanceof KeyguardSliceTextView) { - action.accept((KeyguardSliceTextView) child); - } - } - } - } - } - - /** - * Representation of an item that appears under the clock on main keyguard message. - */ - @VisibleForTesting - static class KeyguardSliceTextView extends TextView { - - @StyleRes - private static int sStyleId = R.style.TextAppearance_Keyguard_Secondary; - - KeyguardSliceTextView(Context context) { - super(context, null /* attrs */, 0 /* styleAttr */, sStyleId); - onDensityOrFontScaleChanged(); - setEllipsize(TruncateAt.END); - } - - public void onDensityOrFontScaleChanged() { - updatePadding(); - } - - public void onOverlayChanged() { - setTextAppearance(sStyleId); - } - - @Override - public void setText(CharSequence text, BufferType type) { - super.setText(text, type); - updatePadding(); - } - - private void updatePadding() { - boolean hasText = !TextUtils.isEmpty(getText()); - boolean isDate = Uri.parse(KeyguardSliceProvider.KEYGUARD_DATE_URI).equals(getTag()); - int padding = (int) getContext().getResources() - .getDimension(R.dimen.widget_horizontal_padding) / 2; - int iconPadding = (int) mContext.getResources() - .getDimension(R.dimen.widget_icon_padding); - // orientation is vertical, so add padding to top & bottom - setPadding(0, padding, 0, hasText ? padding : 0); - - setCompoundDrawablePadding(iconPadding); - } - - @Override - public void setTextColor(int color) { - super.setTextColor(color); - updateDrawableColors(); - } - - @Override - public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, - Drawable bottom) { - super.setCompoundDrawablesRelative(start, top, end, bottom); - updateDrawableColors(); - updatePadding(); - } - - private void updateDrawableColors() { - final int color = getCurrentTextColor(); - for (Drawable drawable : getCompoundDrawables()) { - if (drawable != null) { - drawable.setTint(color); - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java deleted file mode 100644 index f8bde98014abe..0000000000000 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.keyguard; - -import static android.app.slice.Slice.HINT_LIST_ITEM; - -import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; - -import android.app.PendingIntent; -import android.net.Uri; -import android.os.Handler; -import android.os.Trace; -import android.util.Log; -import android.view.Display; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; -import androidx.slice.Slice; -import androidx.slice.SliceViewManager; -import androidx.slice.widget.ListContent; -import androidx.slice.widget.RowContent; -import androidx.slice.widget.SliceContent; -import androidx.slice.widget.SliceLiveData; - -import com.android.systemui.Dumpable; -import com.android.systemui.Flags; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.power.domain.interactor.PowerInteractor; -import com.android.systemui.power.shared.model.ScreenPowerState; -import com.android.systemui.settings.DisplayTracker; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.util.ViewController; - -import kotlin.coroutines.CoroutineContext; -import kotlin.coroutines.EmptyCoroutineContext; - -import java.io.PrintWriter; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -/** Controller for a {@link KeyguardSliceView}. */ -public class KeyguardSliceViewController extends ViewController implements - Dumpable { - private static final String TAG = "KeyguardSliceViewCtrl"; - - private final Handler mHandler; - private final Handler mBgHandler; - private final ActivityStarter mActivityStarter; - private final ConfigurationController mConfigurationController; - private final DumpManager mDumpManager; - private final DisplayTracker mDisplayTracker; - private int mDisplayId; - private LiveData mLiveData; - private Uri mKeyguardSliceUri; - private Slice mSlice; - private Map mClickActions; - private KeyguardInteractor mKeyguardInteractor; - private PowerInteractor mPowerInteractor; - - ConfigurationController.ConfigurationListener mConfigurationListener = - new ConfigurationController.ConfigurationListener() { - @Override - public void onDensityOrFontScaleChanged() { - mView.onDensityOrFontScaleChanged(); - } - @Override - public void onThemeChanged() { - mView.onOverlayChanged(); - } - }; - - Observer mObserver = new Observer() { - @Override - public void onChanged(Slice slice) { - mSlice = slice; - showSlice(slice); - } - }; - - private View.OnClickListener mOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - final PendingIntent action = mClickActions.get(v); - if (action != null && mActivityStarter != null) { - mActivityStarter.startPendingIntentDismissingKeyguard(action); - } - } - }; - - @Inject - public KeyguardSliceViewController( - @Main Handler handler, - @Background Handler bgHandler, - KeyguardSliceView keyguardSliceView, - ActivityStarter activityStarter, - ConfigurationController configurationController, - DumpManager dumpManager, - DisplayTracker displayTracker, - KeyguardInteractor keyguardInteractor, - PowerInteractor powerInteractor) { - super(keyguardSliceView); - mHandler = handler; - mBgHandler = bgHandler; - mActivityStarter = activityStarter; - mConfigurationController = configurationController; - mDumpManager = dumpManager; - mDisplayTracker = displayTracker; - mKeyguardInteractor = keyguardInteractor; - mPowerInteractor = powerInteractor; - } - - @Override - public void onInit() { - setupUri(null); - startCoroutines(EmptyCoroutineContext.INSTANCE); - } - - void startCoroutines(CoroutineContext context) { - collectFlow(mView, mKeyguardInteractor.getDozeTimeTick(), - (Long millis) -> { - refresh(); - }, context); - - collectFlow(mView, mPowerInteractor.getScreenPowerState(), - (ScreenPowerState powerState) -> { - if (powerState == ScreenPowerState.SCREEN_TURNING_ON) { - refresh(); - } - }, context); - } - - @Override - protected void onViewAttached() { - Display display = mView.getDisplay(); - if (display != null) { - mDisplayId = display.getDisplayId(); - } - // Make sure we always have the most current slice - if (mDisplayId == mDisplayTracker.getDefaultDisplayId() && mLiveData != null) { - mLiveData.observeForever(mObserver); - } - mConfigurationController.addCallback(mConfigurationListener); - mDumpManager.registerNormalDumpable( - TAG + "@" + Integer.toHexString( - KeyguardSliceViewController.this.hashCode()), - KeyguardSliceViewController.this); - } - - @Override - protected void onViewDetached() { - // TODO(b/117344873) Remove below work around after this issue be fixed. - if (mDisplayId == mDisplayTracker.getDefaultDisplayId() && mLiveData != null) { - mLiveData.removeObserver(mObserver); - } - mConfigurationController.removeCallback(mConfigurationListener); - mDumpManager.unregisterDumpable( - TAG + "@" + Integer.toHexString( - KeyguardSliceViewController.this.hashCode())); - } - - void updateTopMargin(float clockTopTextPadding) { - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); - lp.topMargin = (int) clockTopTextPadding; - mView.setLayoutParams(lp); - } - - /** - * Sets the slice provider Uri. - */ - public void setupUri(String uriString) { - if (uriString == null) { - uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI; - } - - boolean wasObserving = false; - if (mLiveData != null && mLiveData.hasActiveObservers()) { - wasObserving = true; - mLiveData.removeObserver(mObserver); - } - - mKeyguardSliceUri = Uri.parse(uriString); - mLiveData = SliceLiveData.fromUri(mView.getContext(), mKeyguardSliceUri); - - if (wasObserving) { - mLiveData.observeForever(mObserver); - } - } - - /** - * Update contents of the view. - */ - public void refresh() { - - Trace.beginSection("KeyguardSliceViewController#refresh"); - try { - Slice slice; - if (KeyguardSliceProvider.KEYGUARD_SLICE_URI.equals(mKeyguardSliceUri.toString())) { - KeyguardSliceProvider instance = KeyguardSliceProvider.getAttachedInstance(); - if (instance != null) { - if (Flags.sliceManagerBinderCallBackground()) { - mBgHandler.post(() -> { - Slice _slice = instance.onBindSlice(mKeyguardSliceUri); - mHandler.post(() -> mObserver.onChanged(_slice)); - }); - return; - } - slice = instance.onBindSlice(mKeyguardSliceUri); - } else { - Log.w(TAG, "Keyguard slice not bound yet?"); - slice = null; - } - } else { - // TODO: Make SliceViewManager injectable - slice = SliceViewManager.getInstance(mView.getContext()).bindSlice( - mKeyguardSliceUri); - } - mObserver.onChanged(slice); - } finally { - Trace.endSection(); - } - } - - public void translate(int x, int y) { - mView.setTranslationX(x); - mView.setTranslationY(y); - } - - void showSlice(Slice slice) { - Trace.beginSection("KeyguardSliceViewController#showSlice"); - if (slice == null) { - mView.hideSlice(); - Trace.endSection(); - return; - } - - ListContent lc = new ListContent(slice); - RowContent headerContent = lc.getHeader(); - boolean hasHeader = - headerContent != null && !headerContent.getSliceItem().hasHint(HINT_LIST_ITEM); - - List subItems = lc.getRowItems().stream().filter(sliceContent -> { - String itemUri = sliceContent.getSliceItem().getSlice().getUri().toString(); - // Filter out the action row - return !KeyguardSliceProvider.KEYGUARD_ACTION_URI.equals(itemUri); - }).collect(Collectors.toList()); - - - mClickActions = mView.showSlice(hasHeader ? headerContent : null, subItems); - - Trace.endSection(); - } - - @Override - public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { - pw.println(" mSlice: " + mSlice); - pw.println(" mClickActions: " + mClickActions); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt index e193fd13c58d6..aaea20019fe73 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt @@ -16,23 +16,16 @@ package com.android.systemui.keyguard.data.repository -import android.os.UserHandle -import android.provider.Settings import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserTracker import com.android.systemui.util.settings.SecureSettings -import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn interface KeyguardSmartspaceRepository { val bcSmartspaceVisibility: StateFlow @@ -52,28 +45,16 @@ constructor( private val _bcSmartspaceVisibility: MutableStateFlow = MutableStateFlow(View.GONE) override val bcSmartspaceVisibility: StateFlow = _bcSmartspaceVisibility.asStateFlow() override val isWeatherEnabled: StateFlow = - secureSettings - .observerFlow( - names = arrayOf(Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED), - userId = UserHandle.USER_ALL, - ) - .onStart { emit(Unit) } - .map { getLockscreenWeatherEnabled() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = getLockscreenWeatherEnabled() - ) + MutableStateFlow(true) override fun setBcSmartspaceVisibility(visibility: Int) { _bcSmartspaceVisibility.value = visibility } - private fun getLockscreenWeatherEnabled(): Boolean { - return secureSettings.getIntForUser( - Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED, - 1, - userTracker.userId - ) == 1 - } + /** + * Weather data is always considered enabled because AxQuickLook handles weather + * rendering inside the active Axion clock face. The BC smartspace pipeline is + * permanently disabled — see LockscreenSmartspaceController.isEnabled. + */ + private fun getLockscreenWeatherEnabled(): Boolean = true } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 90806b3e5d643..c9bbad173f580 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -209,8 +209,6 @@ object KeyguardRootViewBinder { viewModel.alpha(viewState).collect { alpha -> view.alpha = alpha childViews[burnInLayerId]?.alpha = alpha - childViews[sliceViewId]?.alpha = alpha - childViews[weatherAreaId]?.alpha = alpha childViews[widgetArea]?.alpha = alpha } } @@ -240,8 +238,6 @@ object KeyguardRootViewBinder { // need to add translation to it here same as translationX viewModel.translationY.collect { y -> childViews[burnInLayerId]?.translationY = y - childViews[sliceViewId]?.translationY = y - childViews[weatherAreaId]?.translationY = y childViews[widgetArea]?.translationY = y childViews[largeClockId]?.translationY = y if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { @@ -259,8 +255,6 @@ object KeyguardRootViewBinder { state.isToOrFrom(KeyguardState.AOD) -> { // Large Clock is not translated in the x direction childViews[burnInLayerId]?.translationX = px - childViews[sliceViewId]?.translationX = px - childViews[weatherAreaId]?.translationX = px childViews[widgetArea]?.translationX = px childViews[aodPromotedNotificationId]?.translationX = px childViews[aodNotificationIconContainerId]?.translationX = px @@ -310,8 +304,6 @@ object KeyguardRootViewBinder { launch { viewModel.burnInLayerVisibility.collect { visibility -> childViews[burnInLayerId]?.visibility = visibility - childViews[sliceViewId]?.visibility = visibility - childViews[weatherAreaId]?.visibility = visibility childViews[widgetArea]?.visibility = visibility } } @@ -604,8 +596,6 @@ object KeyguardRootViewBinder { } private val burnInLayerId = R.id.burn_in_layer - private val sliceViewId = R.id.keyguard_slice_view - private val weatherAreaId = R.id.keyguard_weather_area private val aodPromotedNotificationId = AodPromotedNotificationSection.viewId private val aodNotificationIconContainerId = R.id.aod_notification_icon_container private val largeClockId = ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSliceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSliceViewBinder.kt deleted file mode 100644 index b420373099acf..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSliceViewBinder.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.binder - -import android.annotation.SuppressLint -import android.view.ViewGroup -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.keyguard.KeyguardSliceViewController -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.DisposableHandle - -/** Binding for KeyguardSliceView */ -object KeyguardSliceViewBinder { - @SuppressLint("ClickableViewAccessibility") - @JvmStatic - fun bind( - view: ViewGroup, - keyguardInteractor: KeyguardInteractor, - keyguardSliceViewController: KeyguardSliceViewController, - aodBurnInViewModel: AodBurnInViewModel, - ): DisposableHandle { - return view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - keyguardInteractor.dozeTimeTick.collect { - keyguardSliceViewController.refresh() - } - } - - launch { - aodBurnInViewModel.movement.collect { burnInModel -> - keyguardSliceViewController.translate( - burnInModel.translationX, - burnInModel.translationY, - ) - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt index 4c0d3708fd3dd..a0209fa3202c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt @@ -35,8 +35,6 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguardChipSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import java.util.Optional @@ -69,8 +67,6 @@ constructor( aodBurnInSection: AodBurnInSection, clockSection: ClockSection, smartspaceSection: SmartspaceSection, - keyguardWeatherViewSection: KeyguardWeatherViewSection, - keyguardSliceViewSection: KeyguardSliceViewSection, keyguardWidgetViewSection: KeyguardWidgetViewSection, axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, @@ -93,8 +89,6 @@ constructor( smartspaceSection, aodBurnInSection, clockSection, - keyguardWeatherViewSection, - keyguardSliceViewSection, keyguardWidgetViewSection, defaultDeviceEntrySection, udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index ca46ee5ce54ab..392c534184e3a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -33,8 +33,6 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguardChipSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines @@ -73,8 +71,6 @@ constructor( keyguardWidgetViewSection: KeyguardWidgetViewSection, mediaSection: SplitShadeMediaSection, smartspaceSection: SmartspaceSection, - keyguardWeatherViewSection: KeyguardWeatherViewSection, - keyguardSliceViewSection: KeyguardSliceViewSection, axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, ) : KeyguardBlueprint { override val id: String = ID @@ -97,8 +93,6 @@ constructor( aodBurnInSection, clockSection, smartspaceSection, - keyguardWeatherViewSection, - keyguardSliceViewSection, keyguardWidgetViewSection, mediaSection, defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt deleted file mode 100644 index 4362032e13536..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.keyguard.ui.view.layout.sections - -import android.content.Context -import android.os.Handler -import android.view.LayoutInflater -import androidx.constraintlayout.widget.Barrier -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import com.android.keyguard.KeyguardSliceView -import com.android.keyguard.KeyguardSliceViewController -import com.android.systemui.customization.clocks.R as clocksR -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.binder.KeyguardSliceViewBinder -import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.keyguard.ui.clocks.ClockViewIds -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.res.R -import com.android.systemui.settings.DisplayTracker -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController -import com.android.systemui.statusbar.policy.ConfigurationController -import javax.inject.Inject -import kotlinx.coroutines.DisposableHandle - -class KeyguardSliceViewSection -@Inject -constructor( - private val context: Context, - val smartspaceController: LockscreenSmartspaceController, - val layoutInflater: LayoutInflater, - @Main val handler: Handler, - @Background val bgHandler: Handler, - val activityStarter: ActivityStarter, - val configurationController: ConfigurationController, - val dumpManager: DumpManager, - val displayTracker: DisplayTracker, - val keyguardInteractor: KeyguardInteractor, - val powerInteractor: PowerInteractor, - val aodBurnInViewModel: AodBurnInViewModel, -) : KeyguardSection() { - private lateinit var sliceView: KeyguardSliceView - private var disposableHandle: DisposableHandle? = null - - override fun addViews(constraintLayout: ConstraintLayout) { - if (smartspaceController.isEnabled) return - sliceView = - layoutInflater.inflate(R.layout.keyguard_slice_view, null, false) as KeyguardSliceView - constraintLayout.addView(sliceView) - } - - override fun bindData(constraintLayout: ConstraintLayout) { - if (smartspaceController.isEnabled) return - val controller = - KeyguardSliceViewController( - handler, - bgHandler, - sliceView, - activityStarter, - configurationController, - dumpManager, - displayTracker, - keyguardInteractor, - powerInteractor, - ) - controller.setupUri(null) - controller.init() - - disposableHandle?.dispose() - disposableHandle = - KeyguardSliceViewBinder.bind( - sliceView, - keyguardInteractor, - controller, - aodBurnInViewModel, - ) - } - - override fun applyConstraints(constraintSet: ConstraintSet) { - if (smartspaceController.isEnabled) return - constraintSet.apply { - connect( - R.id.keyguard_slice_view, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize( - clocksR.dimen.status_view_margin_horizontal - ), - ) - connect( - R.id.keyguard_slice_view, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END, - ) - constrainHeight(R.id.keyguard_slice_view, ConstraintSet.WRAP_CONTENT) - - connect( - R.id.keyguard_slice_view, - ConstraintSet.TOP, - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - ConstraintSet.BOTTOM, - ) - - if (!smartspaceController.isOmniWeatherEnabled) { - createBarrier( - R.id.smart_space_barrier_bottom, - Barrier.BOTTOM, - 0, - *intArrayOf(R.id.keyguard_slice_view), - ) - } else { - createBarrier( - R.id.keyguard_weather_area, - Barrier.BOTTOM, - 0, - *intArrayOf(R.id.keyguard_slice_view), - ) - } - } - } - - override fun removeViews(constraintLayout: ConstraintLayout) { - if (smartspaceController.isEnabled) return - - constraintLayout.removeView(R.id.keyguard_slice_view) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt deleted file mode 100644 index 453fcf3aeca75..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2024-2026 crDroid Android Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.keyguard.ui.view.layout.sections - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.constraintlayout.widget.Barrier -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.customization.clocks.R as clocksR -import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController -import javax.inject.Inject - -import com.android.systemui.weather.WeatherInfoView - -class KeyguardWeatherViewSection -@Inject -constructor( - private val context: Context, - val layoutInflater: LayoutInflater, - val smartspaceController: LockscreenSmartspaceController, -) : KeyguardSection() { - private lateinit var weatherView: WeatherInfoView - - override fun addViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - weatherView = - layoutInflater.inflate(R.layout.keyguard_weather_area, null, false) as WeatherInfoView - constraintLayout.addView(weatherView) - } - - override fun bindData(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - weatherView.init() - } - - override fun applyConstraints(constraintSet: ConstraintSet) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - constraintSet.apply { - connect( - R.id.keyguard_weather_area, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize(clocksR.dimen.status_view_margin_horizontal), - ) - connect( - R.id.keyguard_weather_area, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END - ) - constrainHeight(R.id.keyguard_weather_area, ConstraintSet.WRAP_CONTENT) - - connect( - R.id.keyguard_weather_area, - ConstraintSet.TOP, - R.id.keyguard_slice_view, - ConstraintSet.BOTTOM - ) - - // UNIFIED BARRIER - Include ALL status area elements - createUnifiedBarrierAndNotificationConstraints(constraintSet) - } - } - - private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { - constraintSet.apply { - // UNIFIED BARRIER - Include ALL status area elements - createBarrier( - R.id.smart_space_barrier_bottom, - Barrier.BOTTOM, - 0, - *intArrayOf( - R.id.keyguard_slice_view, - R.id.keyguard_weather_area, - R.id.clock_ls, - sharedR.id.bc_smartspace_view, - sharedR.id.date_smartspace_view, - ) - ) - } - } - - override fun removeViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - constraintLayout.findViewById(R.id.keyguard_weather_area)?.let { weatherArea -> - weatherArea.cleanup() - constraintLayout.removeView(weatherArea) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt deleted file mode 100644 index 96cc77ceecb5b..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2023-2024 risingOS Android Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.weather - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.view.View.MeasureSpec -import android.widget.ImageView -import android.widget.TextView -import com.android.systemui.res.R - -class WeatherImageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 -) : ImageView(context, attrs, defStyle) { - - private val maxSizePx: Int = - context.resources.getDimension(R.dimen.weather_image_max_size).toInt() - - private val weatherViewController: WeatherViewController - - init { - visibility = View.GONE - - val stubText = TextView(context) - - weatherViewController = WeatherViewController( - context = context, - weatherIcon = this, - weatherTemp = stubText, - weatherInfoView = this, - ) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - weatherViewController.init() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - weatherViewController.removeObserver() - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val width = maxSizePx.coerceAtMost(MeasureSpec.getSize(widthMeasureSpec)) - val height = maxSizePx.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec)) - setMeasuredDimension(width, height) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherInfoView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherInfoView.kt deleted file mode 100644 index 531c2e486ad15..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherInfoView.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2025 the AxionAOSP Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.weather - -import android.content.Context -import android.util.AttributeSet -import androidx.constraintlayout.widget.ConstraintLayout -import android.widget.ImageView -import android.widget.TextView -import com.android.systemui.res.R - -class WeatherInfoView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 -) : ConstraintLayout(context, attrs, defStyle) { - - private lateinit var weatherIcon: ImageView - private lateinit var weatherTemp: TextView - private lateinit var controller: WeatherViewController - private var initialized = false - - fun init() { - if (initialized) return - initialized = true - - weatherIcon = findViewById(R.id.weather_icon) - weatherTemp = findViewById(R.id.weather_temp) - - controller = WeatherViewController(context, weatherIcon, weatherTemp, this) - controller.init() - } - - fun cleanup() { - if (::controller.isInitialized) controller.removeObserver() - initialized = false - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - if (!initialized) init() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - cleanup() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt deleted file mode 100644 index 21d43d0e8aa5f..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2023-2024 risingOS Android Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.weather - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.TextView -import com.android.systemui.res.R - -class WeatherTextView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 -) : TextView(context, attrs, defStyle) { - - private val mWeatherViewController: WeatherViewController - - init { - visibility = View.GONE - - val stubIcon = android.widget.ImageView(context) - - mWeatherViewController = WeatherViewController( - context = context, - weatherIcon = stubIcon, - weatherTemp = this, - weatherInfoView = this, - ) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - mWeatherViewController.init() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - mWeatherViewController.removeObserver() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt deleted file mode 100644 index fcb2e1ae84de4..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * SPDX-FileCopyrightText: The AxionAOSP Project - * SPDX-FileCopyrightText: crDroid Android Project - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.android.systemui.weather - -import android.content.Context -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler -import android.os.Looper -import android.os.UserHandle -import android.provider.Settings -import android.view.View -import android.widget.ImageView -import android.widget.TextView - -import com.android.internal.util.alpha.OmniJawsClient - -import com.android.systemui.res.R - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest - -class WeatherViewController( - private val context: Context, - private val weatherIcon: ImageView, - private val weatherTemp: TextView, - private val weatherInfoView: View, -) : OmniJawsClient.OmniJawsObserver { - - private var weatherInfo: OmniJawsClient.WeatherInfo? = null - private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) - - private val weatherSettingsFlow = MutableStateFlow(getWeatherSettings()) - - private val settingsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - weatherSettingsFlow.value = getWeatherSettings() - } - } - - fun init() { - registerSettingsObservers() - scope.launch { - weatherSettingsFlow.collectLatest { applyWeatherSettings(it) } - } - } - - private fun registerSettingsObservers() { - val resolver = context.contentResolver - WEATHER_SETTING_KEYS.forEach { key -> - resolver.registerContentObserver( - Settings.System.getUriFor(key), - false, - settingsObserver, - UserHandle.USER_ALL - ) - } - } - - private fun unregisterSettingsObservers() { - context.contentResolver.unregisterContentObserver(settingsObserver) - } - - private fun getConditionText(condition: String): String { - val locale = context.resources.configuration.locales[0] - val isEnglish = locale.language.startsWith("en", ignoreCase = true) - - if (!isEnglish) { - for ((key, value) in WEATHER_CONDITIONS) { - if (condition.contains(key)) { - return context.resources.getString(value) - } - } - } - return condition.split(" ").joinToString(" ") { it.replaceFirstChar { char -> char.uppercaseChar() } } - } - - private fun getWeatherSettings() = WeatherSettings( - weatherEnabled = getSystemSetting(LOCKSCREEN_WEATHER_ENABLED), - showWeatherLocation = getSystemSetting(LOCKSCREEN_WEATHER_LOCATION), - showWeatherText = getSystemSetting(LOCKSCREEN_WEATHER_TEXT, defaultValue = 1), - showWindInfo = getSystemSetting(LOCKSCREEN_WEATHER_WIND_INFO), - showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO) - ) - - private fun getSystemSetting(setting: String, defaultValue: Int = 0) = - Settings.System.getIntForUser(context.contentResolver, setting, defaultValue, UserHandle.USER_CURRENT) != 0 - - private fun applyWeatherSettings(settings: WeatherSettings) { - if (!settings.weatherEnabled) { - hideAllViews() - OmniJawsClient.get().removeObserver(context, this@WeatherViewController) - } else { - OmniJawsClient.get().addObserver(context, this@WeatherViewController) - updateWeather() - showAllViews() - } - } - - override fun weatherUpdated() = updateWeather() - - private fun updateWeather() { - if (!weatherSettingsFlow.value.weatherEnabled) { - hideAllViews() - return - } - - try { - OmniJawsClient.get().queryWeather(context) - weatherInfo = OmniJawsClient.get().weatherInfo - weatherInfo?.let { info -> - weatherIcon.setImageDrawable( - OmniJawsClient.get().getWeatherConditionImage(context, - info.conditionCode)) - weatherTemp.text = buildWeatherText(info) - weatherTemp.isSelected = true - } - } catch (e: Exception) {} - } - - private fun hideAllViews() { - scope.launch { - listOf(weatherInfoView, weatherIcon, weatherTemp).forEach { - updateViewVisibility(it, false) - } - } - } - - private fun showAllViews() { - scope.launch { - listOf(weatherInfoView, weatherIcon, weatherTemp).forEach { - updateViewVisibility(it, true) - } - } - } - - private fun buildWeatherText(info: OmniJawsClient.WeatherInfo): String { - val settings = weatherSettingsFlow.value - val conditionText = getConditionText(info.condition.lowercase()) - - val locationText = if (settings.showWeatherLocation) " • ${info.city}" else "" - val conditionDisplay = if (settings.showWeatherText) " • $conditionText" else "" - val windDisplay = if (settings.showWindInfo) " • ${info.windSpeed} ${info.windUnits} ${info.pinWheel}" else "" - val humidityDisplay = if (settings.showHumidityInfo) " • ${info.humidity}" else "" - - return "${info.temp}${info.tempUnits}$locationText$conditionDisplay$windDisplay$humidityDisplay" - } - - override fun weatherError(errorReason: Int) { - if (errorReason == OmniJawsClient.EXTRA_ERROR_DISABLED) { - weatherInfo = null - weatherIcon.setImageDrawable(null) - weatherTemp.text = "" - hideAllViews() - } - } - - fun removeObserver() { - unregisterSettingsObservers() - scope.cancel() - OmniJawsClient.get().removeObserver(context, this) - } - - private suspend fun updateViewVisibility(view: View, visible: Boolean) { - withContext(Dispatchers.Main) { - view.visibility = if (visible) View.VISIBLE else View.GONE - } - } - - data class WeatherSettings( - val weatherEnabled: Boolean, - val showWeatherLocation: Boolean, - val showWeatherText: Boolean, - val showWindInfo: Boolean, - val showHumidityInfo: Boolean - ) - - companion object { - private const val LOCKSCREEN_WEATHER_ENABLED = "lockscreen_weather_enabled" - private const val LOCKSCREEN_WEATHER_LOCATION = "lockscreen_weather_location" - private const val LOCKSCREEN_WEATHER_TEXT = "lockscreen_weather_text" - private const val LOCKSCREEN_WEATHER_WIND_INFO = "lockscreen_weather_wind_info" - private const val LOCKSCREEN_WEATHER_HUMIDITY_INFO = "lockscreen_weather_humidity_info" - - private val WEATHER_SETTING_KEYS = listOf( - LOCKSCREEN_WEATHER_ENABLED, - LOCKSCREEN_WEATHER_LOCATION, - LOCKSCREEN_WEATHER_TEXT, - LOCKSCREEN_WEATHER_WIND_INFO, - LOCKSCREEN_WEATHER_HUMIDITY_INFO - ) - - private val WEATHER_CONDITIONS = mapOf( - "clouds" to R.string.weather_condition_clouds, - "rain" to R.string.weather_condition_rain, - "clear" to R.string.weather_condition_clear, - "storm" to R.string.weather_condition_storm, - "snow" to R.string.weather_condition_snow, - "wind" to R.string.weather_condition_wind, - "mist" to R.string.weather_condition_mist - ) - } -} From ee5b2464b71bfaa9e72799ce854d3c908bf76d73 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:05:41 +0100 Subject: [PATCH 11/51] SystemUI: Update Axion clock engine and Smartspace --- .../customization/res/values/strings.xml | 1 - .../shared/clocks/AxClockFaceController.kt | 6 +- .../systemui/shared/clocks/AxClockProvider.kt | 6 - .../systemui/shared/clocks/WeatherUtils.kt | 2 +- .../shared/clocks/view/AxClockInteractor.kt | 6 +- .../shared/clocks/view/CyberpunkClockView.kt | 4 +- .../android/keyguard/ClockEventController.kt | 2 +- .../keyguard/dagger/ClockRegistryModule.java | 13 +- .../AxDynamicBarKeyguardChipSection.kt | 4 +- .../ui/view/layout/sections/ClockSection.kt | 143 +++--------------- .../transitions/ClockSizeTransition.kt | 1 - .../LockscreenSmartspaceController.kt | 39 ++--- .../smartspace/BcSmartspaceDataProvider.java | 1 + .../smartspace/CardRecyclerViewAdapter.java | 7 - 14 files changed, 63 insertions(+), 172 deletions(-) diff --git a/packages/SystemUI/customization/res/values/strings.xml b/packages/SystemUI/customization/res/values/strings.xml index 4a8419af3c7e0..55c514fb1ef09 100644 --- a/packages/SystemUI/customization/res/values/strings.xml +++ b/packages/SystemUI/customization/res/values/strings.xml @@ -29,7 +29,6 @@ NDOT DEFAULT OLD_QUICKLOOK - QUICKLOOK POLYLINE SPACE_AGE CYBERPUNK diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt index 026a3d76ddf09..4c803caf8ab42 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt @@ -35,8 +35,12 @@ class AxClockFaceController( override var animations: ClockAnimations = AxClockAnimations(view, 0.0f, 0.0f) internal set + // hasCustomWeatherDataDisplay = true for both small + large variants: every Axion face + // renders its own date/weather row via EnhancedDateArea (or an equivalent inline path), + // so the framework should hide BC date_smartspace_view / weather_smartspace_view whenever + // an Axion face is active, regardless of clock size. See §12 of axion-integration-plan.md. override val config: ClockFaceConfig = - ClockFaceConfig(clockTickRate, false, false, false) + ClockFaceConfig(clockTickRate, true, false, false) override var theme = ThemeConfig(true, null) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt index 29fc78501a30a..383f8bd6d0c65 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt @@ -16,20 +16,14 @@ package com.android.systemui.shared.clocks import android.content.Context import android.content.res.Resources -import android.graphics.drawable.Drawable -import android.os.Vibrator import android.util.Log import android.view.LayoutInflater import com.android.systemui.customization.R import com.android.systemui.plugins.keyguard.ui.clocks.* -const val useAxClocks = true - class AxClockProvider( private val layoutInflater: LayoutInflater, private val resources: Resources, - private val isClockReactiveVariantsEnabled: Boolean = false, - private val vibrator: Vibrator?, ) : ClockProvider { private val tag = "AxClockProvider" diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt index 8a9827a1d8734..5419376cfcd04 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt @@ -19,7 +19,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.drawable.Drawable -import com.android.internal.util.android.OmniJawsClient +import com.android.internal.util.alpha.OmniJawsClient import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData object WeatherUtils { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt index 61c2d6f847f3a..04055fc654c46 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt @@ -97,7 +97,11 @@ class AxClockInteractor( fun refreshDate() { if (cachedDateFormat == null || cachedDateLocale != locale) { - cachedDateFormat = SimpleDateFormat("EEE, dd MMM", locale) + // Respect the locale's preferred day/month order (en_US: "Thu, May 14", + // en_GB/pt_PT: "Thu, 14 May") instead of hardcoding day-first. Matches the + // skeleton used by BC IcuDateTextView (cr_strings smartspace_icu_date_pattern). + val pattern = DateFormat.getBestDateTimePattern(locale, "EEEMMMd") + cachedDateFormat = SimpleDateFormat(pattern, locale) cachedDateLocale = locale } state.dateStrFlow.value = cachedDateFormat!!.format(calendar.time) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt index 22b60203788ba..8aa944dbfe5f5 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt @@ -258,8 +258,10 @@ class CyberpunkClockView @JvmOverloads constructor( ) Spacer(modifier = Modifier.width(8.dp)) + // Decorative label only; the real date is rendered once by + // EnhancedDateArea below to avoid duplicating it with BC smartspace. Text( - text = dateStr.uppercase().ifEmpty { "2077" }, + text = "2077", modifier = Modifier.padding(horizontal = 4.dp), style = TextStyle( fontSize = 16.sp, diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 40921c109314b..cb0694be6082d 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -547,7 +547,7 @@ constructor( depthBlockedByBouncer = showing updateDepthVisibility() } - override fun onDozingChanged() { + override fun onDozingChanged(dozing: Boolean) { updateDepthVisibility() } override fun onKeyguardShowingChanged(showing: Boolean) { diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index 3e76304980450..64c7084505f00 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -18,11 +18,8 @@ import android.content.Context; import android.content.res.Resources; -import android.os.Vibrator; import android.view.LayoutInflater; -import androidx.annotation.Nullable; - import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Background; @@ -58,8 +55,7 @@ public static ClockRegistry getClockRegistry( FeatureFlags featureFlags, @Main Resources resources, LayoutInflater layoutInflater, - ClockMessageBuffers clockBuffers, - @Nullable Vibrator vibrator) { + ClockMessageBuffers clockBuffers) { ClockRegistry registry = new ClockRegistry( context, pluginManager, @@ -69,12 +65,7 @@ public static ClockRegistry getClockRegistry( com.android.systemui.shared.Flags.lockscreenCustomClocks() || featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), /* handleAllUsers= */ true, - new AxClockProvider( - layoutInflater, - resources, - com.android.systemui.shared.Flags.clockReactiveVariants(), - vibrator - ), + new AxClockProvider(layoutInflater, resources), context.getString(R.string.lockscreen_clock_id_fallback), clockBuffers, /* keepAllLoaded = */ false, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index 8a6bd9e064f84..f7a92a1d2c30d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -37,9 +37,7 @@ private val HIDDEN_VIEW_IDS = listOf( R.id.bc_smartspace_view, R.id.smartspace_card_pager, R.id.smartspace_page_indicator, - R.id.keyguard_slice_view, - R.id.keyguard_weather_area, - R.id.clock_ls, + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, ) private fun Float.dpToPx(context: Context): Int = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index cc5f7081c2b53..a30607715fc1f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View -import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -32,7 +31,6 @@ import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.customization.R as custR -import com.android.systemui.customization.clocks.R as clocksR import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor @@ -49,7 +47,6 @@ import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shared.R as sharedR -import com.android.systemui.shared.clocks.useAxClocks import com.android.systemui.util.ui.value import dagger.Lazy import javax.inject.Inject @@ -112,7 +109,6 @@ constructor( clock: ClockController, constraintSet: ConstraintSet, ): ConstraintSet { - val isLargeVisible = keyguardClockViewModel.isLargeClockVisible.value val targetFace = getTargetClockFace(clock) val nonTargetFace = getNonTargetClockFace(clock) val targetViews = targetFace.views @@ -129,26 +125,18 @@ constructor( setAlpha(targetViews, 1F) setAlpha(nonTargetViews, 0F) - if (!keyguardClockViewModel.isLargeClockVisible.value) { - if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value && !useAxClocks) { - connect( - sharedR.id.bc_smartspace_view, - TOP, - sharedR.id.date_smartspace_view, - BOTTOM, - ) - } - } else { - if (aodBurnInViewModel.movement.value.scaleClockOnly) { - setScaleX( - getTargetClockFace(clock).views, - aodBurnInViewModel.movement.value.scale, - ) - setScaleY( - getTargetClockFace(clock).views, - aodBurnInViewModel.movement.value.scale, - ) - } + if ( + keyguardClockViewModel.isLargeClockVisible.value && + aodBurnInViewModel.movement.value.scaleClockOnly + ) { + setScaleX( + getTargetClockFace(clock).views, + aodBurnInViewModel.movement.value.scale, + ) + setScaleY( + getTargetClockFace(clock).views, + aodBurnInViewModel.movement.value.scale, + ) } } } @@ -161,85 +149,20 @@ constructor( if (keyguardClockViewModel.isLargeClockVisible.value) clock.smallClock.layout else clock.largeClock.layout - private fun constrainWeatherClockDateIconsBarrier(constraints: ConstraintSet) { - constraints.apply { - createBarrier( - R.id.weather_clock_bc_smartspace_bottom, - Barrier.BOTTOM, - context.resources.getDimensionPixelSize(clocksR.dimen.enhanced_smartspace_height), - (ClockViewIds.WEATHER_CLOCK_TIME), - ) - if ( - rootViewModel.isNotifIconContainerVisible.value.value && - keyguardClockViewModel.hasAodIcons.value - ) { - createBarrier( - ClockViewIds.WEATHER_CLOCK_DATE_BARRIER_BOTTOM, - Barrier.BOTTOM, - 0, - *intArrayOf( - R.id.aod_notification_icon_container, - R.id.weather_clock_bc_smartspace_bottom, - ), - ) - } else { - createBarrier( - ClockViewIds.WEATHER_CLOCK_DATE_BARRIER_BOTTOM, - Barrier.BOTTOM, - 0, - *intArrayOf(R.id.weather_clock_bc_smartspace_bottom), - ) - } - } - } - fun applyDefaultConstraints(constraints: ConstraintSet) { - val isLargeVisible = keyguardClockViewModel.isLargeClockVisible.value val guideline = if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID else R.id.split_shade_guideline constraints.apply { connect(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, START, PARENT_ID, START) connect(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, END, guideline, END) - if ( - com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout() && - !com.android.systemui.shared.Flags.clockReactiveVariants() - && !useAxClocks - ) { - connect( - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, - BOTTOM, - R.id.device_entry_icon_view, - TOP, - context.resources.getDimensionPixelSize( - clocksR.dimen.date_weather_view_height - ) * 2, - ) - } else { - connect( - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, - BOTTOM, - R.id.device_entry_icon_view, - TOP, - ) - } - val largeClockTopMargin = - if (useAxClocks) { - keyguardClockViewModel.getLargeClockTopMargin() - } else if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - keyguardClockViewModel.getLargeClockTopMargin() + - context.resources.getDimensionPixelSize( - clocksR.dimen.enhanced_smartspace_height - ) - } else { - keyguardClockViewModel.getLargeClockTopMargin() + - context.resources.getDimensionPixelSize( - clocksR.dimen.date_weather_view_height - ) + - context.resources.getDimensionPixelSize( - clocksR.dimen.enhanced_smartspace_height - ) - } + connect( + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, + BOTTOM, + R.id.device_entry_icon_view, + TOP, + ) + val largeClockTopMargin = keyguardClockViewModel.getLargeClockTopMargin() connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, TOP, @@ -285,10 +208,6 @@ constructor( // Explicitly clear pivot to force recalculate pivot instead of using legacy value setTransformPivot(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, Float.NaN, Float.NaN) - val smallClockBottom = - keyguardClockViewModel.getSmallClockTopMargin() + - context.resources.getDimensionPixelSize(clocksR.dimen.small_clock_height) - - context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) val marginBetweenSmartspaceAndNotification = context.resources.getDimensionPixelSize( R.dimen.keyguard_status_view_bottom_margin @@ -299,25 +218,11 @@ constructor( 0 } - if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value && !useAxClocks) { - val dateWeatherSmartspaceHeight = - context.resources - .getDimensionPixelSize(clocksR.dimen.date_weather_view_height) - .toFloat() - clockInteractor.setNotificationStackDefaultTop( - smallClockBottom + - dateWeatherSmartspaceHeight + - marginBetweenSmartspaceAndNotification - ) - } else { - clockInteractor.setNotificationStackDefaultTop( - (keyguardClockViewModel.getSmallClockTopMargin() + - marginBetweenSmartspaceAndNotification) - .toFloat() - ) - } + clockInteractor.setNotificationStackDefaultTop( + (keyguardClockViewModel.getSmallClockTopMargin() + + marginBetweenSmartspaceAndNotification) + .toFloat() + ) } - - if (!useAxClocks) constrainWeatherClockDateIconsBarrier(constraints) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 9d7347aca43a6..0a28968131694 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -84,7 +84,6 @@ class ClockSizeTransition( val parent = view.parent as View val targetSSView = parent.findViewById(sharedR.id.bc_smartspace_view) - ?: parent.findViewById(R.id.keyguard_slice_view) if (targetSSView == null) { logger.e({ "Failed to find smartspace equivalent target under $str1" }) { str1 = "$parent" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 83e4601829a75..22a86bd29f594 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -34,8 +34,6 @@ import android.os.UserHandle import android.provider.Settings import android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS import android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS -import android.provider.Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED -import android.provider.Settings.System.LOCKSCREEN_WEATHER_ENABLED import android.util.Log import android.view.ContextThemeWrapper import android.view.View @@ -307,26 +305,29 @@ constructor( get() = datePlugin != null && weatherPlugin != null val isWeatherEnabled: Boolean - get() { - val showWeather = - secureSettings.getIntForUser(LOCKSCREEN_SMARTSPACE_ENABLED, 1, userTracker.userId) == - 1 - return showWeather - } + get() = false val isOmniWeatherEnabled: Boolean - get() { - val showCustomWeather = - systemSettings.getIntForUser( - LOCKSCREEN_WEATHER_ENABLED, - 0, - userTracker.userId, - ) == 1 - return showCustomWeather && !isWeatherEnabled && !isCustomClockEnabled - } - + get() = false + + /** + * BC smartspace is permanently disabled. Axion clocks render their own date/weather/ + * alarm/now-playing via AxQuickLook — the BC pipeline (date_smartspace_view, + * weather_smartspace_view, bc_smartspace_view) would always duplicate that content. + * See axion-integration-plan.md §12. + */ val isEnabled: Boolean - get() = plugin != null && isWeatherEnabled + get() = false + + /** + * Whether some path is responsible for surfacing smartspace-style content (next alarm, + * now-playing, calendar, weather) on the lockscreen — either the BC plugin (source = + * GOOGLE) or the Axion clock + AxQuickLook bridge (source = AXION). Used by view + * sections (e.g. the legacy keyguard slice) that should not draw redundant content + * while either of those paths is active. + */ + val isSmartspaceContentActive: Boolean + get() = true private fun updateBypassEnabled() { val bypassEnabled = bypassController.bypassEnabled diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java index 41cefca4a59d7..919bbc227f2d0 100644 --- a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java @@ -90,6 +90,7 @@ public void onTargetsAvailable(List list) { mSmartspaceTargets = list.stream() .filter(target -> target.getFeatureType() != 15) + .filter(target -> target.getFeatureType() != 25) .collect(Collectors.toList()); mSmartspaceTargetListeners.forEach( diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java index 5fe31341541d2..4e7c286bbef0a 100644 --- a/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java @@ -731,22 +731,15 @@ public final void updateTargetVisibility(Runnable runnable, boolean z) { smartspaceTargets.stream().collect(Collectors.toList()), runnable); } hasAodLockscreenTransition = targets != lockscreenTargets; - if (configProvider.isDefaultDateWeatherDisabled() - || BcSmartspaceDataPlugin.UI_SURFACE_HOME_SCREEN.equals(uiSurface)) {} BcSmartspaceTemplateDataUtils.updateVisibility( root, smartspaceTargets.isEmpty() ? View.GONE : View.VISIBLE); return; } } - z2 = false; - if (smartspaceTargets == lockscreenTargets) {} - if (!z2) {} - if (!z) {} viewHolders.clear(); resetListIfNeeded(); mDiffer.submitList(smartspaceTargets.stream().collect(Collectors.toList()), runnable); hasAodLockscreenTransition = targets != lockscreenTargets; - if (configProvider.isDefaultDateWeatherDisabled()) {} } public final void setTargets(List list, Runnable runnable) { From 534662539449c254ba53ddc9012a1d15000fde46 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:05:41 +0100 Subject: [PATCH 12/51] SystemUI: Sync Monet fidelity and chroma engine --- .../com/android/systemui/theme/ThemeOverlayController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 07cb8fe4b0f75..e91dc419d85e9 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -710,7 +710,9 @@ private void fetchCustomThemeSettings() { JSONObject object = new JSONObject(overlayPackageJson); mContrast = object.optDouble("_contrast_level", 0.0); mChromaBoost = object.optDouble("_chroma_boost", 0.0); - mIsFidelityEnabled = object.optBoolean("_fidelity_enabled", true); + mIsFidelityEnabled = object.has(OVERLAY_FIDELITY) + ? object.optInt(OVERLAY_FIDELITY, 0) == 1 + : object.optBoolean("_fidelity_enabled", false); if (DEBUG) { Log.d(TAG, "Custom theme settings: contrast=" + mContrast + " chromaBoost=" + mChromaBoost From 0357d3a6db6627e3cd562311dfcbb959a323d9b0 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:05:41 +0100 Subject: [PATCH 13/51] services: Update QuickSwitchService to new AlphaThemePicker namespace --- .../server/alpha/QuickSwitchService.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/services/core/java/com/android/server/alpha/QuickSwitchService.java b/services/core/java/com/android/server/alpha/QuickSwitchService.java index 27f4d9ff094e2..4d17f66fc8be2 100644 --- a/services/core/java/com/android/server/alpha/QuickSwitchService.java +++ b/services/core/java/com/android/server/alpha/QuickSwitchService.java @@ -55,22 +55,21 @@ public final class QuickSwitchService extends SystemService { private static final String LAWNCHAIR_OVERLAY = "app.lawnchair.overlay"; private static final List WALLPAPER_PICKER_PACKAGES = List.of( - "com.android.customization.themes", - "com.android.wallpaper" + "com.android.alpha.themepicker" ); private static final List WALLPAPER_PICKER_GOOGLE_PACKAGES = List.of( - "com.google.android.aicore", - "com.google.android.apps.aiwallpapers", + // "com.google.android.aicore", + // "com.google.android.apps.aiwallpapers", "com.google.android.apps.customization", "com.google.android.apps.customization.pixel", - "com.google.android.apps.emojiwallpaper", - "com.google.android.apps.wallpaper", + // "com.google.android.apps.emojiwallpaper", + // "com.google.android.apps.wallpaper", "com.google.android.apps.wallpaper.overlay.android", - "com.google.android.apps.wallpaper.overlay.launcher", - "com.google.android.apps.wallpaper.pixel", - "com.google.android.wallpaper.effects", - "com.google.pixel.livewallpaper" + "com.google.android.apps.wallpaper.overlay.launcher" + // "com.google.android.apps.wallpaper.pixel", + // "com.google.android.wallpaper.effects", + // "com.google.pixel.livewallpaper" ); private static final List LAUNCHER3_OVERLAYS = List.of( From a898cbdd927ca5213beda91679f4dcbdc03d129e Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sun, 17 May 2026 23:05:41 +0100 Subject: [PATCH 14/51] SystemUI: Notification stack and icon container tweaks --- .../stack/NotificationStackScrollLayout.java | 3 +- .../phone/NotificationIconContainer.java | 34 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 8bd771de901a2..46386b00c1e42 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -89,7 +89,6 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.SystemBarUtils; import com.android.keyguard.BouncerPanelExpansionCalculator; -import com.android.keyguard.KeyguardSliceView; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.ExpandHelper; @@ -3904,7 +3903,7 @@ private void generateTopPaddingEvent() { if (mAmbientState.isDozing()) { event = new AnimationEvent(null /* view */, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED, - KeyguardSliceView.DEFAULT_ANIM_DURATION); + 550); } else { event = new AnimationEvent(null /* view */, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index badf63521bc3b..0d9dd0753c47d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -268,6 +268,18 @@ protected void onAttachedToWindow() { super.onAttachedToWindow(); mSettingsObserver = new SettingsObserver(new Handler()); mSettingsObserver.register(); + ClockSettingsRepository.init(getContext()); + getContext().getContentResolver().registerContentObserver( + ClockSettingsRepository.clockFaceUri, + false, + mClockSettingsObserver + ); + getContext().getContentResolver().registerContentObserver( + ClockSettingsRepository.alignmentUri, + false, + mClockSettingsObserver + ); + updateShouldCenterIcons(); } @Override @@ -276,6 +288,7 @@ protected void onDetachedFromWindow() { if (mSettingsObserver != null) { mSettingsObserver.unregister(); } + getContext().getContentResolver().unregisterContentObserver(mClockSettingsObserver); } @Override @@ -971,27 +984,6 @@ public void initFrom(View view) { } } } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - ClockSettingsRepository.init(getContext()); - getContext().getContentResolver().registerContentObserver( - ClockSettingsRepository.clockFaceUri, - false, - mClockSettingsObserver - ); - getContext().getContentResolver().registerContentObserver( - ClockSettingsRepository.alignmentUri, - false, - mClockSettingsObserver - ); - updateShouldCenterIcons(); - } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - getContext().getContentResolver().unregisterContentObserver(mClockSettingsObserver); - } private void updateShouldCenterIcons() { final boolean shouldCenter = ClockSettingsRepository.shouldCenterIcons(getContext()); if (mShouldCenterIcons != shouldCenter) { From b4c95a2bc32ca430f542b4ff3d8eb3e99f5229ec Mon Sep 17 00:00:00 2001 From: elpaablo Date: Mon, 18 May 2026 11:21:47 +0100 Subject: [PATCH 15/51] SystemUI: Fix custom large clock sizing issues with dynamic clock Some custom large clocks used `match_parent` height, causing KeyguardClockPositionAlgorithm to calculate the wrong translation offsets and pivot points. This resulted in clocks being pushed off-center or scaling incorrectly during dynamic sizing. Changes: - Set layout_height to wrap_content for general, bitmap_compose, and old_quick_look large clocks. - Updated large analog and old quick look compose modifiers to measure accurately within wrapped constraints. --- .../customization/res/layout/clock_bitmap_compose_large.xml | 2 +- .../SystemUI/customization/res/layout/clock_general_large.xml | 2 +- .../customization/res/layout/clock_old_quick_look_large.xml | 2 +- .../systemui/shared/clocks/view/BitmapDigitComposeClockView.kt | 2 +- .../systemui/shared/clocks/view/OldQuickLookClockView.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml b/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml index fd57f89bc3f02..46e64aa5b8fc6 100644 --- a/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml +++ b/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml @@ -2,4 +2,4 @@ + android:layout_height="wrap_content"/> diff --git a/packages/SystemUI/customization/res/layout/clock_general_large.xml b/packages/SystemUI/customization/res/layout/clock_general_large.xml index 01be414c47682..96f5fe2f2859f 100644 --- a/packages/SystemUI/customization/res/layout/clock_general_large.xml +++ b/packages/SystemUI/customization/res/layout/clock_general_large.xml @@ -2,4 +2,4 @@ + android:layout_height="wrap_content"/> diff --git a/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml b/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml index 53c57b24e3279..5f6c37e934e11 100644 --- a/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml +++ b/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml @@ -2,4 +2,4 @@ + android:layout_height="wrap_content"/> diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt index 0b9532026ca2c..8c58930abb9a9 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt @@ -661,7 +661,7 @@ class BitmapDigitComposeClockView @JvmOverloads constructor( val (time, date, isDoze, screenOff, regionDark) = rememberClockState() Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxWidth().aspectRatio(1f), contentAlignment = Alignment.Center, ) { Box(modifier = Modifier.fillMaxSize().analogDrawModifier(config, time, isDoze, screenOff, regionDark)) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt index 2b145914fc64c..8e36e7b5fd53e 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt @@ -203,7 +203,7 @@ class OldQuickLookClockView @JvmOverloads constructor( } Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxWidth().wrapContentHeight(), contentAlignment = Alignment.Center, ) { Column( From de9ef7a8f9c986bed72ffeedc11efd514bee9ba6 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Tue, 19 May 2026 11:56:47 +0100 Subject: [PATCH 16/51] SystemUI: Port legacy Stylish clocks and fix layout bugs - Port legacy Stylish 2 and Stylish 7 clock layouts to Axion Compose framework. - Register STYLISH_2 and STYLISH_7 clocks in AxClockProvider, AxClockType, and ClockConfigs. - Fix compilation error due to missing `aspectRatio` import in `BitmapDigitComposeClockView.kt`. - Fix invisible large clock rendering in `OldQuickLookClockView` by supporting colon separation for time splitting and setting correct column width constraints. --- .../res/layout/clock_stylish2.xml | 8 + .../res/layout/clock_stylish2_large.xml | 9 + .../res/layout/clock_stylish7.xml | 8 + .../res/layout/clock_stylish7_large.xml | 9 + .../customization/res/values/strings.xml | 2 + .../systemui/shared/clocks/AxClockProvider.kt | 2 + .../systemui/shared/clocks/AxClockType.kt | 10 + .../systemui/shared/clocks/ClockConfigs.kt | 20 ++ .../view/BitmapDigitComposeClockView.kt | 1 + .../clocks/view/OldQuickLookClockView.kt | 4 +- .../shared/clocks/view/QuickLookUtils.kt | 16 +- .../shared/clocks/view/Stylish2ClockView.kt | 291 +++++++++++++++++ .../shared/clocks/view/Stylish7ClockView.kt | 294 ++++++++++++++++++ 13 files changed, 669 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/customization/res/layout/clock_stylish2.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_stylish2_large.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_stylish7.xml create mode 100644 packages/SystemUI/customization/res/layout/clock_stylish7_large.xml create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish2ClockView.kt create mode 100644 packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt diff --git a/packages/SystemUI/customization/res/layout/clock_stylish2.xml b/packages/SystemUI/customization/res/layout/clock_stylish2.xml new file mode 100644 index 0000000000000..018bb9f50daf9 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish2.xml @@ -0,0 +1,8 @@ + + + diff --git a/packages/SystemUI/customization/res/layout/clock_stylish2_large.xml b/packages/SystemUI/customization/res/layout/clock_stylish2_large.xml new file mode 100644 index 0000000000000..ad80e55d70fb7 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish2_large.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/customization/res/layout/clock_stylish7.xml b/packages/SystemUI/customization/res/layout/clock_stylish7.xml new file mode 100644 index 0000000000000..cf315deb65cb5 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish7.xml @@ -0,0 +1,8 @@ + + + diff --git a/packages/SystemUI/customization/res/layout/clock_stylish7_large.xml b/packages/SystemUI/customization/res/layout/clock_stylish7_large.xml new file mode 100644 index 0000000000000..83cf0a6019f81 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish7_large.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/customization/res/values/strings.xml b/packages/SystemUI/customization/res/values/strings.xml index 55c514fb1ef09..4d271c5be7ca7 100644 --- a/packages/SystemUI/customization/res/values/strings.xml +++ b/packages/SystemUI/customization/res/values/strings.xml @@ -34,6 +34,8 @@ CYBERPUNK AXION_AGE SEGMENTS + STYLISH_2 + STYLISH_7 ? __° : ) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt index 383f8bd6d0c65..7099e6f60d4e8 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt @@ -67,6 +67,8 @@ class AxClockProvider( add(AxClockType.CYBERPUNK) add(AxClockType.AXION_AGE) add(AxClockType.SEGMENTS) + add(AxClockType.STYLISH_2) + add(AxClockType.STYLISH_7) } return availableTypes.map { type -> diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt index c562bd0d308a2..87522fbe4ced0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt @@ -84,6 +84,16 @@ enum class AxClockType( viewId = R.layout.clock_bitmap_compose, largeViewId = R.layout.clock_bitmap_compose_large, bitmapFaceStyle = ClockFaceStyle.SEGMENTS + ), + STYLISH_2( + clockId = R.string.clock_id_stylish2, + viewId = R.layout.clock_stylish2, + largeViewId = R.layout.clock_stylish2_large + ), + STYLISH_7( + clockId = R.string.clock_id_stylish7, + viewId = R.layout.clock_stylish7, + largeViewId = R.layout.clock_stylish7_large ); companion object { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt index 0d190f971ac1f..a83783f2a2372 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt @@ -117,6 +117,26 @@ object ClockConfigs { Align.CENTER, visible = false, ), + "Stylish2ClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "Stylish2ClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + "Stylish7ClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "Stylish7ClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), ) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt index 8c58930abb9a9..6a9a0fd65ed25 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt @@ -37,6 +37,7 @@ import android.view.animation.PathInterpolator import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt index 8e36e7b5fd53e..992aec7301518 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt @@ -207,12 +207,14 @@ class OldQuickLookClockView @JvmOverloads constructor( contentAlignment = Alignment.Center, ) { Column( + modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Column( + modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, - ) { + ) { Text( text = hours, maxLines = 1, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt index 6c7474881a403..0a1544cf4198f 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt @@ -19,10 +19,18 @@ package com.android.systemui.shared.clocks.view import android.graphics.Typeface import androidx.compose.ui.text.font.FontFamily -fun splitTimeLines(time: String): Pair = when (time.length) { - 4 -> time.substring(0, 2) to time.substring(2, 4) - 3 -> time.substring(0, 1) to time.substring(1, 3) - else -> "" to "" +fun splitTimeLines(time: String): Pair { + if (time.contains(":")) { + val parts = time.split(":") + if (parts.size == 2) { + return parts[0] to parts[1] + } + } + return when (time.length) { + 4 -> time.substring(0, 2) to time.substring(2, 4) + 3 -> time.substring(0, 1) to time.substring(1, 3) + else -> "" to "" + } } fun resolveBodyFontFamily(): FontFamily { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish2ClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish2ClockView.kt new file mode 100644 index 0000000000000..e92dc90a0485b --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish2ClockView.kt @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.shared.clocks.ClockSettingsRepository +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +class Stylish2ClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = + if (isLargeClock) "Stylish2LargeClockView" else "Stylish2ClockView" + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val bgColor = if (isDoze) Color.Transparent else tintColor.copy(alpha = 0.12f) + val textOnBg = tintColor + val greetingAlpha = if (isDoze) 0.6f else 0.85f + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + + val (hours, minutes) = splitTimeLines(time) + + val contentAlign = when { + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + contentAlignment = contentAlign, + ) { + Column( + horizontalAlignment = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + }, + ) { + Text( + text = "Have a great", + style = TextStyle( + fontSize = 28.sp * dynSizeScale, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + Text( + text = "$dayOfWeek !", + style = TextStyle( + fontSize = 28.sp * dynSizeScale, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + TimeBox(hours, textOnBg, bgColor, dynSizeScale, isDoze) + + Spacer(modifier = Modifier.width(8.dp)) + ColonDots(tintColor, dynSizeScale) + Spacer(modifier = Modifier.width(8.dp)) + + TimeBox(minutes, textOnBg, bgColor, dynSizeScale, isDoze) + + Spacer(modifier = Modifier.width(8.dp)) + + DateBox(monthDay, textOnBg, bgColor, dynSizeScale, isDoze) + } + } + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val bgColor = if (isDoze) Color.Transparent else tintColor.copy(alpha = 0.12f) + val textOnBg = tintColor + val greetingAlpha = if (isDoze) 0.6f else 0.85f + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + + val (hours, minutes) = splitTimeLines(time) + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Have a great", + style = TextStyle( + fontSize = 36.sp, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + Text( + text = "$dayOfWeek !", + style = TextStyle( + fontSize = 36.sp, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + TimeBox(hours, textOnBg, bgColor, 1.3f, isDoze) + + Spacer(modifier = Modifier.width(10.dp)) + ColonDots(tintColor, 1.3f) + Spacer(modifier = Modifier.width(10.dp)) + + TimeBox(minutes, textOnBg, bgColor, 1.3f, isDoze) + + Spacer(modifier = Modifier.width(10.dp)) + + DateBox(monthDay, textOnBg, bgColor, 1.3f, isDoze) + } + + Spacer(modifier = Modifier.height(12.dp)) + + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun TimeBox( + text: String, + textColor: Color, + bgColor: Color, + scale: Float, + isDoze: Boolean, + ) { + val shape = RoundedCornerShape(16.dp) + val bg = if (isDoze) Color.White.copy(alpha = 0.08f) else bgColor + Box( + modifier = Modifier + .clip(shape) + .background(bg) + .padding(horizontal = (16 * scale).dp, vertical = (10 * scale).dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + style = TextStyle( + fontSize = 48.sp * scale, + fontWeight = FontWeight.Black, + color = textColor, + letterSpacing = (-1).sp, + ), + ) + } + } + + @Composable + private fun ColonDots(color: Color, scale: Float) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .size((6 * scale).dp) + .background(color, CircleShape), + ) + Box( + modifier = Modifier + .size((6 * scale).dp) + .background(color, CircleShape), + ) + } + } + + @Composable + private fun DateBox( + text: String, + textColor: Color, + bgColor: Color, + scale: Float, + isDoze: Boolean, + ) { + val shape = RoundedCornerShape(16.dp) + val bg = if (isDoze) Color.White.copy(alpha = 0.08f) else bgColor + Box( + modifier = Modifier + .clip(shape) + .background(bg) + .padding(horizontal = (12 * scale).dp, vertical = (10 * scale).dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + style = TextStyle( + fontSize = 16.sp * scale, + fontWeight = FontWeight.Medium, + color = textColor.copy(alpha = 0.8f), + ), + ) + } + } + + private fun formatDayOfWeek(): String { + val sdf = SimpleDateFormat("EEEE", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } + + private fun formatMonthDay(): String { + val sdf = SimpleDateFormat("MMM dd", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt new file mode 100644 index 0000000000000..f3b959937e55a --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt @@ -0,0 +1,294 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.shared.clocks.ClockSettingsRepository +import java.text.SimpleDateFormat +import java.util.Locale + +class Stylish7ClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = + if (isLargeClock) "Stylish7LargeClockView" else "Stylish7ClockView" + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val accentColor = if (isDoze) Color.White else Color(0xFF5AC8FA) + val bgColor = if (isDoze) Color.White.copy(alpha = 0.06f) else tintColor.copy(alpha = 0.08f) + val dividerColor = if (isDoze) Color.White.copy(alpha = 0.3f) else tintColor.copy(alpha = 0.2f) + + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + val (hours, minutes) = splitTimeLines(time) + val timeDisplay = "$hours \u2022 $minutes" + + val contentAlign = when { + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + contentAlignment = contentAlign, + ) { + ClockBody( + timeDisplay = timeDisplay, + monthDay = monthDay, + dayOfWeek = dayOfWeek, + tintColor = tintColor, + accentColor = accentColor, + bgColor = bgColor, + dividerColor = dividerColor, + isDoze = isDoze, + scale = dynSizeScale, + profileSize = 36.dp, + timeFontSize = 42f, + dateFontSize = 14f, + dayFontSize = 13f, + dividerWidth = 2.dp, + sectionHeight = 100.dp, + ) + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val accentColor = if (isDoze) Color.White else Color(0xFF5AC8FA) + val bgColor = if (isDoze) Color.White.copy(alpha = 0.06f) else tintColor.copy(alpha = 0.08f) + val dividerColor = if (isDoze) Color.White.copy(alpha = 0.3f) else tintColor.copy(alpha = 0.2f) + + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + val (hours, minutes) = splitTimeLines(time) + val timeDisplay = "$hours \u2022 $minutes" + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ClockBody( + timeDisplay = timeDisplay, + monthDay = monthDay, + dayOfWeek = dayOfWeek, + tintColor = tintColor, + accentColor = accentColor, + bgColor = bgColor, + dividerColor = dividerColor, + isDoze = isDoze, + scale = 1f, + profileSize = 48.dp, + timeFontSize = 56f, + dateFontSize = 18f, + dayFontSize = 16f, + dividerWidth = 2.5.dp, + sectionHeight = 140.dp, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun ClockBody( + timeDisplay: String, + monthDay: String, + dayOfWeek: String, + tintColor: Color, + accentColor: Color, + bgColor: Color, + dividerColor: Color, + isDoze: Boolean, + scale: Float, + profileSize: Dp, + timeFontSize: Float, + dateFontSize: Float, + dayFontSize: Float, + dividerWidth: Dp, + sectionHeight: Dp, + ) { + val shape = RoundedCornerShape(20.dp) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .clip(shape) + .background(bgColor) + .padding(16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.height(sectionHeight * scale), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxHeight(), + ) { + Box( + modifier = Modifier + .size(profileSize * scale) + .background( + if (isDoze) Color.White.copy(alpha = 0.15f) + else accentColor.copy(alpha = 0.2f), + CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "\u0CA0\u1D25\u0CA0", + style = TextStyle( + fontSize = (profileSize.value * 0.35f * scale).sp, + color = if (isDoze) Color.White else accentColor, + ), + ) + } + } + + Spacer(modifier = Modifier.width(12.dp * scale)) + + Box( + modifier = Modifier + .width(dividerWidth) + .fillMaxHeight(0.7f) + .background(dividerColor), + ) + + Spacer(modifier = Modifier.width(16.dp * scale)) + + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxHeight(), + ) { + Text( + text = timeDisplay, + style = TextStyle( + fontSize = (timeFontSize * scale).sp, + fontWeight = FontWeight.Black, + color = tintColor, + letterSpacing = (-1).sp, + ), + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = monthDay, + style = TextStyle( + fontSize = (dateFontSize * scale).sp, + fontWeight = FontWeight.Medium, + color = tintColor.copy(alpha = if (isDoze) 0.6f else 0.7f), + ), + ) + } + } + } + + Spacer(modifier = Modifier.height(10.dp * scale)) + + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background( + if (isDoze) Color.White.copy(alpha = 0.08f) + else accentColor.copy(alpha = 0.15f), + ) + .padding(horizontal = (16 * scale).dp, vertical = (6 * scale).dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = dayOfWeek.uppercase(), + style = TextStyle( + fontSize = (dayFontSize * scale).sp, + fontWeight = FontWeight.Bold, + color = if (isDoze) Color.White else accentColor, + letterSpacing = 2.sp, + textAlign = TextAlign.Center, + ), + ) + } + } + } + + private fun formatDayOfWeek(): String { + val sdf = SimpleDateFormat("EEEE", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } + + private fun formatMonthDay(): String { + val sdf = SimpleDateFormat("MMMM dd", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } +} From 8f8bef5c87cd6186fb5f7e14a7b65a3014855bb2 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 2 May 2026 11:11:54 +0800 Subject: [PATCH 17/51] [ax_ambient] adding aod screen off animation Change-Id: I1c798bb0c29558c97fcf381d973189ce30af4a1e --- core/java/android/os/PowerManager.java | 10 ++++++++- .../systemui/LauncherProxyService.java | 9 +++++++- .../data/repository/KeyguardRepository.kt | 17 ++++++++++++++ .../repository/LightRevealScrimRepository.kt | 22 ++++++++++++++++++- .../domain/interactor/PowerInteractor.kt | 7 ++++++ .../power/shared/model/WakeSleepReason.kt | 5 ++++- .../NotificationPanelViewController.java | 4 +++- .../systemui/shade/PulsingGestureListener.kt | 5 +++-- .../systemui/shade/QQSGestureListener.kt | 8 +++++-- .../UnlockedScreenOffAnimationController.kt | 21 ++++++++++++++---- 10 files changed, 95 insertions(+), 13 deletions(-) diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 10a2941abd7ea..8f276d00b97b6 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -597,9 +597,15 @@ public static String userActivityEventToString(@UserActivityEvent int userActivi public static final int GO_TO_SLEEP_REASON_UNKNOWN = 14; /** + * Go to sleep reason code: Going to sleep due to a touch tap or double tap. * @hide */ - public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_UNKNOWN; + public static final int GO_TO_SLEEP_REASON_TOUCH = 15; + + /** + * @hide + */ + public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_TOUCH; /** * @hide @@ -620,6 +626,7 @@ public static String sleepReasonToString(@GoToSleepReason int sleepReason) { case GO_TO_SLEEP_REASON_QUIESCENT: return "quiescent"; case GO_TO_SLEEP_REASON_SLEEP_BUTTON: return "sleep_button"; case GO_TO_SLEEP_REASON_TIMEOUT: return "timeout"; + case GO_TO_SLEEP_REASON_TOUCH: return "touch"; case GO_TO_SLEEP_REASON_UNKNOWN: return "unknown"; default: return Integer.toString(sleepReason); } @@ -733,6 +740,7 @@ public static String sleepReasonToString(@GoToSleepReason int sleepReason) { GO_TO_SLEEP_REASON_QUIESCENT, GO_TO_SLEEP_REASON_SLEEP_BUTTON, GO_TO_SLEEP_REASON_TIMEOUT, + GO_TO_SLEEP_REASON_TOUCH, GO_TO_SLEEP_REASON_UNKNOWN, }) @Retention(RetentionPolicy.SOURCE) diff --git a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java index 5bf2f4b7e689c..c35e8cc8ea57b 100644 --- a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java @@ -101,6 +101,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; import com.android.systemui.model.SysUiState; import com.android.systemui.model.SysUiState.SysUiStateCallback; @@ -190,6 +191,7 @@ public class LauncherProxyService implements CallbackController mUnfoldTransitionProgressForwarder; private final UiEventLogger mUiEventLogger; @@ -529,8 +531,11 @@ public void toggleQuickSettingsPanel() { public void onSleepEvent(MotionEvent event) { verifyCallerAndClearCallingIdentity("onSleepEvent", () -> { mHandler.post(() -> { + mPowerInteractor.setLastTouchToSleepPosition( + event.getX(), event.getY()); mContext.getSystemService(PowerManager.class) - .goToSleep(event.getEventTime()); + .goToSleep(event.getEventTime(), + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0); event.recycle(); }); }); @@ -804,6 +809,7 @@ public LauncherProxyService(Context context, UserTracker userTracker, UserManager userManager, WakefulnessLifecycle wakefulnessLifecycle, + PowerInteractor powerInteractor, UiEventLogger uiEventLogger, DisplayTracker displayTracker, KeyguardUnlockAnimationController sysuiUnlockAnimationController, @@ -846,6 +852,7 @@ public LauncherProxyService(Context context, mShadeModeInteractor = shadeModeInteractor; mShadeDisplayPolicy = shadeDisplayPolicy; mUserTracker = userTracker; + mPowerInteractor = powerInteractor; mConnectionBackoffAttempts = 0; int defaultLauncher = android.os.SystemProperties.getInt("persist.sys.default_launcher", 0); String[] launcherComponents = context.getResources().getStringArray(com.android.internal.R.array.config_launcherComponents); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 4f45d44e9c0c3..c96084d5a05e4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -189,6 +189,10 @@ interface KeyguardRepository { val lastDozeTapToWakePosition: StateFlow + val lastTouchToSleepPosition: StateFlow + + fun setLastTouchToSleepPosition(position: Point) + /** Last point that [KeyguardRootView] was tapped */ val lastRootViewTapPosition: MutableStateFlow @@ -284,6 +288,8 @@ interface KeyguardRepository { fun setLastDozeTapToWakePosition(position: Point) + fun clearLastTouchToSleepPosition() + fun setIsDozing(isDozing: Boolean) fun dozeTimeTick() @@ -437,6 +443,17 @@ constructor( _lastDozeTapToWakePosition.value = position } + private val _lastTouchToSleepPosition = MutableStateFlow(null) + override val lastTouchToSleepPosition = _lastTouchToSleepPosition.asStateFlow() + + override fun setLastTouchToSleepPosition(position: Point) { + _lastTouchToSleepPosition.value = position + } + + override fun clearLastTouchToSleepPosition() { + _lastTouchToSleepPosition.value = null + } + override val lastRootViewTapPosition: MutableStateFlow = MutableStateFlow(null) override val ambientIndicationVisible: MutableStateFlow = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt index ad7c25e67b459..bd930f90eba31 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.power.data.repository.PowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakeSleepReason.TAP +import com.android.systemui.power.shared.model.WakeSleepReason.TOUCH_TO_SLEEP import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.CircleReveal @@ -46,7 +47,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -134,17 +137,34 @@ constructor( /** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */ private val nonBiometricRevealEffect: Flow = - powerRepository.wakefulness.flatMapLatest { wakefulnessModel -> + combine( + powerRepository.wakefulness, + keyguardRepository.lastTouchToSleepPosition, + ) { wakefulnessModel, touchSleepPos -> + Pair(wakefulnessModel, touchSleepPos) + }.flatMapLatest { (wakefulnessModel, touchSleepPos) -> when { wakefulnessModel.isAwakeOrAsleepFrom(WakeSleepReason.POWER_BUTTON) -> powerButtonRevealEffect wakefulnessModel.isAwakeFrom(TAP) -> tapRevealEffect + wakefulnessModel.isAwakeOrAsleepFrom(TOUCH_TO_SLEEP) && touchSleepPos != null -> + flowOf(constructCircleRevealFromPoint(touchSleepPos)) else -> flowOf(LiftReveal) } } private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f) + init { + backgroundScope.launch { + powerRepository.wakefulness.collect { model -> + if (model.isAwake()) { + keyguardRepository.clearLastTouchToSleepPosition() + } + } + } + } + override val wallpaperSupportsAmbientMode: MutableStateFlow = MutableStateFlow(false) override val useDarkWallpaperScrim: StateFlow = diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index cef410908b74d..7222a5ca7e1e3 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -17,11 +17,13 @@ package com.android.systemui.power.domain.interactor +import android.graphics.Point import android.os.PowerManager import com.android.systemui.camera.CameraGestureHelper import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -50,6 +52,7 @@ constructor( private val screenOffAnimationController: ScreenOffAnimationController, private val statusBarStateController: StatusBarStateController, private val cameraGestureHelper: Provider, + private val keyguardRepository: KeyguardRepository, ) { /** Whether the screen is on or off. */ val isInteractive: StateFlow = repository.isInteractive @@ -93,6 +96,10 @@ constructor( fun onUserTouch(noChangeLights: Boolean = false) = repository.userTouch(noChangeLights = noChangeLights) + fun setLastTouchToSleepPosition(x: Float, y: Float) { + keyguardRepository.setLastTouchToSleepPosition(Point(x.toInt(), y.toInt())) + } + /** * Wakes up the device if the device was dozing. * diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt index c57b53bab4422..cd390cffd85cc 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt @@ -60,7 +60,9 @@ enum class WakeSleepReason( FOLD(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD), /** Device goes to sleep because it timed out. */ - TIMEOUT(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); + TIMEOUT(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT), + + TOUCH_TO_SLEEP(isTouch = true, PowerManager.GO_TO_SLEEP_REASON_TOUCH); companion object { fun fromPowerManagerWakeReason(reason: Int): WakeSleepReason { @@ -84,6 +86,7 @@ enum class WakeSleepReason( PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON -> SLEEP_BUTTON PowerManager.GO_TO_SLEEP_REASON_TIMEOUT -> TIMEOUT PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD + PowerManager.GO_TO_SLEEP_REASON_TOUCH -> TOUCH_TO_SLEEP else -> OTHER } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 1763c406efc64..db9428a3a4aed 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -830,7 +830,9 @@ public void onViewDetachedFromWindow(View v) {} @Override public boolean onDoubleTap(MotionEvent e) { if (mPowerManager != null) { - mPowerManager.goToSleep(e.getEventTime()); + mPowerInteractor.setLastTouchToSleepPosition(e.getX(), e.getY()); + mPowerManager.goToSleep(e.getEventTime(), + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0); } return true; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt index fc53866cef7de..996058524d5ae 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt @@ -121,10 +121,10 @@ constructor( return false } - return onDoubleTapEvent() + return onDoubleTapEvent(e.x, e.y) } - fun onDoubleTapEvent(): Boolean { + fun onDoubleTapEvent(x: Float = 0f, y: Float = 0f): Boolean { // React to the [MotionEvent.ACTION_UP] event after double tap is detected. Falsing // checks MUST be on the ACTION_UP event. if ( @@ -133,6 +133,7 @@ constructor( !falsingManager.isProximityNear && !falsingManager.isFalseDoubleTap ) { + dozeInteractor.setLastTapToWakePosition(Point(x.toInt(), y.toInt())) powerInteractor.wakeUpIfDozing("PULSING_DOUBLE_TAP", PowerManager.WAKE_REASON_TAP) return true } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt index 002aeb31629f0..2126e512defa1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt @@ -26,9 +26,10 @@ import android.view.MotionEvent import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.CentralSurfaces +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import javax.inject.Inject @SysUISingleton @@ -39,6 +40,7 @@ class QQSGestureListener @Inject constructor( private val statusBarStateController: StatusBarStateController, private val selectedUserInteractor: SelectedUserInteractor, private val centralSurfaces: CentralSurfaces, + private val powerInteractor: PowerInteractor, ) : GestureDetector.SimpleOnGestureListener() { private var doubleTapToSleepEnabled = false @@ -80,7 +82,9 @@ class QQSGestureListener @Inject constructor( e.getY() < quickQsOffsetHeight && !falsingManager.isFalseDoubleTap ) { - powerManager.goToSleep(e.getEventTime()) + powerInteractor.setLastTouchToSleepPosition(e.x, e.y) + powerManager.goToSleep(e.eventTime, + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0) return true } else if (!statusBarStateController.isDozing && lockscreenDT2SEnabled && diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 75dc42ba4b4ee..642fb231a9622 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.display.domain.interactor.DisplayStateInteractor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.res.R import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor @@ -31,6 +32,7 @@ import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LiftReveal import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim +import com.android.systemui.statusbar.PowerButtonReveal import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.StatusBarStateControllerImpl @@ -132,10 +134,12 @@ constructor( } override fun onAnimationStart(animation: Animator) { - if (dozeParameters.get().isMinModeActive()) { - lightRevealScrim.revealEffect = LiftReveal - } else { - lightRevealScrim.revealEffect = revealEffect + if (!ambientAod()) { + if (dozeParameters.get().isMinModeActive()) { + lightRevealScrim.revealEffect = LiftReveal + } else { + lightRevealScrim.revealEffect = revealEffect + } } interactionJankMonitor.begin( notifShadeWindowControllerLazy.get().windowRootView, @@ -298,6 +302,15 @@ constructor( lightRevealAnimator.setDuration(LIGHT_REVEAL_ANIMATION_DURATION_MINMODE) } + if (wakefulnessLifecycle.lastSleepReason == PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON) { + val powerButtonY = context.resources.getDimensionPixelSize( + R.dimen.physical_power_button_center_screen_location_y + ).toFloat() + revealEffect = PowerButtonReveal(powerButtonY) + } else { + revealEffect = LiftReveal + } + // Start the animation on the next frame. startAnimation() is called after // PhoneWindowManager makes a binder call to System UI on // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is From ca31269d868666721f1a3b21585f515d173f9e17 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 9 May 2026 15:07:03 +0800 Subject: [PATCH 18/51] [ax_animations] adding screen on wp depth animation Change-Id: I07ffb4a8581f9d24f6ea19f9e31d32b40223acc9 --- .../KeyguardUnlockAnimationController.kt | 83 ++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index c6ca2d0a6d952..e825b9eb4677a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -125,6 +125,10 @@ const val LAUNCHER_ICONS_ANIMATION_DURATION_MS = 633L */ const val CANNED_UNLOCK_START_DELAY_MS = 25L +const val LOCKSCREEN_WALLPAPER_ZOOM = 1f +const val SCREEN_ON_WALLPAPER_ZOOM_DURATION_MS = 650L +const val SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS = 300L + /** * Duration for the alpha animation on the surface behind. This plays to fade in the surface during * a swipe to unlock (and to fade it back out if the swipe is cancelled). @@ -164,7 +168,11 @@ constructor( private val notificationShadeWindowController: NotificationShadeWindowController, private val powerManager: PowerManager, private val wallpaperManager: WallpaperManager, -) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() { + private val wakefulnessLifecycle: WakefulnessLifecycle, +) : + KeyguardStateController.Callback, + WakefulnessLifecycle.Observer, + ISysuiUnlockAnimationController.Stub() { interface KeyguardUnlockAnimationListener { /** @@ -304,6 +312,9 @@ constructor( */ @VisibleForTesting val surfaceBehindEntryAnimator = ValueAnimator.ofFloat(0f, 1f) + private var wallpaperZoomAnimator: ValueAnimator? = null + private var hasPendingScreenOnWallpaperZoom = false + /** Rounded corner radius to apply to the surface behind the keyguard. */ private var roundedCornerRadius = 0f @@ -437,6 +448,7 @@ constructor( // Listen for changes in the dismiss amount. keyguardStateController.addCallback(this) + wakefulnessLifecycle.addObserver(this) roundedCornerRadius = resources.getDimensionPixelSize(R.dimen.rounded_corner_radius).toFloat() @@ -451,6 +463,32 @@ constructor( listeners.remove(listener) } + override fun onStartedWakingUp() { + if (keyguardStateController.isShowing) { + cancelWallpaperZoomAnimator() + hasPendingScreenOnWallpaperZoom = true + setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + } + } + + override fun onFinishedWakingUp() { + if (!hasPendingScreenOnWallpaperZoom) { + return + } + hasPendingScreenOnWallpaperZoom = false + if (keyguardStateController.isShowing) { + animateKeyguardWallpaperZoom() + } else { + setKeyguardWallpaperZoom(0f) + } + } + + override fun onStartedGoingToSleep() { + hasPendingScreenOnWallpaperZoom = false + cancelWallpaperZoomAnimator() + setKeyguardWallpaperZoom(0f) + } + /** * Whether we should be able to do the in-window launcher animations given the current state of * the device. @@ -1102,6 +1140,9 @@ constructor( surfaceBehindEntryAnimator.cancel() wallpaperCannedUnlockAnimator.cancel() wallpaperFadeOutUnlockAnimator.cancel() + cancelWallpaperZoomAnimator() + hasPendingScreenOnWallpaperZoom = false + setKeyguardWallpaperZoom(0f) // That target is no longer valid since the animation finished, null it out. surfaceBehindRemoteAnimationTargets = null @@ -1114,6 +1155,46 @@ constructor( willUnlockWithSmartspaceTransition = false } + private fun animateKeyguardWallpaperZoom() { + cancelWallpaperZoomAnimator() + wallpaperZoomAnimator = + ValueAnimator.ofFloat(LOCKSCREEN_WALLPAPER_ZOOM, 0f).apply { + duration = SCREEN_ON_WALLPAPER_ZOOM_DURATION_MS + startDelay = SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS + interpolator = Interpolators.FAST_OUT_SLOW_IN + addUpdateListener { valueAnimator -> + setKeyguardWallpaperZoom(valueAnimator.animatedValue as Float) + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + setKeyguardWallpaperZoom(0f) + if (wallpaperZoomAnimator === animation) { + wallpaperZoomAnimator = null + } + } + } + ) + start() + } + } + + private fun cancelWallpaperZoomAnimator() { + wallpaperZoomAnimator?.cancel() + wallpaperZoomAnimator = null + } + + private fun setKeyguardWallpaperZoom(zoom: Float) { + val boundedZoom = zoom.coerceIn(0f, 1f) + val rootView = keyguardViewController.viewRootImpl.view ?: return + val windowToken = rootView.windowToken ?: return + try { + wallpaperManager.setWallpaperZoomOut(windowToken, boundedZoom) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Unable to set keyguard wallpaper zoom", e) + } + } + /** * Asks the keyguard view to hide, using the start time from the beginning of the remote * animation. From a84c9f98fad5ade0b6dd436ba4d36fc853965a74 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 12 May 2026 07:21:22 +0800 Subject: [PATCH 19/51] fixup! [ax_animations] adding screen on wp depth animation --- .../KeyguardUnlockAnimationController.kt | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index e825b9eb4677a..f8d204d72b745 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -464,11 +464,9 @@ constructor( } override fun onStartedWakingUp() { - if (keyguardStateController.isShowing) { - cancelWallpaperZoomAnimator() - hasPendingScreenOnWallpaperZoom = true - setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) - } + cancelWallpaperZoomAnimator() + hasPendingScreenOnWallpaperZoom = true + setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) } override fun onFinishedWakingUp() { @@ -476,11 +474,7 @@ constructor( return } hasPendingScreenOnWallpaperZoom = false - if (keyguardStateController.isShowing) { - animateKeyguardWallpaperZoom() - } else { - setKeyguardWallpaperZoom(0f) - } + animateKeyguardWallpaperZoom() } override fun onStartedGoingToSleep() { @@ -1140,9 +1134,14 @@ constructor( surfaceBehindEntryAnimator.cancel() wallpaperCannedUnlockAnimator.cancel() wallpaperFadeOutUnlockAnimator.cancel() - cancelWallpaperZoomAnimator() + + if (wallpaperZoomAnimator == null && hasPendingScreenOnWallpaperZoom) { + hasPendingScreenOnWallpaperZoom = false + animateKeyguardWallpaperZoom() + } else if (wallpaperZoomAnimator == null) { + setKeyguardWallpaperZoom(0f) + } hasPendingScreenOnWallpaperZoom = false - setKeyguardWallpaperZoom(0f) // That target is no longer valid since the animation finished, null it out. surfaceBehindRemoteAnimationTargets = null From 638512a7ce28efb84aa8a09724472abd1f5ee7e2 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 14 May 2026 15:04:26 +0800 Subject: [PATCH 20/51] [ax_animations] fixing wp depth animation delay https: //github.com/AxionAOSP/issue_tracker/issues/209 Change-Id: Ica094b2ee401ed37d818d986448bc676d4c87931 --- .../KeyguardUnlockAnimationController.kt | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index f8d204d72b745..07db3b0ee3596 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -53,6 +53,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM +import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy import javax.inject.Inject @@ -126,8 +127,8 @@ const val LAUNCHER_ICONS_ANIMATION_DURATION_MS = 633L const val CANNED_UNLOCK_START_DELAY_MS = 25L const val LOCKSCREEN_WALLPAPER_ZOOM = 1f -const val SCREEN_ON_WALLPAPER_ZOOM_DURATION_MS = 650L -const val SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS = 300L +const val SCREEN_ON_WALLPAPER_ZOOM_DURATION_MS = 550L +const val SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS = 0L /** * Duration for the alpha animation on the surface behind. This plays to fade in the surface during @@ -314,6 +315,9 @@ constructor( private var wallpaperZoomAnimator: ValueAnimator? = null private var hasPendingScreenOnWallpaperZoom = false + private val setScreenOnWallpaperZoomRunnable = Runnable { + setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + } /** Rounded corner radius to apply to the surface behind the keyguard. */ private var roundedCornerRadius = 0f @@ -463,10 +467,40 @@ constructor( listeners.remove(listener) } + private fun isDozeWakeUp(): Boolean = + statusBarStateController.isDozing || statusBarStateController.dozeAmount > 0f + + private fun getScreenOnWallpaperZoomStartDelay(): Long = + if (isDozeWakeUp()) { + CentralSurfaces.FADE_KEYGUARD_DURATION_PULSING.toLong() + } else { + SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS + } + + private fun getLauncherUnlockRevealStartDelay(): Long { + val biometricUnlockController = biometricUnlockControllerLazy.get() + return if (biometricUnlockController.isWakeAndUnlock || isDozeWakeUp()) { + CentralSurfaces.FADE_KEYGUARD_DURATION_PULSING.toLong() + } else if (keyguardStateController.isKeyguardFadingAway) { + keyguardStateController.keyguardFadingAwayDelay + + keyguardStateController.keyguardFadingAwayDuration + } else { + (CentralSurfaces.FADE_KEYGUARD_START_DELAY + + CentralSurfaces.FADE_KEYGUARD_DURATION).toLong() + } + } + override fun onStartedWakingUp() { cancelWallpaperZoomAnimator() - hasPendingScreenOnWallpaperZoom = true - setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + hasPendingScreenOnWallpaperZoom = + keyguardStateController.isShowing || statusBarStateController.isDozing + if (hasPendingScreenOnWallpaperZoom) { + keyguardViewController.viewRootImpl.view?.postOnAnimation( + setScreenOnWallpaperZoomRunnable + ) ?: setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + } else { + setKeyguardWallpaperZoom(0f) + } } override fun onFinishedWakingUp() { @@ -702,12 +736,18 @@ constructor( val isWakeAndUnlockNotFromDream = biometricUnlockControllerLazy.get().isWakeAndUnlock && biometricUnlockControllerLazy.get().mode != MODE_WAKE_AND_UNLOCK_FROM_DREAM + val unlockAnimationStartDelay = + if (willUnlockWithInWindowLauncherAnimations) { + getLauncherUnlockRevealStartDelay() + } else { + CANNED_UNLOCK_START_DELAY_MS + } listeners.forEach { it.onUnlockAnimationStarted( playingCannedUnlockAnimation /* playingCannedAnimation */, isWakeAndUnlockNotFromDream /* isWakeAndUnlockNotFromDream */, - CANNED_UNLOCK_START_DELAY_MS /* unlockStartDelay */, + unlockAnimationStartDelay /* unlockStartDelay */, LAUNCHER_ICONS_ANIMATION_DURATION_MS, /* unlockAnimationDuration */ ) } @@ -728,6 +768,9 @@ constructor( private fun playCannedUnlockAnimation() { Log.d(TAG, "playCannedUnlockAnimation") playingCannedUnlockAnimation = true + hasPendingScreenOnWallpaperZoom = false + cancelWallpaperZoomAnimator() + setKeyguardWallpaperZoom(0f) when { // If we're set up for in-window launcher animations, ask Launcher to play its in-window @@ -781,7 +824,7 @@ constructor( launcherUnlockController?.playUnlockAnimation( true /* unlocked */, LAUNCHER_ICONS_ANIMATION_DURATION_MS /* duration */, - CANNED_UNLOCK_START_DELAY_MS, /* startDelay */ + getLauncherUnlockRevealStartDelay(), /* startDelay */ ) } catch (e: DeadObjectException) { // Hello! If you are here investigating a bug where Launcher is blank (no icons) @@ -1135,7 +1178,10 @@ constructor( wallpaperCannedUnlockAnimator.cancel() wallpaperFadeOutUnlockAnimator.cancel() - if (wallpaperZoomAnimator == null && hasPendingScreenOnWallpaperZoom) { + if (!showKeyguard) { + cancelWallpaperZoomAnimator() + setKeyguardWallpaperZoom(0f) + } else if (wallpaperZoomAnimator == null && hasPendingScreenOnWallpaperZoom) { hasPendingScreenOnWallpaperZoom = false animateKeyguardWallpaperZoom() } else if (wallpaperZoomAnimator == null) { @@ -1159,13 +1205,17 @@ constructor( wallpaperZoomAnimator = ValueAnimator.ofFloat(LOCKSCREEN_WALLPAPER_ZOOM, 0f).apply { duration = SCREEN_ON_WALLPAPER_ZOOM_DURATION_MS - startDelay = SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS + startDelay = getScreenOnWallpaperZoomStartDelay() interpolator = Interpolators.FAST_OUT_SLOW_IN addUpdateListener { valueAnimator -> setKeyguardWallpaperZoom(valueAnimator.animatedValue as Float) } addListener( object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + } + override fun onAnimationEnd(animation: Animator) { setKeyguardWallpaperZoom(0f) if (wallpaperZoomAnimator === animation) { @@ -1181,6 +1231,7 @@ constructor( private fun cancelWallpaperZoomAnimator() { wallpaperZoomAnimator?.cancel() wallpaperZoomAnimator = null + keyguardViewController.viewRootImpl.view?.removeCallbacks(setScreenOnWallpaperZoomRunnable) } private fun setKeyguardWallpaperZoom(zoom: Float) { From 5a1dd3a64ea271065841c43e67142bb26573d993 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Fri, 22 May 2026 19:14:37 +0100 Subject: [PATCH 21/51] SystemUI: Finalize Stylish 7 clock layout and scaling - Adjusted time and date alignment to perfectly match the username baseline - Removed duplicate EnhancedDateArea from LargeContent - Integrated WeatherRow directly into the day pill matching original XML - Fixed depth controller issues --- .../clocks/view/ClockDepthController.kt | 48 +-- .../shared/clocks/view/Stylish7ClockView.kt | 320 ++++++++++++------ .../android/keyguard/ClockEventController.kt | 12 - 3 files changed, 238 insertions(+), 142 deletions(-) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt index b12e0fdb41cfa..83549f61fc4b1 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt @@ -35,6 +35,21 @@ class ClockDepthController(private val view: View) { style = Paint.Style.FILL } + private val currentGlobalMatrix = Matrix() + private val lastGlobalMatrix = Matrix() + + private val preDrawListener = android.view.ViewTreeObserver.OnPreDrawListener { + if (shouldApplyDepth()) { + currentGlobalMatrix.reset() + view.transformMatrixToGlobal(currentGlobalMatrix) + if (currentGlobalMatrix != lastGlobalMatrix) { + lastGlobalMatrix.set(currentGlobalMatrix) + view.invalidate() + } + } + true + } + private val wallpaperMaxScale: Float = try { val id = Resources.getSystem().getIdentifier( "config_wallpaperMaxScale", "dimen", "android" @@ -78,6 +93,7 @@ class ClockDepthController(private val view: View) { if (!enabled) return DepthWallpaperProvider.init(view.context) DepthWallpaperProvider.addListener(listener) + view.viewTreeObserver.addOnPreDrawListener(preDrawListener) } fun onDetached() { @@ -85,6 +101,7 @@ class ClockDepthController(private val view: View) { maskAnimator = null maskAlpha = 0f revealProgress = 0f + view.viewTreeObserver.removeOnPreDrawListener(preDrawListener) DepthWallpaperProvider.removeListener(listener) subjectPath = null depthActive = false @@ -101,9 +118,7 @@ class ClockDepthController(private val view: View) { if (visible) { animateReveal() } else { - maskAlpha = 0f - revealProgress = 0f - view.postInvalidateOnAnimation() + animateHide() } } @@ -115,10 +130,6 @@ class ClockDepthController(private val view: View) { fun drawWithDepth(canvas: Canvas, drawSuper: (Canvas) -> Unit) { val path = subjectPath ?: run { drawSuper(canvas); return } - view.getLocationOnScreen(location) - val viewX = location[0].toFloat() - val viewY = location[1].toFloat() - val realMetrics = DisplayMetrics() view.context.display?.getRealMetrics(realMetrics) val screenW = realMetrics.widthPixels.toFloat() @@ -133,9 +144,10 @@ class ClockDepthController(private val view: View) { val visibleW: Float val visibleH: Float if (wallAspect > screenAspect) { + // Wallpaper wider than screen — left-aligned on lockscreen (offset 0) visibleW = (screenAspect / wallAspect) * 10000f visibleH = 10000f - cropLeft = (10000f - visibleW) / 2f + cropLeft = 0f cropTop = 0f } else { visibleW = 10000f @@ -150,13 +162,12 @@ class ClockDepthController(private val view: View) { if (zoom != 1f) { pathMatrix.postScale(zoom, zoom, screenW / 2f, screenH / 2f) } - pathMatrix.postTranslate(-viewX, -viewY) - val viewScaleX = view.scaleX - val viewScaleY = view.scaleY - if (viewScaleX != 1f || viewScaleY != 1f) { - pathMatrix.postScale(1f / viewScaleX, 1f / viewScaleY) - } + // Map from screen coordinates to view-local canvas coordinates + val globalToLocal = Matrix() + view.transformMatrixToGlobal(globalToLocal) + globalToLocal.invert(globalToLocal) + pathMatrix.postConcat(globalToLocal) transformedPath.reset() path.transform(pathMatrix, transformedPath) @@ -164,11 +175,8 @@ class ClockDepthController(private val view: View) { val pathBounds = RectF() transformedPath.computeBounds(pathBounds, true) - val layerLeft = -viewX - val layerTop = -viewY - val layerRight = screenW - viewX - val layerBottom = screenH - viewY - val layerRect = RectF(layerLeft, layerTop, layerRight, layerBottom) + val layerRect = RectF(0f, 0f, screenW, screenH) + globalToLocal.mapRect(layerRect) if (!RectF.intersects(pathBounds, layerRect)) { drawSuper(canvas) @@ -209,7 +217,7 @@ class ClockDepthController(private val view: View) { transformedPath.transform(revealMatrix) } - val layerCount = canvas.saveLayer(layerLeft, layerTop, layerRight, layerBottom, null) + val layerCount = canvas.saveLayer(layerRect.left, layerRect.top, layerRect.right, layerRect.bottom, null) drawSuper(canvas) maskPaint.alpha = (maskAlpha * 255f).toInt().coerceIn(0, 255) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt index f3b959937e55a..963670adfd2ce 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt @@ -6,17 +6,20 @@ package com.android.systemui.shared.clocks.view import android.content.Context +import android.graphics.Bitmap +import android.os.UserManager import android.util.AttributeSet +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -31,7 +34,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -52,6 +58,31 @@ class Stylish7ClockView @JvmOverloads constructor( override fun getTag(): String = if (isLargeClock) "Stylish7LargeClockView" else "Stylish7ClockView" + private val userManager: UserManager? = + context.getSystemService(Context.USER_SERVICE) as? UserManager + + private fun loadUserAvatar(): Bitmap? { + return try { + val userId = android.app.ActivityManager.getCurrentUser() + var icon = userManager?.getUserIcon(userId) + if (icon == null) { + val drawable = com.android.internal.util.UserIcons.getDefaultUserIcon( + context.resources, userId, false + ) + icon = com.android.internal.util.UserIcons.convertToBitmap(drawable) + } + icon + } catch (_: Exception) { null } + } + + private fun loadUserName(): String { + return try { + val userId = android.app.ActivityManager.getCurrentUser() + val userInfo = userManager?.getUserInfo(userId) + userInfo?.name?.takeIf { it.isNotBlank() } ?: "\u0CA0\u1D25\u0CA0" + } catch (_: Exception) { "\u0CA0\u1D25\u0CA0" } + } + @Composable override fun Content() { if (isLargeClock) LargeContent() else SmallContent() @@ -61,16 +92,16 @@ class Stylish7ClockView @JvmOverloads constructor( private fun SmallContent() { val (time, date, isDoze, screenOff, regionDark) = rememberClockState() val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() - val tintColor = tintColor(isDoze, screenOff, regionDark) - val accentColor = if (isDoze) Color.White else Color(0xFF5AC8FA) - val bgColor = if (isDoze) Color.White.copy(alpha = 0.06f) else tintColor.copy(alpha = 0.08f) - val dividerColor = if (isDoze) Color.White.copy(alpha = 0.3f) else tintColor.copy(alpha = 0.2f) + val accent1 = Color(context.getColor(android.R.color.system_accent1_600)) + val accent3 = Color(context.getColor(android.R.color.system_accent3_600)) val dayOfWeek = remember(date) { formatDayOfWeek() } val monthDay = remember(date) { formatMonthDay() } val (hours, minutes) = splitTimeLines(time) val timeDisplay = "$hours \u2022 $minutes" + val avatar = remember { loadUserAvatar() } + val userName = remember { loadUserName() } val contentAlign = when { isLeftAligned -> Alignment.CenterStart @@ -96,18 +127,19 @@ class Stylish7ClockView @JvmOverloads constructor( timeDisplay = timeDisplay, monthDay = monthDay, dayOfWeek = dayOfWeek, - tintColor = tintColor, - accentColor = accentColor, - bgColor = bgColor, - dividerColor = dividerColor, + accent1 = accent1, + accent3 = accent3, isDoze = isDoze, scale = dynSizeScale, - profileSize = 36.dp, - timeFontSize = 42f, - dateFontSize = 14f, - dayFontSize = 13f, - dividerWidth = 2.dp, - sectionHeight = 100.dp, + avatar = avatar, + userName = userName, + profileFrameSize = 75.dp, + profileImageSize = 64.dp, + timeFontSize = 34f, + dateFontSize = 16f, + dayFontSize = 20f, + userNameSize = 14f, + dividerHeight = 32.dp, ) } } @@ -115,16 +147,17 @@ class Stylish7ClockView @JvmOverloads constructor( @Composable private fun LargeContent() { val (time, date, isDoze, screenOff, regionDark) = rememberClockState() - val tintColor = tintColor(isDoze, screenOff, regionDark) + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() - val accentColor = if (isDoze) Color.White else Color(0xFF5AC8FA) - val bgColor = if (isDoze) Color.White.copy(alpha = 0.06f) else tintColor.copy(alpha = 0.08f) - val dividerColor = if (isDoze) Color.White.copy(alpha = 0.3f) else tintColor.copy(alpha = 0.2f) + val accent1 = Color(context.getColor(android.R.color.system_accent1_600)) + val accent3 = Color(context.getColor(android.R.color.system_accent3_600)) val dayOfWeek = remember(date) { formatDayOfWeek() } val monthDay = remember(date) { formatMonthDay() } val (hours, minutes) = splitTimeLines(time) val timeDisplay = "$hours \u2022 $minutes" + val avatar = remember { loadUserAvatar() } + val userName = remember { loadUserName() } Column( modifier = Modifier @@ -136,27 +169,19 @@ class Stylish7ClockView @JvmOverloads constructor( timeDisplay = timeDisplay, monthDay = monthDay, dayOfWeek = dayOfWeek, - tintColor = tintColor, - accentColor = accentColor, - bgColor = bgColor, - dividerColor = dividerColor, + accent1 = accent1, + accent3 = accent3, isDoze = isDoze, - scale = 1f, - profileSize = 48.dp, - timeFontSize = 56f, - dateFontSize = 18f, - dayFontSize = 16f, - dividerWidth = 2.5.dp, - sectionHeight = 140.dp, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - EnhancedDateArea( - textColor = tintColor, - textSize = 16.sp, - iconSize = 18.dp, - rowArrangement = Arrangement.Center, + scale = dynSizeScale, + avatar = avatar, + userName = userName, + profileFrameSize = 96.dp, + profileImageSize = 82.dp, + timeFontSize = 38f, + dateFontSize = 20f, + dayFontSize = 24f, + userNameSize = 18f, + dividerHeight = 40.dp, ) } } @@ -166,114 +191,189 @@ class Stylish7ClockView @JvmOverloads constructor( timeDisplay: String, monthDay: String, dayOfWeek: String, - tintColor: Color, - accentColor: Color, - bgColor: Color, - dividerColor: Color, + accent1: Color, + accent3: Color, isDoze: Boolean, scale: Float, - profileSize: Dp, + avatar: Bitmap?, + userName: String, + profileFrameSize: Dp, + profileImageSize: Dp, timeFontSize: Float, dateFontSize: Float, dayFontSize: Float, - dividerWidth: Dp, - sectionHeight: Dp, + userNameSize: Float, + dividerHeight: Dp, ) { - val shape = RoundedCornerShape(20.dp) + val bgBrush = if (isDoze) { + Brush.horizontalGradient(listOf(Color.White.copy(alpha = 0.08f), Color.White.copy(alpha = 0.08f))) + } else { + Brush.horizontalGradient(listOf(accent1, accent3)) + } + val dayPillBg = if (isDoze) Color.White.copy(alpha = 0.08f) else Color.White + val dayTextColor = if (isDoze) Color.White else accent1 Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = Modifier - .clip(shape) - .background(bgColor) - .padding(16.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.height(sectionHeight * scale), + Box { + // Background Card + Box( + modifier = Modifier + .padding(top = (profileFrameSize * scale) / 2) + .clip(RoundedCornerShape(20.dp * scale)) + .background(bgBrush) ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxHeight(), + // Foreground Content + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding( + start = 20.dp * scale, + end = 20.dp * scale, + top = 0.dp, + bottom = 24.dp * scale + ) ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.width(profileFrameSize * scale) + ) { + Spacer(modifier = Modifier.height((profileFrameSize * scale) / 2)) + Spacer(modifier = Modifier.height(8.dp * scale)) + Text( + text = userName, + maxLines = 1, + style = TextStyle( + fontSize = (userNameSize * scale).sp, + fontWeight = FontWeight.Bold, + color = Color.White, + ) + ) + } + + Spacer(modifier = Modifier.width(12.dp * scale)) + Box( modifier = Modifier - .size(profileSize * scale) - .background( - if (isDoze) Color.White.copy(alpha = 0.15f) - else accentColor.copy(alpha = 0.2f), - CircleShape, - ), - contentAlignment = Alignment.Center, + .width(2.dp * scale) + .height(dividerHeight * scale) + .background(Color.White), + ) + + Spacer(modifier = Modifier.width(12.dp * scale)) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "\u0CA0\u1D25\u0CA0", + text = timeDisplay, + modifier = Modifier.padding(top = 6.dp * scale), + style = TextStyle( + fontSize = (timeFontSize * scale).sp, + fontWeight = FontWeight.Bold, + color = Color.White, + ), + ) + Text( + text = monthDay, style = TextStyle( - fontSize = (profileSize.value * 0.35f * scale).sp, - color = if (isDoze) Color.White else accentColor, + fontSize = (dateFontSize * scale).sp, + fontWeight = FontWeight.Bold, + color = Color.White, ), ) } } + } - Spacer(modifier = Modifier.width(12.dp * scale)) - - Box( - modifier = Modifier - .width(dividerWidth) - .fillMaxHeight(0.7f) - .background(dividerColor), - ) - - Spacer(modifier = Modifier.width(16.dp * scale)) - - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxHeight(), - ) { - Text( - text = timeDisplay, - style = TextStyle( - fontSize = (timeFontSize * scale).sp, - fontWeight = FontWeight.Black, - color = tintColor, - letterSpacing = (-1).sp, - ), + // Avatar Box + Box( + modifier = Modifier + .padding(start = 20.dp * scale) + .size(profileFrameSize * scale) + .background(Color.White, CircleShape), + contentAlignment = Alignment.Center, + ) { + if (avatar != null) { + Image( + bitmap = avatar.asImageBitmap(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(profileImageSize * scale) + .clip(CircleShape), ) - Spacer(modifier = Modifier.height(4.dp)) + } else { Text( - text = monthDay, + text = "\u0CA0\u1D25\u0CA0", style = TextStyle( - fontSize = (dateFontSize * scale).sp, - fontWeight = FontWeight.Medium, - color = tintColor.copy(alpha = if (isDoze) 0.6f else 0.7f), + fontSize = (profileFrameSize.value * 0.3f * scale).sp, + color = accent1, ), ) } } } - Spacer(modifier = Modifier.height(10.dp * scale)) - + // Day Pill (overlapping the main card by offset) Box( modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background( - if (isDoze) Color.White.copy(alpha = 0.08f) - else accentColor.copy(alpha = 0.15f), - ) - .padding(horizontal = (16 * scale).dp, vertical = (6 * scale).dp), + .offset(y = (-16.dp * scale)) + .clip(RoundedCornerShape(12.dp * scale)) + .background(dayPillBg) + .padding(horizontal = 16.dp * scale, vertical = 8.dp * scale), contentAlignment = Alignment.Center, ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = dayOfWeek.uppercase(), + style = TextStyle( + fontSize = (dayFontSize * scale).sp, + fontWeight = FontWeight.Bold, + color = dayTextColor, + letterSpacing = 2.sp, + textAlign = TextAlign.Center, + ), + ) + WeatherRow( + textColor = dayTextColor, + scale = scale, + ) + } + } + } + } + + @Composable + private fun WeatherRow( + textColor: Color, + scale: Float, + ) { + val display = viewModel.rememberResolvedDisplay() + if (display is DateDisplay.Weather && display.temp.isNotEmpty()) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Spacer(modifier = Modifier.width(4.dp * scale)) + display.icon?.let { icon -> + Image( + bitmap = icon.asImageBitmap(), + contentDescription = null, + modifier = Modifier.size((18 * scale).dp), + ) + Spacer(modifier = Modifier.width(4.dp * scale)) + } Text( - text = dayOfWeek.uppercase(), + text = display.temp, style = TextStyle( - fontSize = (dayFontSize * scale).sp, + fontSize = (18f * scale).sp, fontWeight = FontWeight.Bold, - color = if (isDoze) Color.White else accentColor, + color = textColor, letterSpacing = 2.sp, - textAlign = TextAlign.Center, ), ) } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index cb0694be6082d..a5f093f54161f 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -529,22 +529,15 @@ constructor( clock?.largeClock?.events?.onPulsingChanged(pulsing) } } - private var depthBlockedByFading = false - private var depthBlockedByGoingAway = false - private var depthBlockedByBouncer = false - private var depthBlockedByAlpha = false private var depthBlockedByDozeAmount = false private val depthScrimListener = object : ScrimUtils.ScrimEventListener { override fun onKeyguardFadingAwayChanged(fadingAway: Boolean) { - depthBlockedByFading = fadingAway updateDepthVisibility() } override fun onKeyguardGoingAwayChanged(goingAway: Boolean) { - depthBlockedByGoingAway = goingAway updateDepthVisibility() } override fun onPrimaryBouncerShowingChanged(showing: Boolean) { - depthBlockedByBouncer = showing updateDepthVisibility() } override fun onDozingChanged(dozing: Boolean) { @@ -554,7 +547,6 @@ constructor( updateDepthVisibility() } override fun onKeyguardAlphaChanged(alpha: Float) { - depthBlockedByAlpha = alpha < 1f updateDepthVisibility() } } @@ -562,10 +554,6 @@ constructor( val scrim = ScrimUtils.get() val visible = scrim.isKeyguardShowing() && !scrim.isDozing() - && !depthBlockedByFading - && !depthBlockedByGoingAway - && !depthBlockedByBouncer - && !depthBlockedByAlpha && !depthBlockedByDozeAmount clock?.events?.onDepthEffectVisibilityChanged(visible) } From 38d79f72f7cf0bf7847283a26b7b4e669b056e99 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Mon, 25 May 2026 22:28:37 +0100 Subject: [PATCH 22/51] Notifier: Reduce user activity broadcast minimum interval to 100ms Lower USER_ACTIVITY_MIN_INTERVAL_MS from 500ms to 100ms so user activity events are reported more responsively to the power manager, reducing the window where touch input can fail to extend the screen-on timeout. --- services/core/java/com/android/server/power/Notifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 2b153f8ca6fe8..b21ac2847de5f 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -211,7 +211,7 @@ private static class Interactivity { private int mBroadcastedInteractiveState; private boolean mBroadcastInProgress; private long mBroadcastStartTime; - private static final long USER_ACTIVITY_MIN_INTERVAL_MS = 500; + private static final long USER_ACTIVITY_MIN_INTERVAL_MS = 100; private long mLastUserActivityTimeMs; // True if a user activity message should be sent. From b63a38fa890b2de9b6b9d9be488ca8c50d314d05 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Mon, 25 May 2026 22:28:56 +0100 Subject: [PATCH 23/51] SystemUI: Remove notification layer-type debounce in ViewState The debouncedSetFaded() helper posted setNotificationFaded() and setLayerType() calls with a 150ms delay via a static Handler/WeakHashMap. This caused hardware layer promotions to lag behind the animation that needs them, producing visible compositing artifacts on notification appear/disappear transitions. Remove the debounce and apply layer-type changes immediately, matching the upstream AOSP behavior. --- .../notification/stack/ViewState.java | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java index 8006657eb9e67..667cb93f7ce7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java @@ -25,8 +25,7 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; -import android.os.Handler; -import android.os.Looper; + import android.util.Log; import android.util.Property; import android.view.View; @@ -50,8 +49,7 @@ import kotlin.jvm.functions.Function1; import java.io.PrintWriter; -import java.util.Map; -import java.util.WeakHashMap; + import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -62,18 +60,7 @@ */ public class ViewState implements Dumpable { - private static final Handler sLayerHandler = new Handler(Looper.getMainLooper()); - private static final long LAYER_DEBOUNCE_MS = 150; - private static final Map sPendingLayerChanges = new WeakHashMap<>(); - private static void debouncedSetFaded(View view, Runnable action) { - Runnable prev = sPendingLayerChanges.get(view); - if (prev != null) { - sLayerHandler.removeCallbacks(prev); - } - sPendingLayerChanges.put(view, action); - sLayerHandler.postDelayed(action, LAYER_DEBOUNCE_MS); - } public ViewState() { this(physicalNotificationMovement()); @@ -354,7 +341,7 @@ public void applyToView(View view) { FadeOptimizedNotification fadeOptimizedView = (FadeOptimizedNotification) view; boolean isFaded = fadeOptimizedView.isNotificationFaded(); if (isFaded != becomesFaded) { - debouncedSetFaded(view, () -> fadeOptimizedView.setNotificationFaded(becomesFaded)); + fadeOptimizedView.setNotificationFaded(becomesFaded); } } else { boolean newLayerTypeIsHardware = becomesFaded && view.hasOverlappingRendering(); @@ -362,7 +349,7 @@ public void applyToView(View view) { int newLayerType = newLayerTypeIsHardware ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE; if (layerType != newLayerType) { - debouncedSetFaded(view, () -> view.setLayerType(newLayerType, null)); + view.setLayerType(newLayerType, null); } } From 18cb8e9075c532d85cb31f4130f876087c571336 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Mon, 25 May 2026 22:29:10 +0100 Subject: [PATCH 24/51] SettingsLib: Fix UsageProgressBarPreference progress animation Each onBindViewHolder() call was creating a new ValueAnimator starting from 0, stacking multiple animators on the same ProgressBar. The underlying LinearProgressIndicator runs its own smooth animator, so the two clocks fought each other and produced visible jitter on rebind. Reuse a single mProgressAnimator instance: cancel the previous one before starting a new animation, start from the last animated value instead of 0, skip the animation when the value is unchanged, and use a material standard easing curve (PathInterpolator 0.4/0/0.2/1) with a 700ms duration. --- .../widget/UsageProgressBarPreference.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java index c2d45e2119d67..74e3e3501f44f 100644 --- a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java +++ b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java @@ -25,6 +25,7 @@ import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; import android.view.View; +import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; @@ -55,6 +56,8 @@ public class UsageProgressBarPreference extends Preference implements GroupSecti private CharSequence mBottomSummaryContentDescription; private ImageView mCustomImageView; private int mPercent = -1; + private int mLastAnimatedPercent = -1; + private ValueAnimator mProgressAnimator; /** * Perform inflation from XML and apply a class-specific base style. @@ -209,7 +212,7 @@ public void onBindViewHolder(PreferenceViewHolder holder) { progressBar.setIndeterminate(true); } else { progressBar.setIndeterminate(false); - animateBatteryLevel(progressBar, 0, mPercent); + animateBatteryLevel(progressBar, mPercent); } } @@ -244,16 +247,26 @@ private CharSequence enlargeFontOfNumber(CharSequence summary) { return summary; } - private void animateBatteryLevel(final ProgressBar progressbar, final int startValue, final int endValue) { - final ValueAnimator animator = ValueAnimator.ofInt(startValue, endValue); - animator.setDuration(1000); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int animatedValue = (int) animation.getAnimatedValue(); - progressbar.setProgress(animatedValue); - } - }); - animator.start(); + private void animateBatteryLevel(final ProgressBar progressbar, final int endValue) { + // Reuse a single animator across rebinds. Stacking ValueAnimator instances on the same + // ProgressBar produces visible jitter because each animator drives setProgress() on its + // own frame cadence and the underlying LinearProgressIndicator runs its own smooth + // animator on top, so the two clocks fight each other. + final int startValue = mLastAnimatedPercent < 0 ? 0 : mLastAnimatedPercent; + if (mProgressAnimator != null) { + mProgressAnimator.cancel(); + } + if (startValue == endValue) { + progressbar.setProgress(endValue, false); + mLastAnimatedPercent = endValue; + return; + } + mProgressAnimator = ValueAnimator.ofInt(startValue, endValue); + mProgressAnimator.setDuration(700L); + mProgressAnimator.setInterpolator(new PathInterpolator(0.4f, 0f, 0.2f, 1f)); + mProgressAnimator.addUpdateListener(animation -> + progressbar.setProgress((int) animation.getAnimatedValue(), false)); + mProgressAnimator.start(); + mLastAnimatedPercent = endValue; } } From 0192d23a2ad15c0ed4ad864622e5a613760e7e09 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Mon, 25 May 2026 22:29:36 +0100 Subject: [PATCH 25/51] view/SystemUI: UI thread performance optimizations Choreographer.getLatestExpectedPresentTimeNanos(): Replace the mInDoFrameCallback gate (only set inside Trace.isTagEnabled, making the cache a no-op outside systrace) with mLastFrameTimeNanos > 0L. mLastFrameTimeNanos is Long.MIN_VALUE until the first doFrame populates mLastVsyncEventData, so the cached path is only taken once the data is valid. Same staleness contract as getFrameDeadline() / getVsyncId(). Eliminates the binder round-trip to SurfaceFlinger on every caller in production after the first frame. ViewRootImpl.handleWindowFocusChanged(): Make mWindowFocusChanged volatile and add a lock-free early-exit check before the synchronized block. handleWindowFocusChanged() is called on every input event delivery path (InputStage.deliver()), so at 240Hz touch the synchronized block was entered on every event even when focus had not changed. The double-check pattern is safe: the volatile read covers the no-op path; the mutation still happens under synchronized. NotificationStackScrollLayout.avoidNotificationOverlaps(): Debounce the O(N) sort + two-pass clipping calculation. Skip the pass when scrollY, expansionFraction, and childCount are all unchanged and no animation is running. Use epsilon comparisons (0.5px scroll, 1e-3 fraction) instead of exact float equality to defeat FPU jitter during steady flings. Track childCount as a cache key to catch silent mutations that bypass requestChildrenUpdate(). Sentinels use Float.NaN so the first call always reaches the body. NotificationStackScrollLayout.getRelativePosition(): Add hasIdentityMatrix() to the direct-child fast-path. Without it, rows animated via setTranslationY() (appear/disappear, swipe-dismiss, heads-up entry) had overlap clipping computed from the static layout position instead of the live visual position, causing micro-flickers mid-animation. Static rows (the common case) still take the fast-path. --- core/java/android/view/Choreographer.java | 11 ++++++ core/java/android/view/ViewRootImpl.java | 6 ++- .../stack/NotificationStackScrollLayout.java | 39 ++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index eb1c6bdbdfe94..81f4d02c1e079 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -873,6 +873,17 @@ public long getExpectedPresentationTimeMillis() { * @hide */ public long getLatestExpectedPresentTimeNanos() { + // mLastVsyncEventData is refreshed at the start of every doFrame + // (see copyFrom() under mLock at the end of the synchronized block in doFrame). + // Once at least one frame has run, the cached value is identical to what the + // IPC would return for the same vsync, so we can skip the binder round-trip on + // the UI thread. Same staleness contract as getFrameDeadline() / getVsyncId(), + // which already trust this field unconditionally. mLastFrameTimeNanos is + // initialized to Long.MIN_VALUE in the constructor and only becomes positive + // once doFrame has populated mLastVsyncEventData via copyFrom(). + if (mLastFrameTimeNanos > 0L) { + return mLastVsyncEventData.preferredFrameTimeline().expectedPresentationTime; + } if (mDisplayEventReceiver == null) { return System.nanoTime(); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9486887f3e01e..9247f5af2b376 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -708,8 +708,7 @@ default void onConfigurationChanged(@NonNull Configuration overrideConfig, // This is used to reduce the race between window focus changes being dispatched from // the window manager and input events coming through the input system. - @GuardedBy("this") - boolean mWindowFocusChanged; + volatile boolean mWindowFocusChanged; @GuardedBy("this") boolean mUpcomingWindowFocus; @GuardedBy("this") @@ -4915,6 +4914,9 @@ private void maybeHandleWindowMove(Rect frame) { } private void handleWindowFocusChanged() { + if (!mWindowFocusChanged) { + return; + } final boolean hasWindowFocus; synchronized (this) { if (!mWindowFocusChanged) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 46386b00c1e42..94ddbcc60c8e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -389,6 +389,13 @@ public boolean onPreDraw() { // Rect of QsHeader. Kept as a field just to avoid creating a new one each time. private final Rect mQsHeaderBound = new Rect(); private boolean mContinuousShadowUpdate; + + private float mLastAvoidOverlapScrollY = Float.NaN; + private float mLastAvoidOverlapExpansionFraction = Float.NaN; + private int mLastAvoidOverlapChildCount = -1; + private static final float AVOID_OVERLAP_SCROLL_EPSILON = 0.5f; + private static final float AVOID_OVERLAP_FRACTION_EPSILON = 1e-3f; + private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> { updateViewShadows(); return true; @@ -1503,6 +1510,25 @@ void avoidNotificationOverlaps() { if (!physicalNotificationMovement()) { return; } + + float scrollY = getOwnScrollY(); + float expansionFraction = mAmbientState.getExpansionFraction(); + int childCount = getChildCount(); + // Epsilon comparisons defeat FPU jitter on otherwise-identical interpolated + // inputs during a steady fling. Tracking childCount as a cache key catches + // silent mutations (add/remove paths that fail to call requestChildrenUpdate()). + if (Math.abs(mLastAvoidOverlapScrollY - scrollY) < AVOID_OVERLAP_SCROLL_EPSILON + && Math.abs(mLastAvoidOverlapExpansionFraction - expansionFraction) + < AVOID_OVERLAP_FRACTION_EPSILON + && mLastAvoidOverlapChildCount == childCount + && !mChildrenUpdateRequested + && !isCurrentlyAnimating()) { + return; + } + mLastAvoidOverlapScrollY = scrollY; + mLastAvoidOverlapExpansionFraction = expansionFraction; + mLastAvoidOverlapChildCount = childCount; + createSortedNotificationLists(mTmpSortedChildren, mTmpNonOverlapChildren); // Now lets update the overlaps for the views, ensuring that we set the values for every // view, otherwise they might get stuck. @@ -1589,6 +1615,18 @@ void avoidNotificationOverlaps() { * getLocationOnScreen[1] however this also works for transientViews. */ private float getRelativePosition(ExpandableView expandableView) { + ViewParent viewParent = expandableView.getParent(); + if (viewParent == this && expandableView.hasIdentityMatrix()) { + // Common steady-state case: direct child with no in-flight transform. + // The layout Y equals the visual Y, so we can skip both the matrix + // mapPoints() call and the parent-chain walk below. The identity-matrix + // guard is required because rows use setTranslationY() heavily during + // appear/disappear, swipe-dismiss, and heads-up entry animations; without + // it the overlap clipping would be computed from the layout position + // instead of the animated visual position. + return expandableView.getTop(); + } + mTempFloat2[0] = 0f; mTempFloat2[1] = 0f; if (!expandableView.hasIdentityMatrix()) { @@ -1597,7 +1635,6 @@ private float getRelativePosition(ExpandableView expandableView) { mTempFloat2[1] += expandableView.getTop(); - ViewParent viewParent = expandableView.getParent(); while (viewParent instanceof View && viewParent != this) { final View view = (View) viewParent; From 88f357589126d9a65f406c04c1799097b94fba40 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Tue, 26 May 2026 00:56:28 +0100 Subject: [PATCH 26/51] axdynamicbar: Fix sports event team-name regression and add live event fallback Fixes: - Rewrote SportsText to mirror the team-name-first logic already present in AxDynamicBarChip: shows [team1] score - score [team2] when both teams are known, or [team1] vs [team2] when no score yet - Added SportsTeamLabel helper: team icon (14dp circular) when available, otherwise a 3-letter uppercase text abbreviation - Added 'Live event' fallback (ax_dynamic_bar_sports_live_event) shown when neither team identifiers nor scores are present in the event - Fixed keyguard chip fallback: score -> team1Name -> 'Live event' priority instead of unconditional score1-score2 - Fixed keyguard chip textKeyFor: guards against empty team names before building the 'Team1 vs Team2' key string --- packages/SystemUI/res/values/cr_strings.xml | 1 + .../ui/compose/AxDynamicBarKeyguardChip.kt | 14 ++++- .../ui/compose/PillIslandContent.kt | 54 ++++++++++++++----- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/packages/SystemUI/res/values/cr_strings.xml b/packages/SystemUI/res/values/cr_strings.xml index 7c30f088ff5c9..6f2d930e270d3 100644 --- a/packages/SystemUI/res/values/cr_strings.xml +++ b/packages/SystemUI/res/values/cr_strings.xml @@ -284,6 +284,7 @@ Final Halftime Upcoming + Live event Screenshot diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index 5d9bc6db0bdb5..64c55c805c222 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -800,7 +800,12 @@ private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modi event.shortText.ifEmpty { event.title.ifEmpty { event.appName } }, color, modifier, ) is IslandEvent.Sports -> MarqueeText( - "${event.score1}-${event.score2}", color, modifier, + when { + event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" + event.team1Name.isNotEmpty() -> event.team1Name + else -> stringResource(R.string.ax_dynamic_bar_sports_live_event) + }, + color, modifier, ) is IslandEvent.NowPlaying -> MarqueeText( "${event.songTitle} · ${event.artist}".trimEnd(' ', '·', ' '), color, modifier, @@ -834,7 +839,12 @@ private fun secondaryTextFor(event: IslandEvent): String? = when (event) { is IslandEvent.AppSwitch -> null is IslandEvent.Notification -> event.text?.take(30) is IslandEvent.PromotedOngoing -> event.text.takeIf { it.isNotBlank() }?.take(20) - is IslandEvent.Sports -> "${event.team1Name} ${stringResource(R.string.ax_dynamic_bar_sports_vs)} ${event.team2Name}" + is IslandEvent.Sports -> when { + event.team1Name.isNotEmpty() || event.team2Name.isNotEmpty() -> + "${event.team1Name} ${stringResource(R.string.ax_dynamic_bar_sports_vs)} ${event.team2Name}" + event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" + else -> stringResource(R.string.ax_dynamic_bar_sports_live_event) + } is IslandEvent.KeyguardIndication -> when (event.indicationType) { IslandEvent.KeyguardIndication.IndicationType.BIOMETRIC -> stringResource(R.string.ax_dynamic_bar_biometric) IslandEvent.KeyguardIndication.IndicationType.TRUST -> stringResource(R.string.ax_dynamic_bar_trust) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index f460a77f4fddc..68b9c82b44847 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -25,9 +25,11 @@ import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -972,26 +974,50 @@ private fun SportsPillIcon(event: IslandEvent.Sports) { @Composable private fun SportsText(event: IslandEvent.Sports, modifier: Modifier, overrideColor: Color? = null) { val color = overrideColor ?: accentColorFor(event) - val scoreText = when { - event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" - event.team2Name.isEmpty() -> event.team1Name - else -> "vs" - } Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - Text(scoreText, color = color, style = PillAccent, maxLines = 1) - event.team2Icon?.let { icon -> - Box(modifier = Modifier.padding(start = 4.dp)) { - Image( - bitmap = icon.toScaledBitmap(14.dp), - contentDescription = null, - modifier = Modifier.size(14.dp).clip(CircleShape), - contentScale = ContentScale.Crop, - ) + if (event.team2Name.isNotEmpty()) { + SportsTeamLabel(event.team1Name, event.team1Icon, color) + Spacer(Modifier.width(SpaceXs)) + Text( + if (event.score1.isNotEmpty()) "${event.score1} - ${event.score2}" + else stringResource(R.string.ax_dynamic_bar_sports_vs), + color = color, + style = PillAccent, + maxLines = 1, + ) + Spacer(Modifier.width(SpaceXs)) + SportsTeamLabel(event.team2Name, event.team2Icon, color) + } else { + val fallback = when { + event.team1Name.isNotEmpty() -> event.team1Name + event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" + else -> stringResource(R.string.ax_dynamic_bar_sports_live_event) } + Text(fallback, color = color, style = PillAccent, maxLines = 1) } } } +@Composable +private fun SportsTeamLabel(name: String, icon: Drawable?, color: Color) { + val badgeSize = 14.dp + if (icon != null) { + Image( + bitmap = icon.toScaledBitmap(badgeSize), + contentDescription = name, + modifier = Modifier.size(badgeSize).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } else { + Text( + name.take(3).uppercase(), + color = color, + style = TsBadge, + maxLines = 1, + ) + } +} + @Composable private fun AppSwitchPillIcon(event: IslandEvent.AppSwitch) { val app = event.previousApp ?: event.recentApps.firstOrNull() From 35d318fa9bf84546080b75767d2149bd133d6f75 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Tue, 26 May 2026 15:14:59 +0100 Subject: [PATCH 27/51] SystemUI/theme: fall back to iconpack for statusbar wifi/signal Two fixes that together restore the expected layering: a wifi or signal overlay wins when active, but disabling it lets an active iconpack provide its own ic_wifi_signal_* / ic_signal_cellular_* drawables instead of skipping straight back to AOSP. ThemeOverlayApplier Wifi/signal RROs declare targetPackage=android, but the category-to- target map pointed them at SystemUI. The disable scan therefore never saw them in OverlayManager listings and could not toggle them off. Point both categories at ANDROID_PACKAGE and add them to that package's category set so the disable pass works correctly. ThemeEngineManagerService Register the active iconpack's resources via loadTargetArrays so the set of names it actually overrides is known to the lookup. Extend getThemePackageForResource to consult that set when no per-category statusbar theme (wifi/signal) is active, so ic_wifi_signal_* and ic_signal_cellular_* drawables resolve to the iconpack instead of falling all the way through to AOSP. --- .../systemui/theme/ThemeOverlayApplier.java | 7 ++++--- .../theme/ThemeEngineManagerService.java | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index 309217ce52bff..627814622907a 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -190,7 +190,8 @@ public ThemeOverlayApplier(OverlayManager overlayManager, OVERLAY_CATEGORY_SYSTEM_PALETTE, OVERLAY_CATEGORY_ACCENT_COLOR, OVERLAY_CATEGORY_DYNAMIC_COLOR, OVERLAY_CATEGORY_FONT, OVERLAY_CATEGORY_SHAPE, - OVERLAY_CATEGORY_ICON_ANDROID)); + OVERLAY_CATEGORY_ICON_ANDROID, + OVERLAY_CATEGORY_ICON_SIGNAL, OVERLAY_CATEGORY_ICON_WIFI)); mTargetPackageToCategories.put(SYSUI_PACKAGE, Sets.newHashSet(OVERLAY_CATEGORY_ICON_SYSUI, OVERLAY_CATEGORY_BACK_GESTURE, OVERLAY_CATEGORY_CHARGING_ANIMATION, OVERLAY_CATEGORY_BATTERY_STYLE)); @@ -206,8 +207,8 @@ public ThemeOverlayApplier(OverlayManager overlayManager, mCategoryToTargetPackage.put(OVERLAY_CATEGORY_CHARGING_ANIMATION, SYSUI_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_BATTERY_STYLE, SYSUI_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SETTINGS, SETTINGS_PACKAGE); - mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SIGNAL, SYSUI_PACKAGE); - mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_WIFI, SYSUI_PACKAGE); + mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SIGNAL, ANDROID_PACKAGE); + mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_WIFI, ANDROID_PACKAGE); dumpManager.registerDumpable(TAG, this); } diff --git a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java index be14c0d91f8eb..a96a0d5d806a4 100644 --- a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java +++ b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java @@ -280,6 +280,12 @@ private synchronized void loadThemeConfig() { if (iconPackPkg != null && (!iconPackPkg.equals(mIconPackPackage) || mIconPackMap.isEmpty())) { loadIconPack(iconPackPkg); + // Register the iconpack's overlaid resources so getThemePackageForResource + // can fall back to it when no per-category statusbar theme (wifi/signal) is + // active. This lets an iconpack contribute its bundled ic_wifi_signal_* / + // ic_signal_cellular_* drawables when the user has not picked a dedicated + // statusbar overlay. + loadTargetArrays(iconPackPkg); } else if (iconPackPkg == null && mIconPackPackage != null) { mIconPackPackage = null; mIconPackMap.clear(); @@ -489,6 +495,19 @@ private String getThemePackageForResource(@NonNull String resourceName) { } } + // Fall back to the active iconpack when a statusbar resource (wifi/signal) + // has no dedicated category theme. Iconpacks that ship ic_wifi_signal_* / + // ic_signal_cellular_* drawables get registered in mTargetArrayCache via + // loadTargetArrays at iconpack load time; consult that set so we don't + // serve from an iconpack that does not actually theme this resource. + String iconPackPkg = mIconPackPackage; + if (iconPackPkg != null) { + Set iconPackTargets = mTargetArrayCache.get(iconPackPkg); + if (iconPackTargets != null && iconPackTargets.contains(resourceName)) { + return iconPackPkg; + } + } + return null; } From ca27677eedd7648940ef59f7457258c47e8f31e8 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Tue, 26 May 2026 22:10:30 +0100 Subject: [PATCH 28/51] core: tune default animation durations for snappier UX Reintroduces the spirit of 5c0a63d9b799 ("android.view: reduce animation speeds") that was dropped to investigate animation jitter, but only for the genuinely animation-timing portion of the patch and with milder reductions that keep every duration above the per-frame floor where ease curves still render cleanly. config_shortAnimTime 200 -> 150 config_mediumAnimTime 400 -> 300 config_longAnimTime 500 -> 350 config_activityShortDur 150 -> 120 config_activityDefaultDur 220 -> 170 The previous halving (200 -> 100, 220 -> 110, etc.) pushed activity and short-anim durations into 6-7 frames at 60 Hz, which the standard fast-out-slow-in / activity transition curves cannot compress without front-loading nearly all of the visible motion into a single frame. That reads as a hop rather than a slide and surfaces every existing pipeline drop. Roughly 25-35 % reductions stay clear of that floor while still feeling meaningfully snappier than stock. Deliberately not picked from the original commit: - ViewConfiguration timing constants (long press / global actions / jump tap / edge slop / zoom controls). None are animation timings; they affect input disambiguation and accessibility behavior, and halving them broke long-press on lists and made accidental global-actions invocations more likely. Long-press snappiness, if desired, belongs in the Settings.Secure.LONG_PRESS_TIMEOUT user preference, not the framework constant. Companion commit 2b8393dd8582 ("speedup window animation scale", which set DEFAULT_ANIMATION_SCALE to 0.7f in WindowManagerService) is dropped entirely: it is almost always inert because SettingsProvider populates window/transition scales at 100 % on first boot, and where it does fire it desyncs Developer Options from the actual scale. If a global default of < 1.0 is wanted later, it should be done coherently across SettingsProvider def_window_animation_scale, def_window_transition_scale, def_animator_duration_scale, and config_appTransitionAnimationDurationScaleDefault. Change-Id: I7e5f5db5a3d6c8a2c1b9d4f0e6c1a2b3c4d5e6f7a Signed-off-by: elpaablo --- core/res/res/values/config.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index a4b5fb010acb6..9b88eb96dc801 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -146,17 +146,17 @@ false - 200 + 150 - 400 + 300 - 500 + 350 - 150 - 220 + 120 + 170 116 From f8d3a5ee906c7d4da9b742ddcfce8f69550608d7 Mon Sep 17 00:00:00 2001 From: elpaablo Date: Fri, 29 May 2026 18:26:57 +0100 Subject: [PATCH 29/51] ThemeEngine: icon-pack overlay support and statusbar fallback fixes - Register icon-pack categoryThemes overlays (icon_pack.*) in ThemeEngineManagerService without requiring a themes.icon_pack entry - Add mIconPackOverlayPackages set; resolve primary pack from android-suffix overlay - registerStatusbarDrawableTargets: probe wifi/signal drawables in all overlay types - resolveIconPackPackageForResource: fall back to icon-pack overlays for statusbar icons - Fix getThemePackageForResource to verify resource is actually in the target set - Add re-entrancy guard on notifyThemeChangedInternal (mBroadcasting flag) - isTargetedResource simplified to delegate to getThemePackageForResource - ThemeIconController: add THEME_ICON_SIZE_SCALE constant (1.1f, not yet wired) --- .../connectivity/ThemeIconController.kt | 6 + .../theme/ThemeEngineManagerService.java | 224 ++++++++++++------ 2 files changed, 162 insertions(+), 68 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt index 7402e7cd7ebfc..19fa499bea4c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt @@ -36,6 +36,12 @@ object ThemeIconController { private const val NEW_WIFI_WIDTH_DP = 17f private const val NEW_WIFI_HEIGHT_DP = 12.58f + /** + * Applied when rendering ThemeEngine wifi/signal bitmaps (dedicated RROs and icon-pack + * fallbacks). Overlay vectors often use tighter padding than stock status-bar art. + */ + private const val THEME_ICON_SIZE_SCALE = 1.1f + private val SIGNAL_4BAR_NAMES = arrayOf( "ic_signal_cellular_0_4_bar", "ic_signal_cellular_1_4_bar", diff --git a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java index a96a0d5d806a4..23dcbbd46f3f7 100644 --- a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java +++ b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java @@ -82,6 +82,26 @@ public class ThemeEngineManagerService extends SystemService { private static final String TARGET_ARRAY_ANDROID = "target_android"; private static final String TARGET_ARRAY_SETTINGS = "target_settings"; + /** Overlay-manager category prefix for built-in icon pack RROs (Theme Store path). */ + private static final String ICON_PACK_OVERLAY_PREFIX = + "android.theme.customization.icon_pack."; + + private static final String OVERLAY_CATEGORY_WIFI_ICON = + "android.theme.customization.wifi_icon"; + private static final String OVERLAY_CATEGORY_SIGNAL_ICON = + "android.theme.customization.signal_icon"; + + private static final String[] STATUSBAR_THEMED_DRAWABLE_NAMES = { + "ic_signal_cellular_0_4_bar", "ic_signal_cellular_1_4_bar", + "ic_signal_cellular_2_4_bar", "ic_signal_cellular_3_4_bar", + "ic_signal_cellular_4_4_bar", + "ic_signal_cellular_0_5_bar", "ic_signal_cellular_1_5_bar", + "ic_signal_cellular_2_5_bar", "ic_signal_cellular_3_5_bar", + "ic_signal_cellular_4_5_bar", "ic_signal_cellular_5_5_bar", + "ic_wifi_signal_0", "ic_wifi_signal_1", "ic_wifi_signal_2", + "ic_wifi_signal_3", "ic_wifi_signal_4", + }; + private static final int BITMAP_CACHE_SIZE = 5 * 1024 * 1024; private final Context mContext; @@ -98,6 +118,8 @@ public class ThemeEngineManagerService extends SystemService { private volatile String mActiveSystemThemeIcons = null; private volatile String mIconPackPackage = null; + /** All icon-pack overlay APKs active in categoryThemes (android, systemui, …). */ + private final Set mIconPackOverlayPackages = new HashSet<>(); private final Map mIconPackMap = new ConcurrentHashMap<>(); private final List mIconBackList = new CopyOnWriteArrayList<>(); @@ -116,6 +138,9 @@ public class ThemeEngineManagerService extends SystemService { private boolean mTextColorPrimaryCached = false; private ContentObserver mSettingsObserver; + /** Guards against re-entrant beginBroadcast() (settings observer + explicit notify). */ + private boolean mBroadcasting = false; + private static final long PERSIST_DEBOUNCE_MS = 500; private final Runnable mPersistPerAppRunnable = this::persistPerAppIconPacksNow; @@ -203,6 +228,7 @@ private synchronized void loadThemeConfig() { mActiveSystemThemeIcons = null; mSystemThemeIconTargets.clear(); mCategoryThemes.clear(); + mIconPackOverlayPackages.clear(); mBitmapCache.evictAll(); mPerAppIconPacks.clear(); mPerAppIconPackMaps.clear(); @@ -255,9 +281,19 @@ private synchronized void loadThemeConfig() { while (catKeys.hasNext()) { String category = catKeys.next(); String pkgName = categoryThemes.optString(category); + if (pkgName == null || pkgName.isEmpty()) { + continue; + } + if (isIconPackOverlayCategory(category)) { + // Theme Store enables icon packs via categoryThemes only (no themes.icon_pack). + mIconPackOverlayPackages.add(pkgName); + mCategoryThemes.put(category, pkgName); + loadTargetArrays(pkgName); + continue; + } boolean isCategoryEnabled = mEnabledThemes.containsKey(category) || mSystemThemeIconTargets.contains(category); - if (pkgName != null && !pkgName.isEmpty() && isCategoryEnabled) { + if (isCategoryEnabled) { mCategoryThemes.put(category, pkgName); loadTargetArrays(pkgName); } @@ -277,6 +313,9 @@ private synchronized void loadThemeConfig() { } String iconPackPkg = mEnabledThemes.get(CATEGORY_ICON_PACK); + if (iconPackPkg == null && !mIconPackOverlayPackages.isEmpty()) { + iconPackPkg = resolvePrimaryIconPackPackage(); + } if (iconPackPkg != null && (!iconPackPkg.equals(mIconPackPackage) || mIconPackMap.isEmpty())) { loadIconPack(iconPackPkg); @@ -329,42 +368,93 @@ private void loadTargetArrays(@NonNull String packageName) { } } - if (allTargets.isEmpty()) { - String overlayCategory = null; - try { - android.content.pm.PackageInfo pi = mContext.getPackageManager() - .getPackageInfo(packageName, 0); - overlayCategory = pi.overlayCategory; - } catch (Exception e) { - Slog.d(TAG, "Failed to get overlay category for " + packageName, e); - } + registerStatusbarDrawableTargets(themeResources, packageName, allTargets); + + mTargetArrayCache.put(packageName, allTargets); + } + + private static boolean isIconPackOverlayCategory(@NonNull String category) { + return category.startsWith(ICON_PACK_OVERLAY_PREFIX); + } - String resCategory = null; - if ("android.theme.customization.signal_icon".equals(overlayCategory)) { - resCategory = "signal"; - } else if ("android.theme.customization.wifi_icon".equals(overlayCategory)) { - resCategory = "wifi"; + /** + * Prefer the android overlay (appfilter + framework wifi/signal drawables), else any member. + */ + @Nullable + private String resolvePrimaryIconPackPackage() { + for (String pkg : mIconPackOverlayPackages) { + if (pkg.endsWith(".android")) { + return pkg; } + } + for (String pkg : mIconPackOverlayPackages) { + return pkg; + } + return null; + } - String[] knownNames = { - "ic_signal_cellular_0_4_bar", "ic_signal_cellular_1_4_bar", - "ic_signal_cellular_2_4_bar", "ic_signal_cellular_3_4_bar", - "ic_signal_cellular_4_4_bar", - "ic_signal_cellular_0_5_bar", "ic_signal_cellular_1_5_bar", - "ic_signal_cellular_2_5_bar", "ic_signal_cellular_3_5_bar", - "ic_signal_cellular_4_5_bar", "ic_signal_cellular_5_5_bar", - "ic_wifi_signal_0", "ic_wifi_signal_1", "ic_wifi_signal_2", - "ic_wifi_signal_3", "ic_wifi_signal_4", - }; - for (String name : knownNames) { - if (themeResources.getIdentifier(name, "drawable", packageName) != 0) { - allTargets.add(name); - if (resCategory != null) mResourceCategoryCache.put(name, resCategory); + /** + * Probe for wifi/signal drawables shipped in icon-pack overlays. Runs even when target_* arrays + * are present so statusbar icons are registered for ThemeEngine fallback. + */ + private void registerStatusbarDrawableTargets(@NonNull Resources themeResources, + @NonNull String packageName, @NonNull Set allTargets) { + String overlayCategory = null; + try { + android.content.pm.PackageInfo pi = mContext.getPackageManager() + .getPackageInfo(packageName, 0); + overlayCategory = pi.overlayCategory; + } catch (Exception e) { + Slog.d(TAG, "Failed to get overlay category for " + packageName, e); + } + + String resCategory = null; + if ("android.theme.customization.signal_icon".equals(overlayCategory)) { + resCategory = CATEGORY_STATUSBAR_SIGNAL; + } else if ("android.theme.customization.wifi_icon".equals(overlayCategory)) { + resCategory = CATEGORY_STATUSBAR_WIFI; + } + + for (String name : STATUSBAR_THEMED_DRAWABLE_NAMES) { + if (themeResources.getIdentifier(name, "drawable", packageName) != 0) { + allTargets.add(name); + if (resCategory != null) { + mResourceCategoryCache.put(name, resCategory); } } } + } - mTargetArrayCache.put(packageName, allTargets); + private boolean packageTargetsResource(@Nullable String packageName, + @NonNull String resourceName) { + if (packageName == null) return false; + Set targets = mTargetArrayCache.get(packageName); + return targets != null && targets.contains(resourceName); + } + + @Nullable + private String resolveIconPackPackageForResource(@NonNull String resourceName) { + for (String pkg : mIconPackOverlayPackages) { + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } + } + String primary = mIconPackPackage; + if (primary != null && packageTargetsResource(primary, resourceName)) { + return primary; + } + return null; + } + + private boolean shouldUseActiveSystemThemeIcons(@Nullable String category) { + if (mActiveSystemThemeIcons == null) return false; + if (category == null || mSystemThemeIconTargets.isEmpty()) { + return true; + } + if (mSystemThemeIconTargets.contains(category)) { + return true; + } + return mSystemThemeIconTargets.contains("statusbar_" + category); } private void loadIconPack(String packageName) { @@ -474,41 +564,47 @@ private String getThemePackageForResource(@NonNull String resourceName) { String category = mResourceCategoryCache.get(resourceName); if (category != null && mCategoryThemes.containsKey(category)) { - return mCategoryThemes.get(category); + String pkg = mCategoryThemes.get(category); + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } } if (category != null) { String aliasCategory = "statusbar_" + category; if (mCategoryThemes.containsKey(aliasCategory)) { - return mCategoryThemes.get(aliasCategory); + String pkg = mCategoryThemes.get(aliasCategory); + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } } - } - - if (mActiveSystemThemeIcons != null) { - if (category == null || mSystemThemeIconTargets.isEmpty() - || mSystemThemeIconTargets.contains(category)) { - return mActiveSystemThemeIcons; + if (CATEGORY_STATUSBAR_WIFI.equals(category) + && mCategoryThemes.containsKey(OVERLAY_CATEGORY_WIFI_ICON)) { + String pkg = mCategoryThemes.get(OVERLAY_CATEGORY_WIFI_ICON); + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } } - if (category != null - && mSystemThemeIconTargets.contains("statusbar_" + category)) { - return mActiveSystemThemeIcons; + if (CATEGORY_STATUSBAR_SIGNAL.equals(category) + && mCategoryThemes.containsKey(OVERLAY_CATEGORY_SIGNAL_ICON)) { + String pkg = mCategoryThemes.get(OVERLAY_CATEGORY_SIGNAL_ICON); + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } } } + if (shouldUseActiveSystemThemeIcons(category) + && packageTargetsResource(mActiveSystemThemeIcons, resourceName)) { + return mActiveSystemThemeIcons; + } + // Fall back to the active iconpack when a statusbar resource (wifi/signal) // has no dedicated category theme. Iconpacks that ship ic_wifi_signal_* / // ic_signal_cellular_* drawables get registered in mTargetArrayCache via // loadTargetArrays at iconpack load time; consult that set so we don't // serve from an iconpack that does not actually theme this resource. - String iconPackPkg = mIconPackPackage; - if (iconPackPkg != null) { - Set iconPackTargets = mTargetArrayCache.get(iconPackPkg); - if (iconPackTargets != null && iconPackTargets.contains(resourceName)) { - return iconPackPkg; - } - } - - return null; + return resolveIconPackPackageForResource(resourceName); } @NonNull @@ -563,7 +659,15 @@ private int getTextColorPrimary() { } private void notifyThemeChangedInternal(@Nullable String category) { + if (mBroadcasting) { + // theme_engine_data can change while callbacks run (e.g. AlphaVisuals sync + + // notifyThemeChanged); defer so RemoteCallbackList is not re-entered. + mHandler.post(() -> notifyThemeChangedInternal(category)); + return; + } + final long ident = Binder.clearCallingIdentity(); + mBroadcasting = true; try { loadThemeConfig(); @@ -584,6 +688,7 @@ private void notifyThemeChangedInternal(@Nullable String category) { mContext.sendBroadcastAsUser(intent, UserHandle.ALL); } } finally { + mBroadcasting = false; Binder.restoreCallingIdentity(ident); } } @@ -774,24 +879,7 @@ public Bitmap getSystemThemeIconDrawable(String resourceName, int density) { public boolean isTargetedResource(String resourceName) { if (resourceName == null) return false; synchronized (ThemeEngineManagerService.this) { - String category = mResourceCategoryCache.get(resourceName); - - if (category != null && mCategoryThemes.containsKey(category)) { - String pkgName = mCategoryThemes.get(category); - Set targets = mTargetArrayCache.get(pkgName); - return targets != null && targets.contains(resourceName); - } - - if (mActiveSystemThemeIcons == null) return false; - - Set targets = mTargetArrayCache.get(mActiveSystemThemeIcons); - if (targets == null || !targets.contains(resourceName)) return false; - - if (category != null && !mSystemThemeIconTargets.isEmpty()) { - return mSystemThemeIconTargets.contains(category); - } - - return !mSystemThemeIconTargets.isEmpty(); + return getThemePackageForResource(resourceName) != null; } } From d75fa308aa91141758289c496be323710682b8ef Mon Sep 17 00:00:00 2001 From: elpaablo Date: Sat, 30 May 2026 11:37:21 +0100 Subject: [PATCH 30/51] SystemUI/ThemeEngine: static QS connectivity icons when icon pack is active - ThemeIconController: add usesStaticQsConnectivityTileIcons(), qsTileFullWifiIconResId(), qsTileFullCellularIconResId() helpers for QS interactor gate - WifiTileDataInteractor: use full-strength ic_wifi_3 for all states when pack active (connected, inactive, pausing, scanning, radio-off); AOSP levels/slash when no pack - MobileDataTileDataInteractor: use ic_mobile_4_4_bar for cellular + data-off states when pack active; SignalDrawable / ic_signal_mobile_data_off when no pack - InternetTileDataInteractor: use ic_mobile_4_4_bar for cellular default, ic_wifi_3 for wifi default when pack active; satellite and ethernet paths unchanged - ThemeEngineManagerService: always register dedicated wifi/signal RRO categories; add hasDedicatedWifiOverlay/hasDedicatedSignalOverlay guards so iconpack fallback never overrides a dedicated statusbar RRO; consolidate alias/category lookup --- .../MobileDataTileDataInteractor.kt | 39 ++++++-- .../interactor/InternetTileDataInteractor.kt | 23 ++++- .../interactor/WifiTileDataInteractor.kt | 37 ++++++- .../connectivity/ThemeIconController.kt | 51 ++++++++++ .../theme/ThemeEngineManagerService.java | 96 +++++++++++++++---- 5 files changed, 213 insertions(+), 33 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt index bc2bfd7c5de6a..add624fa497e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.qs.tiles.base.domain.model.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.cell.domain.model.MobileDataTileIcon import com.android.systemui.qs.tiles.impl.cell.domain.model.MobileDataTileModel import com.android.systemui.res.R +import com.android.systemui.statusbar.connectivity.ThemeIconController import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import javax.inject.Inject @@ -73,17 +74,28 @@ constructor( } else { combine(it.isDataEnabled, it.signalLevelIcon) { isDataEnabled, signalLevelIcon -> + val useStaticIcon = + ThemeIconController.usesStaticQsCellularTileIcon(context) val icon = if (isDataEnabled) { when (signalLevelIcon) { is SignalIconModel.Cellular -> { - val signalState = - SignalDrawable.getState( - signalLevelIcon.level, - signalLevelIcon.numberOfLevels, - signalLevelIcon.showExclamationMark, + if (useStaticIcon) { + MobileDataTileIcon.ResourceIcon( + Icon.Resource( + ThemeIconController.qsTileFullCellularIconResId(), + ContentDescription.Loaded(mobileDataLabel), + ) ) - MobileDataTileIcon.SignalIcon(signalState) + } else { + val signalState = + SignalDrawable.getState( + signalLevelIcon.level, + signalLevelIcon.numberOfLevels, + signalLevelIcon.showExclamationMark, + ) + MobileDataTileIcon.SignalIcon(signalState) + } } is SignalIconModel.Satellite -> { @@ -98,7 +110,11 @@ constructor( } else { MobileDataTileIcon.ResourceIcon( Icon.Resource( - R.drawable.ic_signal_mobile_data_off, + if (useStaticIcon) { + ThemeIconController.qsTileFullCellularIconResId() + } else { + R.drawable.ic_signal_mobile_data_off + }, ContentDescription.Loaded(mobileDataLabel), ) ) @@ -117,7 +133,14 @@ constructor( icon = MobileDataTileIcon.ResourceIcon( Icon.Resource( - R.drawable.ic_signal_mobile_data_off, + if (ThemeIconController.usesStaticQsCellularTileIcon( + context + ) + ) { + ThemeIconController.qsTileFullCellularIconResId() + } else { + R.drawable.ic_signal_mobile_data_off + }, ContentDescription.Loaded(mobileDataLabel), ) ), diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index 01b6a0e6cefd6..43f0a49f024ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileMode import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider +import com.android.systemui.statusbar.connectivity.ThemeIconController import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor @@ -72,10 +73,16 @@ constructor( val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true) if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) { val secondary = removeDoubleQuotes(it.ssid) + val iconResId = + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + wifiIcon.icon.resId + } flowOf( InternetTileModel.Active( secondaryTitle = secondary, - icon = InternetTileIconModel.ResourceId(wifiIcon.icon.resId), + icon = InternetTileIconModel.ResourceId(iconResId), stateDescription = wifiIcon.contentDescription, contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), ) @@ -128,9 +135,21 @@ constructor( dataContentDescription, ) + val icon = + if (ThemeIconController.usesStaticQsCellularTileIcon( + context + ) + ) { + InternetTileIconModel.ResourceId( + ThemeIconController.qsTileFullCellularIconResId() + ) + } else { + InternetTileIconModel.Cellular(signalIcon.level) + } + InternetTileModel.Active( secondaryTitle = secondary, - icon = InternetTileIconModel.Cellular(signalIcon.level), + icon = icon, stateDescription = ContentDescription.Loaded(secondary.toString()), contentDescription = ContentDescription.Loaded(internetLabel), diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt index 6967212568854..7994d69165600 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.qs.tiles.base.domain.model.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.wifi.domain.model.WifiTileModel import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.statusbar.connectivity.ThemeIconController import com.android.systemui.statusbar.connectivity.WifiIcons import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository @@ -70,16 +71,28 @@ constructor( val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true) if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) { val secondary = removeDoubleQuotes(it.ssid) + val iconResId = + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + wifiIcon.icon.resId + } flowOf( WifiTileModel.Active( - icon = WifiTileIconModel(wifiIcon.icon.resId), + icon = WifiTileIconModel(iconResId), secondaryLabel = secondary, ) ) } else { + val iconResId = + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + WifiIcons.WIFI_NO_SIGNAL + } flowOf( WifiTileModel.Inactive( - icon = WifiTileIconModel(WifiIcons.WIFI_NO_SIGNAL), + icon = WifiTileIconModel(iconResId), secondaryLabel = null, ) ) @@ -235,12 +248,24 @@ constructor( if (toggleState == WifiToggleState.Pausing) { return@combine WifiTileModel.Inactive( - icon = WifiTileIconModel(WifiIcons.WIFI_NO_SIGNAL), + icon = WifiTileIconModel( + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + WifiIcons.WIFI_NO_SIGNAL + } + ), secondaryLabel = notConnectedDescription, ) } else if (toggleState == WifiToggleState.Scanning) { return@combine WifiTileModel.Active( - icon = WifiTileIconModel(WifiIcons.WIFI_NO_SIGNAL), + icon = WifiTileIconModel( + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + WifiIcons.WIFI_NO_SIGNAL + } + ), secondaryLabel = context.getString(R.string.quick_settings_scanning_for_wifi), ) } @@ -255,7 +280,9 @@ constructor( WifiTileModel.Inactive( icon = WifiTileIconModel( - if (isEnabled) { + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else if (isEnabled) { WifiIcons.WIFI_NO_SIGNAL } else { R.drawable.ic_signal_wifi_off diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt index 19fa499bea4c2..be66557eaedf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt @@ -112,6 +112,57 @@ object ThemeIconController { return engine.isTargetedResource(SIGNAL_4BAR_NAMES[0]) } + /** + * Returns true when a dedicated wifi RRO or an icon pack with wifi art is active. + * + * Business rules for QS wifi tile icon selection: + * 1. Dedicated wifi RRO active → render themed wifi icon (this gate). + * 2. Icon pack active but no dedicated wifi RRO → render icon-pack wifi art (this gate, + * because icon packs register ic_wifi_signal_* in the android overlay via ThemeEngine). + * 3. Neither active → AOSP path (gate returns false). + * + * Checked independently from [usesStaticQsCellularTileIcon] — a wifi RRO being active + * must not affect the cellular tile, and vice versa. + */ + @JvmStatic + fun usesStaticQsWifiTileIcon(context: Context): Boolean { + return ThemeEngine.getInstance(context)?.isTargetedResource("ic_wifi_signal_4") == true + } + + /** + * Returns true when a dedicated signal RRO or an icon pack with signal art is active. + * + * Business rules for QS cellular tile icon selection: + * 1. Dedicated signal RRO active → render themed cellular icon (this gate). + * 2. Icon pack active but no dedicated signal RRO → render icon-pack signal art (this gate, + * because icon packs register ic_signal_cellular_* in the android overlay via ThemeEngine). + * 3. Neither active → AOSP path (gate returns false). + * + * Checked independently from [usesStaticQsWifiTileIcon] — a signal RRO being active + * must not affect the wifi tile, and vice versa. + */ + @JvmStatic + fun usesStaticQsCellularTileIcon(context: Context): Boolean { + return hasThemedSignalIcons(context) + } + + /** + * Resource ID of the full-strength QS wifi glyph (last entry of [WifiIcons.WIFI_FULL_ICONS]). + * Used by QS interactors when [usesStaticQsWifiTileIcon] is true. + */ + @JvmStatic + fun qsTileFullWifiIconResId(): Int = + com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS[ + com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS.size - 1] + + /** + * Resource ID of the full-strength QS cellular static glyph. + * Used by QS interactors when [usesStaticQsCellularTileIcon] is true. + */ + @JvmStatic + fun qsTileFullCellularIconResId(): Int = + com.android.settingslib.R.drawable.ic_mobile_4_4_bar + @JvmStatic fun getThemedWifiIcon(context: Context, resId: Int): Drawable? { val level = mapWifiResIdToLevel(context, resId) diff --git a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java index 23dcbbd46f3f7..40fd913837133 100644 --- a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java +++ b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java @@ -102,6 +102,9 @@ public class ThemeEngineManagerService extends SystemService { "ic_wifi_signal_3", "ic_wifi_signal_4", }; + private static final String ENGINE_KEY_WIFI = "wifi"; + private static final String ENGINE_KEY_SIGNAL = "signal"; + private static final int BITMAP_CACHE_SIZE = 5 * 1024 * 1024; private final Context mContext; @@ -291,6 +294,15 @@ private synchronized void loadThemeConfig() { loadTargetArrays(pkgName); continue; } + // Dedicated statusbar wifi/signal RROs use overlay-manager category ids; + // themes.wifi uses the short key "wifi" — always register when present in + // categoryThemes so getThemePackageForResource can prefer them over iconpack. + if (OVERLAY_CATEGORY_WIFI_ICON.equals(category) + || OVERLAY_CATEGORY_SIGNAL_ICON.equals(category)) { + mCategoryThemes.put(category, pkgName); + loadTargetArrays(pkgName); + continue; + } boolean isCategoryEnabled = mEnabledThemes.containsKey(category) || mSystemThemeIconTargets.contains(category); if (isCategoryEnabled) { @@ -432,8 +444,64 @@ private boolean packageTargetsResource(@Nullable String packageName, return targets != null && targets.contains(resourceName); } + private boolean hasDedicatedWifiOverlay() { + return mCategoryThemes.containsKey(OVERLAY_CATEGORY_WIFI_ICON) + || mCategoryThemes.containsKey(ENGINE_KEY_WIFI) + || mCategoryThemes.containsKey(CATEGORY_STATUSBAR_WIFI); + } + + private boolean hasDedicatedSignalOverlay() { + return mCategoryThemes.containsKey(OVERLAY_CATEGORY_SIGNAL_ICON) + || mCategoryThemes.containsKey(ENGINE_KEY_SIGNAL) + || mCategoryThemes.containsKey(CATEGORY_STATUSBAR_SIGNAL); + } + + @Nullable + private String getDedicatedStatusbarPackage(@Nullable String category) { + if (category == null) return null; + if (CATEGORY_STATUSBAR_WIFI.equals(category)) { + return firstCategoryThemePackage( + OVERLAY_CATEGORY_WIFI_ICON, + ENGINE_KEY_WIFI, + CATEGORY_STATUSBAR_WIFI); + } + if (CATEGORY_STATUSBAR_SIGNAL.equals(category)) { + return firstCategoryThemePackage( + OVERLAY_CATEGORY_SIGNAL_ICON, + ENGINE_KEY_SIGNAL, + CATEGORY_STATUSBAR_SIGNAL); + } + return null; + } + + @Nullable + private String firstCategoryThemePackage(String... categories) { + for (String category : categories) { + String pkg = mCategoryThemes.get(category); + if (pkg != null && !pkg.isEmpty()) { + return pkg; + } + } + return null; + } + + private static boolean isWifiStatusbarResource(@NonNull String resourceName) { + return resourceName.startsWith("ic_wifi_signal_"); + } + + private static boolean isCellularStatusbarResource(@NonNull String resourceName) { + return resourceName.startsWith("ic_signal_cellular_"); + } + @Nullable private String resolveIconPackPackageForResource(@NonNull String resourceName) { + // Iconpack wifi/signal art is fallback only — never override a dedicated statusbar RRO. + if (isWifiStatusbarResource(resourceName) && hasDedicatedWifiOverlay()) { + return null; + } + if (isCellularStatusbarResource(resourceName) && hasDedicatedSignalOverlay()) { + return null; + } for (String pkg : mIconPackOverlayPackages) { if (packageTargetsResource(pkg, resourceName)) { return pkg; @@ -571,26 +639,18 @@ private String getThemePackageForResource(@NonNull String resourceName) { } if (category != null) { - String aliasCategory = "statusbar_" + category; - if (mCategoryThemes.containsKey(aliasCategory)) { - String pkg = mCategoryThemes.get(aliasCategory); - if (packageTargetsResource(pkg, resourceName)) { - return pkg; - } - } - if (CATEGORY_STATUSBAR_WIFI.equals(category) - && mCategoryThemes.containsKey(OVERLAY_CATEGORY_WIFI_ICON)) { - String pkg = mCategoryThemes.get(OVERLAY_CATEGORY_WIFI_ICON); - if (packageTargetsResource(pkg, resourceName)) { - return pkg; + if (!category.startsWith("statusbar_")) { + String aliasCategory = "statusbar_" + category; + if (mCategoryThemes.containsKey(aliasCategory)) { + String pkg = mCategoryThemes.get(aliasCategory); + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } } } - if (CATEGORY_STATUSBAR_SIGNAL.equals(category) - && mCategoryThemes.containsKey(OVERLAY_CATEGORY_SIGNAL_ICON)) { - String pkg = mCategoryThemes.get(OVERLAY_CATEGORY_SIGNAL_ICON); - if (packageTargetsResource(pkg, resourceName)) { - return pkg; - } + String dedicatedPkg = getDedicatedStatusbarPackage(category); + if (dedicatedPkg != null && packageTargetsResource(dedicatedPkg, resourceName)) { + return dedicatedPkg; } } From a9e9f7f39ec722e28222b1b7b258b198a29f8394 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:28:59 +0100 Subject: [PATCH 31/51] New translations alpha_strings.xml (Romanian) [ci skip] --- core/res/res/values-ro-rRO/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-ro-rRO/alpha_strings.xml b/core/res/res/values-ro-rRO/alpha_strings.xml index 8d6b1e62efeb4..de419dfe94ed6 100644 --- a/core/res/res/values-ro-rRO/alpha_strings.xml +++ b/core/res/res/values-ro-rRO/alpha_strings.xml @@ -17,9 +17,6 @@ Este necesară repornirea interfeței sistemului For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Nu acum - Se repornește interfața sistem pentru a aplica modificările... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From b8e59e69f85e931475ff2d7ce30d3fe444d68e06 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:01 +0100 Subject: [PATCH 32/51] New translations alpha_strings.xml (French) [ci skip] --- core/res/res/values-fr-rFR/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-fr-rFR/alpha_strings.xml b/core/res/res/values-fr-rFR/alpha_strings.xml index 83bbbe8f388ae..409a1b6781521 100644 --- a/core/res/res/values-fr-rFR/alpha_strings.xml +++ b/core/res/res/values-fr-rFR/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Oui - Plus tard - Redémarrer l\'interface système pour appliquer les changements... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 3e222f5e1616a7aef063bd4a5a5c3551e948a784 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:04 +0100 Subject: [PATCH 33/51] New translations alpha_strings.xml (Spanish) [ci skip] --- core/res/res/values-es-rES/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-es-rES/alpha_strings.xml b/core/res/res/values-es-rES/alpha_strings.xml index a1f71638efce8..f1b80fbf8315c 100644 --- a/core/res/res/values-es-rES/alpha_strings.xml +++ b/core/res/res/values-es-rES/alpha_strings.xml @@ -17,9 +17,6 @@ Se requiere el reinicio de la interfaz de usuario del sistema Para que todos los cambios surtan efecto, es necesario reiniciar SystemUI. ¿Reiniciar SystemUI ahora? - Si - Ahora no - Reiniciando interfaz de usuario para aplicar los cambios... Es necesario reiniciar el lanzador Para que todos los cambios surtan efecto, es necesario reiniciar el lanzador. ¿Reiniciar el lanzador ahora? From 6a92384c9ab37f6e33e7b5e49eca21eb1c5b3c22 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:06 +0100 Subject: [PATCH 34/51] New translations alpha_strings.xml (Bulgarian) [ci skip] --- core/res/res/values-bg-rBG/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-bg-rBG/alpha_strings.xml b/core/res/res/values-bg-rBG/alpha_strings.xml index ed7bbcb56202b..409a1b6781521 100644 --- a/core/res/res/values-bg-rBG/alpha_strings.xml +++ b/core/res/res/values-bg-rBG/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Не сега - Рестартиране на launcher-a за прилагане на промените... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 13a32022710398b4c52cc3be8c2872dc6799b2b6 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:09 +0100 Subject: [PATCH 35/51] New translations alpha_strings.xml (Czech) [ci skip] --- core/res/res/values-cs-rCZ/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-cs-rCZ/alpha_strings.xml b/core/res/res/values-cs-rCZ/alpha_strings.xml index 2e454b207ff5a..09ea4c9b0801d 100644 --- a/core/res/res/values-cs-rCZ/alpha_strings.xml +++ b/core/res/res/values-cs-rCZ/alpha_strings.xml @@ -17,9 +17,6 @@ Je vyžadován restart SystemUI For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Teď ne - Restartuje se SystemUI, aby se projevily změny… Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 2abeacbf83b0033363634909dc7c8fce28d9854f Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:11 +0100 Subject: [PATCH 36/51] New translations alpha_strings.xml (German) [ci skip] --- core/res/res/values-de-rDE/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-de-rDE/alpha_strings.xml b/core/res/res/values-de-rDE/alpha_strings.xml index 0e5c81101faaa..f69841e0ff784 100644 --- a/core/res/res/values-de-rDE/alpha_strings.xml +++ b/core/res/res/values-de-rDE/alpha_strings.xml @@ -17,9 +17,6 @@ System-UI-Neustart erforderlich For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Später - Neustart der Systemoberfläche, um Änderungen anzuwenden... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 59e2f694bf48ae884799485a00a94cfb64a71f46 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:14 +0100 Subject: [PATCH 37/51] New translations alpha_strings.xml (Greek) [ci skip] --- core/res/res/values-el-rGR/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-el-rGR/alpha_strings.xml b/core/res/res/values-el-rGR/alpha_strings.xml index ffe157a000e17..409a1b6781521 100644 --- a/core/res/res/values-el-rGR/alpha_strings.xml +++ b/core/res/res/values-el-rGR/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Όχι τώρα - Restarting SystemUI to apply changes... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 4fccce41eb63a8d60a69aab10c85564d6e8dc507 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:16 +0100 Subject: [PATCH 38/51] New translations alpha_strings.xml (Hungarian) [ci skip] --- core/res/res/values-hu-rHU/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-hu-rHU/alpha_strings.xml b/core/res/res/values-hu-rHU/alpha_strings.xml index be0ffaf958013..0f96f28189cf3 100644 --- a/core/res/res/values-hu-rHU/alpha_strings.xml +++ b/core/res/res/values-hu-rHU/alpha_strings.xml @@ -17,9 +17,6 @@ Rendszerfelület-újraindítás szükséges For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Igen - Most nem - Rendszerfelület-újraindítás a változások életbe léptetéséhez... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From b53867ae8484ed75ac647d0f66c8dd8f73606371 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:19 +0100 Subject: [PATCH 39/51] New translations alpha_strings.xml (Italian) [ci skip] --- core/res/res/values-it-rIT/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-it-rIT/alpha_strings.xml b/core/res/res/values-it-rIT/alpha_strings.xml index fc11da48d79fe..75f6b0d9371e2 100644 --- a/core/res/res/values-it-rIT/alpha_strings.xml +++ b/core/res/res/values-it-rIT/alpha_strings.xml @@ -17,9 +17,6 @@ Riavvio SystemUI richiesto For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Non ora - Riavvio SystemUI per applicare i cambiamenti... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From c03d93f26ba64cc65239bc5c886d227c9ff34ede Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:21 +0100 Subject: [PATCH 40/51] New translations alpha_strings.xml (Polish) [ci skip] --- core/res/res/values-pl-rPL/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-pl-rPL/alpha_strings.xml b/core/res/res/values-pl-rPL/alpha_strings.xml index 64c1e731be3fd..89c46c856315b 100644 --- a/core/res/res/values-pl-rPL/alpha_strings.xml +++ b/core/res/res/values-pl-rPL/alpha_strings.xml @@ -17,9 +17,6 @@ Wymagany restart interfejsu systemowego For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Nie teraz - Ponowne uruchamianie interfejsu systemowego, aby zastosować zmiany... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 77b215b0cb74337bbbc648eabd4b433a332f7b32 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:23 +0100 Subject: [PATCH 41/51] New translations alpha_strings.xml (Portuguese) [ci skip] --- core/res/res/values-pt-rPT/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-pt-rPT/alpha_strings.xml b/core/res/res/values-pt-rPT/alpha_strings.xml index 9ed423dc6711f..9b3a4929912d4 100644 --- a/core/res/res/values-pt-rPT/alpha_strings.xml +++ b/core/res/res/values-pt-rPT/alpha_strings.xml @@ -17,9 +17,6 @@ É necessário o reinício da UI do sistema É necessário reiniciar a interface do sistema para que todas as modificações tenham efeito. Reiniciar agora? - Sim - Agora não - A reiniciar a interface do sistema para aplicar as alterações... Reinício do launcher necessário É necessário reiniciar o launcher para que todas as modificações tenham efeito. Reiniciar agora? From 3cc50266d6c6bc224262149657f6745ef1806d63 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:28 +0100 Subject: [PATCH 42/51] New translations alpha_strings.xml (Albanian) [ci skip] --- core/res/res/values-sq-rAL/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-sq-rAL/alpha_strings.xml b/core/res/res/values-sq-rAL/alpha_strings.xml index 30092a5f8f98a..409a1b6781521 100644 --- a/core/res/res/values-sq-rAL/alpha_strings.xml +++ b/core/res/res/values-sq-rAL/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Not now - Restarting SystemUI to apply changes... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 122195189c22e28955fdc134dcbadca191e65eee Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:30 +0100 Subject: [PATCH 43/51] New translations alpha_strings.xml (Albanian) [ci skip] --- .../SpaPrivileged/res/values-sq-rAL/alpha_strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml b/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml index fa42452485f45..5204c1ba9a97e 100644 --- a/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml +++ b/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml @@ -4,6 +4,6 @@ SPDX-License-Identifier: Apache-2.0 --> - Installed: %1$s - Updated: %1$s + Instaluar: %1$s + Përditësuar: %1$s From 4b1bffef47bf1bf76500d655fd848ffda1491c82 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:31 +0100 Subject: [PATCH 44/51] New translations alpha_strings.xml (Turkish) [ci skip] --- core/res/res/values-tr-rTR/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-tr-rTR/alpha_strings.xml b/core/res/res/values-tr-rTR/alpha_strings.xml index 9acdc782c71ee..0df3601103093 100644 --- a/core/res/res/values-tr-rTR/alpha_strings.xml +++ b/core/res/res/values-tr-rTR/alpha_strings.xml @@ -17,9 +17,6 @@ Sistem arayüzünü yeniden başlatmak gerekli Tüm değişikliklerin etkili olabilmesi için, sistem arayüzü yeniden başlatılmalı. Yeniden başlatmak ister misiniz? - Evet - Şimdi değil - Değişiklikleri uygulamak için sistem arayüzü yeniden başlatılıyor... Başlatıcının yeniden başlatılması gerekiyor Tüm değişikliklerin etkili olması için başlatıcının yeniden başlatılması gerekiyor. Başlatıcıyı şimdi yeniden başlatmak ister misiniz? From c9fc164bea1f1741cae933d3a91550716d9b477c Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:33 +0100 Subject: [PATCH 45/51] New translations alpha_strings.xml (Chinese Simplified) [ci skip] --- core/res/res/values-zh-rCN/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-zh-rCN/alpha_strings.xml b/core/res/res/values-zh-rCN/alpha_strings.xml index 0c40991660c9e..c40eae1cbb83e 100644 --- a/core/res/res/values-zh-rCN/alpha_strings.xml +++ b/core/res/res/values-zh-rCN/alpha_strings.xml @@ -17,9 +17,6 @@ 需要重启系统界面 要使所有更改生效,需要重新启动系统界面。现在重新启动系统界面吗? - - 暂不 - 正在重启系统界面以应用更改…… 需要重启系统桌面 要使所有更改生效,需要重新启动桌面。现在重新启动桌面吗? From 9bfd1b8277fbb6eb31437dec01902a10f3458776 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:36 +0100 Subject: [PATCH 46/51] New translations alpha_strings.xml (Chinese Traditional) [ci skip] --- core/res/res/values-zh-rTW/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-zh-rTW/alpha_strings.xml b/core/res/res/values-zh-rTW/alpha_strings.xml index ee1484547d328..7d28f2a1f3c07 100644 --- a/core/res/res/values-zh-rTW/alpha_strings.xml +++ b/core/res/res/values-zh-rTW/alpha_strings.xml @@ -17,9 +17,6 @@ 需要重新啟動系統介面 For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - 暫時不要 - 正在重新啟動系統介面以套用變更... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 0c8ad44e171f1c46badced00e17d3dd62462c995 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:38 +0100 Subject: [PATCH 47/51] New translations alpha_strings.xml (Vietnamese) [ci skip] --- core/res/res/values-vi-rVN/alpha_strings.xml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/core/res/res/values-vi-rVN/alpha_strings.xml b/core/res/res/values-vi-rVN/alpha_strings.xml index 3ef578d28a55a..99f660d1b09de 100644 --- a/core/res/res/values-vi-rVN/alpha_strings.xml +++ b/core/res/res/values-vi-rVN/alpha_strings.xml @@ -7,20 +7,17 @@ An error occured while uploading the log - Digital Wellbeing + Sức khoẻ kỹ thuật số Settings restart required For all changes to take effect, a Settings app restart is required. Restart Settings app now? - System restart required - For all the changes to take effect, a system restart is required. Perform system restart now? + Yêu cầu khởi động lại hệ thống + Để áp dụng tất cả thay đổi, cần khởi động lại hệ thống. Thực hiện khởi động lại ngay? Yêu cầu khởi động lại giao diện hệ thống For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - - Không phải bây giờ - Đang mở lại giao diện hệ thống để áp dụng các thay đổi... - Launcher restart required - For all changes to take effect, a Launcher restart is required. Restart Launcher now? + Yêu cầu khởi động lại Trình khởi chạy + Để tất cả các thay đổi có hiệu lực, cần phải khởi động lại Trình khởi chạy. Khởi động lại Trình khởi chạy ngay bây giờ? From 38637a12fd7970c25e8019b18938637440e05120 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:41 +0100 Subject: [PATCH 48/51] New translations alpha_strings.xml (Portuguese, Brazilian) [ci skip] --- core/res/res/values-pt-rBR/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-pt-rBR/alpha_strings.xml b/core/res/res/values-pt-rBR/alpha_strings.xml index 6c9f8ee125fa5..a027042a7b64c 100644 --- a/core/res/res/values-pt-rBR/alpha_strings.xml +++ b/core/res/res/values-pt-rBR/alpha_strings.xml @@ -17,9 +17,6 @@ É necessário reiniciar a Interface do Sistema Para que todas as alterações entrem em vigor, é necessário reiniciar o SystemUI. Reiniciar o SystemUI agora? - Sim - Agora não - Reiniciando a Interface do Sistema para aplicar as alterações... É necessário reiniciar o Launcher Para que todas as alterações entrem em vigor, é necessário reiniciar o Launcher. Reiniciar o Launcher agora? From 31a35a0b9c49131ca9fb112e9bac9f022ed2947e Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:43 +0100 Subject: [PATCH 49/51] New translations alpha_strings.xml (Indonesian) [ci skip] --- core/res/res/values-in-rID/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-in-rID/alpha_strings.xml b/core/res/res/values-in-rID/alpha_strings.xml index 4a4c00a68dfac..1b3bfcfcdf2f5 100644 --- a/core/res/res/values-in-rID/alpha_strings.xml +++ b/core/res/res/values-in-rID/alpha_strings.xml @@ -17,9 +17,6 @@ Diperlukan mengulang SystemUI Agar semua perubahan bisa terlihat, SystemUI butuh dijalankan ulang. Jalankan ulang SystemUI sekarang? - Ya - Tidak sekarang - Mulai ulang SystemUI untuk menerapkan perubahan... Peluncur butuh dijalankan ulang Agar semua perubahan bisa terlihat, Peluncur butuh dijalankan ulang. Jalankan ulang Peluncur sekarang? From 50367e56c8f9d83b41d9f5851ab50d03a902fe2c Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:46 +0100 Subject: [PATCH 50/51] New translations alpha_strings.xml (Hindi) [ci skip] --- core/res/res/values-hi-rIN/alpha_strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/res/res/values-hi-rIN/alpha_strings.xml b/core/res/res/values-hi-rIN/alpha_strings.xml index 30092a5f8f98a..409a1b6781521 100644 --- a/core/res/res/values-hi-rIN/alpha_strings.xml +++ b/core/res/res/values-hi-rIN/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Not now - Restarting SystemUI to apply changes... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? From 912a00a2dd4f1287da6d8a628c9c3644a8410ab7 Mon Sep 17 00:00:00 2001 From: elpaablo <63260027+elpaablo@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:48 +0100 Subject: [PATCH 51/51] New translations alpha_strings.xml (Hindi) [ci skip] --- .../SpaPrivileged/res/values-hi-rIN/alpha_strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml b/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml index fa42452485f45..42f9c0dc773cc 100644 --- a/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml +++ b/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml @@ -4,6 +4,6 @@ SPDX-License-Identifier: Apache-2.0 --> - Installed: %1$s - Updated: %1$s + स्थापित: %1$s + अपडेट किया गया: %1$s