From c2a07780329834c57c0987d5a31622818ac03060 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:39:54 +0530 Subject: [PATCH 1/5] Add update_system_ui for status and navigation bar colors Implement update_system_ui function for Android UI customization. --- .../recipes/android/src/android/utils.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 pythonforandroid/recipes/android/src/android/utils.py diff --git a/pythonforandroid/recipes/android/src/android/utils.py b/pythonforandroid/recipes/android/src/android/utils.py new file mode 100644 index 0000000000..3dfa732179 --- /dev/null +++ b/pythonforandroid/recipes/android/src/android/utils.py @@ -0,0 +1,150 @@ +from android.runnable import Runnable +from jnius import autoclass, java_method, PythonJavaClass +from typing import Literal + +__all__ = ('update_system_ui') + +def update_system_ui( + status_bar_color: list[float] | str, + navigation_bar_color: list[float] | str, + icon_style: Literal["Light", "Dark"] = "Dark", + pad_status: bool = True, + pad_nav: bool = False, +) -> None: + """ + Provides control of colors for the status and navigation bar and also handle insets padding on Android 15 and above. + + For `status_bar_color` and `navigation_bar_color` either provide a hex color code or rgba (tuple or list) values. + `pad_status` and `pad_nav` will take effect only above Android 15. + IF `icon_style` IS `Dark` THE ICONS WILL BE DARK. + IF `icon_style` IS `Light` THE ICONS WILL BE LIGHT. + + Original code at https://github.com/CarbonKivy/CarbonKivy/blob/39e360314a3885f3b462add4475e6c609b5bef53/carbonkivy/utils.py#L43 (subject to active changes ahead). + """ + + Color = autoclass("android.graphics.Color") + Build_VERSION = autoclass("android.os.Build$VERSION") + WindowInsetsType = autoclass("android.view.WindowInsets$Type") + PythonActivity = autoclass("org.kivy.android.PythonActivity") + View = autoclass("android.view.View") + + activity = PythonActivity.mActivity + window = activity.getWindow() + decor_view = window.getDecorView() + content_view = window.findViewById(autoclass("android.R$id").content) + + try: + WindowCompat = autoclass("androidx.core.view.WindowCompat") + inset_controller = WindowCompat.getInsetsController(window, decor_view) + except Exception as e: + inset_controller = None + + def parse_color(value): + if isinstance(value, str): + return Color.parseColor(value) + elif isinstance(value, (list, tuple)) and len(value) == 4: + r, g, b, a = value + return Color.argb(a, r, g, b) + else: + raise ValueError("Color must be hex string or RGBA tuple") + + def apply_system_bars(): + status_color_int = parse_color(status_bar_color) + navigation_color_int = parse_color(navigation_bar_color) + + # Beleive me, I once drew `dark icons over dark` and `light icons over light` but this won't happen ever again! + if (Build_VERSION.SDK_INT >= 30): + # API 30+ (Android 10+) + if inset_controller and "WindowInsetsControllerCompat" in str(type(inset_controller)): + # Compat wrapper (AndroidX) + # I suggest to include androidx in builds, it actually helps! + if icon_style == "Dark": + inset_controller.setAppearanceLightStatusBars(False) + inset_controller.setAppearanceLightNavigationBars(False) + else: + inset_controller.setAppearanceLightStatusBars(True) + inset_controller.setAppearanceLightNavigationBars(True) + else: + # Platform controller + controller = inset_controller or window.getInsetsController() + WindowInsetsController = autoclass("android.view.WindowInsetsController") + if icon_style == "Dark": + controller.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, + ) + else: + controller.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, + ) + + else: + # Legacy flags for API 23–29 + # Yepp, python3.14 with ndk 28c doesn't support building for android <= 11 with 32 bit armeabi-v7a cpu so this may never be called but who knows?? + visibility_flags = decor_view.getSystemUiVisibility() + + if icon_style == "Dark": + visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + if Build_VERSION.SDK_INT >= 26: + visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + else: + visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + if Build_VERSION.SDK_INT >= 26: + visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + + decor_view.setSystemUiVisibility(visibility_flags) + + # Oops!! android 15+ needs a listener + if Build_VERSION.SDK_INT >= 35: + + class InsetsListener(PythonJavaClass): + __javainterfaces__ = [ + "android/view/View$OnApplyWindowInsetsListener" + ] + __javacontext__ = "app" + + def __init__(self, status_color, navigation_color): + super().__init__() + self.status_color = status_color + self.navigation_color = navigation_color + + @java_method( + "(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;" + ) + def onApplyWindowInsets(self, view, insets): + try: + status_insets = insets.getInsets( + WindowInsetsType.statusBars() + ) + nav_insets = insets.getInsets( + WindowInsetsType.navigationBars() + ) + + top_pad = status_insets.top if pad_status else 0 + bottom_pad = nav_insets.bottom if pad_nav else 0 + + content_view.setPadding(0, top_pad, 0, bottom_pad) + content_view.setBackgroundColor(self.status_color) + + window.setNavigationBarColor(self.navigation_color) + except Exception as e: + print("Insets error:", e) + import traceback + traceback.print_exc() + return insets + + listener = InsetsListener(status_color_int, navigation_color_int) + # I don't know why but sometimes pyjnius failed to find invoke, maybe due to garbage collection and so I made a reference + activity._system_ui_listener = listener + decor_view.setOnApplyWindowInsetsListener(listener) + decor_view.requestApplyInsets() + else: + window.setStatusBarColor(status_color_int) + window.setNavigationBarColor(navigation_color_int) + + # even if it fails it fails in a separate thread + Runnable(apply_system_bars)() From 54c58f0e7c398e43f7df826a8a063025a0b881a7 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:16:54 +0530 Subject: [PATCH 2/5] Update apis.rst --- doc/source/apis.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/source/apis.rst b/doc/source/apis.rst index c9e30699ce..e692109241 100644 --- a/doc/source/apis.rst +++ b/doc/source/apis.rst @@ -5,6 +5,23 @@ Working on Android This page gives details on accessing Android APIs and managing other interactions on Android. +Handling system bars and Edge-to-Edge enforcement +------------------------------------------------- + +**Egde-to-Edge is enforced on all android apis >=35 by default i.e. Android 15 and above.** + +You can control the overall layout and system bars appearance in following ways:: + + from android.utils import update_system_ui + + update_system_ui( + "#0f62fe", # status_bar_color: hex color code or rgba (tuple or list) values + [0.059, 0.384, 0.996, 1.000], # navigation_bar_color: hex color code or rgba (tuple or list) values + "Light", # icon_style: "Dark" means dark icons will be drawn, "Light" means light icons will be drawn, Literal["Dark", or "Light"] + True, # pad_status: Adds a padding to top of content_view, Will take effect on Android 15+ + True, # pad_nav: Adds a padding to bottom of content_view, Will take effect on Android 15+ + ) + Storage paths ------------- From 7f38e644ebf164332ca15fe41c8e4d255d9dfef7 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:36:54 +0530 Subject: [PATCH 3/5] Update utils.py --- pythonforandroid/recipes/android/src/android/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/android/src/android/utils.py b/pythonforandroid/recipes/android/src/android/utils.py index 3dfa732179..e3467e5291 100644 --- a/pythonforandroid/recipes/android/src/android/utils.py +++ b/pythonforandroid/recipes/android/src/android/utils.py @@ -4,6 +4,7 @@ __all__ = ('update_system_ui') + def update_system_ui( status_bar_color: list[float] | str, navigation_bar_color: list[float] | str, @@ -36,7 +37,7 @@ def update_system_ui( try: WindowCompat = autoclass("androidx.core.view.WindowCompat") inset_controller = WindowCompat.getInsetsController(window, decor_view) - except Exception as e: + except: inset_controller = None def parse_color(value): From ea262740531c788735eb9a9fd7a21f092bc9ecb9 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:38:27 +0530 Subject: [PATCH 4/5] Update utils.py --- pythonforandroid/recipes/android/src/android/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/android/src/android/utils.py b/pythonforandroid/recipes/android/src/android/utils.py index e3467e5291..0c85d33612 100644 --- a/pythonforandroid/recipes/android/src/android/utils.py +++ b/pythonforandroid/recipes/android/src/android/utils.py @@ -37,7 +37,7 @@ def update_system_ui( try: WindowCompat = autoclass("androidx.core.view.WindowCompat") inset_controller = WindowCompat.getInsetsController(window, decor_view) - except: + except Exception: inset_controller = None def parse_color(value): From 1ee731c3845b82602cfb667685788bea8b3182b8 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:19:27 +0530 Subject: [PATCH 5/5] Update code quality and readbility --- .../recipes/android/src/android/utils.py | 236 +++++++++--------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/pythonforandroid/recipes/android/src/android/utils.py b/pythonforandroid/recipes/android/src/android/utils.py index 0c85d33612..a4f1c56712 100644 --- a/pythonforandroid/recipes/android/src/android/utils.py +++ b/pythonforandroid/recipes/android/src/android/utils.py @@ -1,10 +1,73 @@ -from android.runnable import Runnable +from android import mActivity +from android.runnable import run_on_ui_thread from jnius import autoclass, java_method, PythonJavaClass from typing import Literal -__all__ = ('update_system_ui') +__all__ = ("update_system_ui") + + +Color = autoclass("android.graphics.Color") +Build_VERSION = autoclass("android.os.Build$VERSION") +WindowInsetsType = autoclass("android.view.WindowInsets$Type") +View = autoclass("android.view.View") +window = mActivity.getWindow() +decor_view = window.getDecorView() +content_view = window.findViewById(autoclass("android.R$id").content) + + +def parse_color(value): + if isinstance(value, str): + return Color.parseColor(value) + elif isinstance(value, (list, tuple)) and len(value) == 4: + r, g, b, a = value + return Color.argb(a, r, g, b) + else: + raise ValueError("Color must be hex string or RGBA tuple") + + +# Oops!! android 15+ needs a listener +if Build_VERSION.SDK_INT >= 35: + + class InsetsListener(PythonJavaClass): + __javainterfaces__ = [ + "android/view/View$OnApplyWindowInsetsListener" + ] + __javacontext__ = "app" + + def __init__(self, status_color, navigation_color, pad_status, pad_nav): + super().__init__() + self.status_color = status_color + self.navigation_color = navigation_color + self.pad_status = pad_status + self.pad_nav = pad_nav + + @java_method( + "(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;" + ) + def onApplyWindowInsets(self, view, insets): + try: + status_insets = insets.getInsets( + WindowInsetsType.statusBars() + ) + nav_insets = insets.getInsets( + WindowInsetsType.navigationBars() + ) + + top_pad = status_insets.top if self.pad_status else 0 + bottom_pad = nav_insets.bottom if self.pad_nav else 0 + + content_view.setPadding(0, top_pad, 0, bottom_pad) + content_view.setBackgroundColor(self.status_color) + window.setNavigationBarColor(self.navigation_color) + except Exception as e: + print("Insets error:", e) + import traceback + traceback.print_exc() + return insets + +@run_on_ui_thread def update_system_ui( status_bar_color: list[float] | str, navigation_bar_color: list[float] | str, @@ -20,132 +83,69 @@ def update_system_ui( IF `icon_style` IS `Dark` THE ICONS WILL BE DARK. IF `icon_style` IS `Light` THE ICONS WILL BE LIGHT. - Original code at https://github.com/CarbonKivy/CarbonKivy/blob/39e360314a3885f3b462add4475e6c609b5bef53/carbonkivy/utils.py#L43 (subject to active changes ahead). + Adapted from https://github.com/CarbonKivy/CarbonKivy/blob/39e360314a3885f3b462add4475e6c609b5bef53/carbonkivy/utils.py#L43 """ - Color = autoclass("android.graphics.Color") - Build_VERSION = autoclass("android.os.Build$VERSION") - WindowInsetsType = autoclass("android.view.WindowInsets$Type") - PythonActivity = autoclass("org.kivy.android.PythonActivity") - View = autoclass("android.view.View") - - activity = PythonActivity.mActivity - window = activity.getWindow() - decor_view = window.getDecorView() - content_view = window.findViewById(autoclass("android.R$id").content) - try: WindowCompat = autoclass("androidx.core.view.WindowCompat") inset_controller = WindowCompat.getInsetsController(window, decor_view) except Exception: inset_controller = None - def parse_color(value): - if isinstance(value, str): - return Color.parseColor(value) - elif isinstance(value, (list, tuple)) and len(value) == 4: - r, g, b, a = value - return Color.argb(a, r, g, b) - else: - raise ValueError("Color must be hex string or RGBA tuple") - - def apply_system_bars(): - status_color_int = parse_color(status_bar_color) - navigation_color_int = parse_color(navigation_bar_color) - - # Beleive me, I once drew `dark icons over dark` and `light icons over light` but this won't happen ever again! - if (Build_VERSION.SDK_INT >= 30): - # API 30+ (Android 10+) - if inset_controller and "WindowInsetsControllerCompat" in str(type(inset_controller)): - # Compat wrapper (AndroidX) - # I suggest to include androidx in builds, it actually helps! - if icon_style == "Dark": - inset_controller.setAppearanceLightStatusBars(False) - inset_controller.setAppearanceLightNavigationBars(False) - else: - inset_controller.setAppearanceLightStatusBars(True) - inset_controller.setAppearanceLightNavigationBars(True) + status_color_int = parse_color(status_bar_color) + navigation_color_int = parse_color(navigation_bar_color) + + # Beleive me, I once drew `dark icons over dark` and `light icons over light` but this won't happen ever again! + if (Build_VERSION.SDK_INT >= 30): + # API 30+ (Android 10+) + if inset_controller and "WindowInsetsControllerCompat" in str(type(inset_controller)): + # Compat wrapper (AndroidX) + # I suggest to include androidx in builds, it actually helps! + if icon_style == "Light": + inset_controller.setAppearanceLightStatusBars(False) + inset_controller.setAppearanceLightNavigationBars(False) else: - # Platform controller - controller = inset_controller or window.getInsetsController() - WindowInsetsController = autoclass("android.view.WindowInsetsController") - if icon_style == "Dark": - controller.setSystemBarsAppearance( - 0, - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS - | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, - ) - else: - controller.setSystemBarsAppearance( - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS - | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS - | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, - ) - + inset_controller.setAppearanceLightStatusBars(True) + inset_controller.setAppearanceLightNavigationBars(True) else: - # Legacy flags for API 23–29 - # Yepp, python3.14 with ndk 28c doesn't support building for android <= 11 with 32 bit armeabi-v7a cpu so this may never be called but who knows?? - visibility_flags = decor_view.getSystemUiVisibility() - - if icon_style == "Dark": - visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - if Build_VERSION.SDK_INT >= 26: - visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + # Platform controller + controller = window.getInsetsController() + WindowInsetsController = autoclass("android.view.WindowInsetsController") + if icon_style == "Light": + controller.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, + ) else: - visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - if Build_VERSION.SDK_INT >= 26: - visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - - decor_view.setSystemUiVisibility(visibility_flags) - - # Oops!! android 15+ needs a listener - if Build_VERSION.SDK_INT >= 35: - - class InsetsListener(PythonJavaClass): - __javainterfaces__ = [ - "android/view/View$OnApplyWindowInsetsListener" - ] - __javacontext__ = "app" - - def __init__(self, status_color, navigation_color): - super().__init__() - self.status_color = status_color - self.navigation_color = navigation_color - - @java_method( - "(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;" + controller.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, ) - def onApplyWindowInsets(self, view, insets): - try: - status_insets = insets.getInsets( - WindowInsetsType.statusBars() - ) - nav_insets = insets.getInsets( - WindowInsetsType.navigationBars() - ) - - top_pad = status_insets.top if pad_status else 0 - bottom_pad = nav_insets.bottom if pad_nav else 0 - - content_view.setPadding(0, top_pad, 0, bottom_pad) - content_view.setBackgroundColor(self.status_color) - - window.setNavigationBarColor(self.navigation_color) - except Exception as e: - print("Insets error:", e) - import traceback - traceback.print_exc() - return insets - - listener = InsetsListener(status_color_int, navigation_color_int) - # I don't know why but sometimes pyjnius failed to find invoke, maybe due to garbage collection and so I made a reference - activity._system_ui_listener = listener - decor_view.setOnApplyWindowInsetsListener(listener) - decor_view.requestApplyInsets() + else: + # Legacy flags for API 23–29 + # Yepp, python3.14 with ndk 28c doesn't support building for android <= 11 with 32 bit armeabi-v7a cpu so this may never be called but who knows?? + visibility_flags = decor_view.getSystemUiVisibility() + + if icon_style == "Light": + visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + if Build_VERSION.SDK_INT >= 26: + visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else: - window.setStatusBarColor(status_color_int) - window.setNavigationBarColor(navigation_color_int) - - # even if it fails it fails in a separate thread - Runnable(apply_system_bars)() + visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + if Build_VERSION.SDK_INT >= 26: + visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + + decor_view.setSystemUiVisibility(visibility_flags) + + if Build_VERSION.SDK_INT >= 35: + listener = InsetsListener(status_color_int, navigation_color_int, pad_status, pad_nav) + # I don't know why but sometimes pyjnius failed to find invoke, maybe due to garbage collection and so I made a reference + mActivity._system_ui_listener = listener + decor_view.setOnApplyWindowInsetsListener(listener) + decor_view.requestApplyInsets() + else: + window.setStatusBarColor(status_color_int) + window.setNavigationBarColor(navigation_color_int)