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 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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) From 36f4936301ad82c84e3ec3c9d5113597ff57aaa1 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sun, 15 Mar 2026 12:04:27 +0530 Subject: [PATCH 06/13] Create a _global_listener to prevent garbage collection --- pythonforandroid/recipes/android/src/android/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pythonforandroid/recipes/android/src/android/utils.py b/pythonforandroid/recipes/android/src/android/utils.py index a4f1c56712..d187182d62 100644 --- a/pythonforandroid/recipes/android/src/android/utils.py +++ b/pythonforandroid/recipes/android/src/android/utils.py @@ -66,6 +66,7 @@ def onApplyWindowInsets(self, view, insets): traceback.print_exc() return insets + _global_listener: InsetsListener @run_on_ui_thread def update_system_ui( @@ -141,10 +142,10 @@ def update_system_ui( 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) + global _global_listener + _global_listener = InsetsListener(status_color_int, navigation_color_int, pad_status, pad_nav) + decor_view.setOnApplyWindowInsetsListener(_global_listener) decor_view.requestApplyInsets() else: window.setStatusBarColor(status_color_int) From f4008d060c8718b08cf6ec2abfdf7950c26b2f56 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sun, 15 Mar 2026 12:32:51 +0530 Subject: [PATCH 07/13] Update utils.py --- pythonforandroid/recipes/android/src/android/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pythonforandroid/recipes/android/src/android/utils.py b/pythonforandroid/recipes/android/src/android/utils.py index d187182d62..37b95bc18c 100644 --- a/pythonforandroid/recipes/android/src/android/utils.py +++ b/pythonforandroid/recipes/android/src/android/utils.py @@ -68,6 +68,7 @@ def onApplyWindowInsets(self, view, insets): _global_listener: InsetsListener + @run_on_ui_thread def update_system_ui( status_bar_color: list[float] | str, From 3c07c54463932a18731299be53b927650d1f1e9c Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Wed, 6 May 2026 18:07:40 +0530 Subject: [PATCH 08/13] Set _global_listener to None in utils.py Initialize _global_listener to None. --- 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 37b95bc18c..c52ee70ed0 100644 --- a/pythonforandroid/recipes/android/src/android/utils.py +++ b/pythonforandroid/recipes/android/src/android/utils.py @@ -66,7 +66,7 @@ def onApplyWindowInsets(self, view, insets): traceback.print_exc() return insets - _global_listener: InsetsListener + _global_listener: InsetsListener = None @run_on_ui_thread From 222f06c3681d2a5f73c8ac81b7bf2b89835170e8 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sat, 30 May 2026 15:32:55 +0530 Subject: [PATCH 09/13] Move existing logic to display_cutout --- .../android/src/android/display_cutout.py | 237 ++++++++++++++++-- .../recipes/android/src/android/utils.py | 153 ----------- 2 files changed, 214 insertions(+), 176 deletions(-) delete mode 100644 pythonforandroid/recipes/android/src/android/utils.py diff --git a/pythonforandroid/recipes/android/src/android/display_cutout.py b/pythonforandroid/recipes/android/src/android/display_cutout.py index a52868502d..076efa9503 100644 --- a/pythonforandroid/recipes/android/src/android/display_cutout.py +++ b/pythonforandroid/recipes/android/src/android/display_cutout.py @@ -1,11 +1,77 @@ -from jnius import autoclass +from android import mActivity +from android.runnable import run_on_ui_thread +from jnius import autoclass, java_method, PythonJavaClass +from typing import Literal + from kivy.core.window import Window -from android import mActivity +__all__ = ( + "get_cutout_pos", + "get_cutout_size", + "get_width_of_bar", + "get_height_of_bar", + "get_size_of_bar", + "get_width_of_bar", + "get_cutout_mode", + "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 -__all__ = ('get_cutout_pos', 'get_cutout_size', 'get_width_of_bar', - 'get_height_of_bar', 'get_size_of_bar', 'get_width_of_bar', - 'get_cutout_mode') + @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 + + _global_listener: InsetsListener = None def _core_cutout(): @@ -17,7 +83,7 @@ def _core_cutout(): def get_cutout_pos(): """Get position of the display-cutout. - Returns integer for each positions (xy) + Returns integer for each positions (xy) """ try: cutout = _core_cutout() @@ -29,36 +95,37 @@ def get_cutout_pos(): def get_cutout_size(): """Get the size (xy) of the front camera. - Returns size with float values + Returns size with float values """ try: cutout = _core_cutout() return float(cutout.width()), float(cutout.height()) except Exception: # Doesn't have a camera builtin with the display - return 0., 0. + return 0.0, 0.0 def get_height_of_bar(bar_target=None): """Get the height of either statusbar or navigationbar - bar_target = status or navigation and defaults to status + bar_target = status or navigation and defaults to status """ - bar_target = bar_target or 'status' + bar_target = bar_target or "status" - if bar_target not in ('status', 'navigation'): + if bar_target not in ("status", "navigation"): raise Exception("bar_target must be 'status' or 'navigation'") try: - displayMetrics = autoclass('android.util.DisplayMetrics') + displayMetrics = autoclass("android.util.DisplayMetrics") mActivity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics()) resources = mActivity.getResources() - resourceId = resources.getIdentifier(f'{bar_target}_bar_height', 'dimen', - 'android') + resourceId = resources.getIdentifier( + f"{bar_target}_bar_height", "dimen", "android" + ) return float(max(resources.getDimensionPixelSize(resourceId), 0)) except Exception: # Getting the size is not supported on older Androids - return 0. + return 0.0 def get_width_of_bar(bar_target=None): @@ -68,32 +135,156 @@ def get_width_of_bar(bar_target=None): def get_size_of_bar(bar_target=None): """Get the size of either statusbar or navigationbar - bar_target = status or navigation and defaults to status + bar_target = status or navigation and defaults to status """ return get_width_of_bar(), get_height_of_bar(bar_target) def get_heights_of_both_bars(): """Return heights of both bars""" - return get_height_of_bar('status'), get_height_of_bar('navigation') + return get_height_of_bar("status"), get_height_of_bar("navigation") def get_cutout_mode(): """Return mode for cutout supported applications""" - BuildVersion = autoclass('android.os.Build$VERSION') + BuildVersion = autoclass("android.os.Build$VERSION") cutout_modes = {} if BuildVersion.SDK_INT >= 28: - LayoutParams = autoclass('android.view.WindowManager$LayoutParams') + LayoutParams = autoclass("android.view.WindowManager$LayoutParams") window = mActivity.getWindow() layout_params = window.getAttributes() cutout_mode = layout_params.layoutInDisplayCutoutMode - cutout_modes.update({LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT: 'default', - LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES: 'shortEdges'}) + cutout_modes.update( + { + LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT: "default", + LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES: "shortEdges", + } + ) if BuildVersion.SDK_INT >= 30: - cutout_modes[LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS] = 'always' + cutout_modes[LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS] = "always" - return cutout_modes.get(cutout_mode, 'never') + return cutout_modes.get(cutout_mode, "never") return None + + +@run_on_ui_thread +def set_immersive_mode(hide_nav=True, hide_status=True, disable_contrast=True): + """ + Configures Android UI visibility, optionally hiding the navigation and status bars, + and optionally disabling system bar contrast on Android Q (API 29+). + """ + if not mActivity: + return + + window = mActivity.getWindow() + decor_view = window.getDecorView() + + ui_flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + + if hide_nav: + ui_flags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + ui_flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + + if hide_status: + ui_flags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + ui_flags |= View.SYSTEM_UI_FLAG_FULLSCREEN + + if hide_nav or hide_status: + ui_flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + + decor_view.setSystemUiVisibility(ui_flags) + + if disable_contrast and Build_VERSION.SDK_INT >= 29: + window.setNavigationBarContrastEnforced(False) + window.setStatusBarContrastEnforced(False) + + +@run_on_ui_thread +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. + + Adapted from https://github.com/CarbonKivy/CarbonKivy/blob/39e360314a3885f3b462add4475e6c609b5bef53/carbonkivy/utils.py#L43 + """ + + try: + WindowCompat = autoclass("androidx.core.view.WindowCompat") + inset_controller = WindowCompat.getInsetsController(window, decor_view) + except Exception: + inset_controller = None + + 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: + inset_controller.setAppearanceLightStatusBars(True) + inset_controller.setAppearanceLightNavigationBars(True) + else: + # 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: + 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 == "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: + 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: + # I don't know why but sometimes pyjnius failed to find invoke, maybe due to garbage collection and so I made a reference + global _global_listener + _global_listener = InsetsListener( + status_color_int, navigation_color_int, pad_status, pad_nav + ) + decor_view.setOnApplyWindowInsetsListener(_global_listener) + decor_view.requestApplyInsets() + else: + window.setStatusBarColor(status_color_int) + window.setNavigationBarColor(navigation_color_int) diff --git a/pythonforandroid/recipes/android/src/android/utils.py b/pythonforandroid/recipes/android/src/android/utils.py deleted file mode 100644 index c52ee70ed0..0000000000 --- a/pythonforandroid/recipes/android/src/android/utils.py +++ /dev/null @@ -1,153 +0,0 @@ -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") - - -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 - - _global_listener: InsetsListener = None - - -@run_on_ui_thread -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. - - Adapted from https://github.com/CarbonKivy/CarbonKivy/blob/39e360314a3885f3b462add4475e6c609b5bef53/carbonkivy/utils.py#L43 - """ - - try: - WindowCompat = autoclass("androidx.core.view.WindowCompat") - inset_controller = WindowCompat.getInsetsController(window, decor_view) - except Exception: - inset_controller = None - - 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: - inset_controller.setAppearanceLightStatusBars(True) - inset_controller.setAppearanceLightNavigationBars(True) - else: - # 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: - 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 == "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: - 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: - # I don't know why but sometimes pyjnius failed to find invoke, maybe due to garbage collection and so I made a reference - global _global_listener - _global_listener = InsetsListener(status_color_int, navigation_color_int, pad_status, pad_nav) - decor_view.setOnApplyWindowInsetsListener(_global_listener) - decor_view.requestApplyInsets() - else: - window.setStatusBarColor(status_color_int) - window.setNavigationBarColor(navigation_color_int) From 83e236eb7479d33d0fdef741fc7e1816b656bf01 Mon Sep 17 00:00:00 2001 From: Kartavya Shukla <87070473+Novfensec@users.noreply.github.com> Date: Sat, 30 May 2026 15:33:07 +0530 Subject: [PATCH 10/13] Bug fixes --- .../bootstraps/_sdl_common/build/templates/strings.tmpl.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/bootstraps/_sdl_common/build/templates/strings.tmpl.xml b/pythonforandroid/bootstraps/_sdl_common/build/templates/strings.tmpl.xml index 17e376adbd..c10266e039 100644 --- a/pythonforandroid/bootstraps/_sdl_common/build/templates/strings.tmpl.xml +++ b/pythonforandroid/bootstraps/_sdl_common/build/templates/strings.tmpl.xml @@ -3,7 +3,7 @@