From c58d1955cb94bf9084610dc019644ebe9cc49990 Mon Sep 17 00:00:00 2001 From: Kenechukwu Akubue Date: Sun, 26 Apr 2026 20:14:53 +0100 Subject: [PATCH 1/2] Add dark mode listener support across all Android bootstraps Introduce `DarkModeListener` interface to detect and handle system dark mode changes. Update Java and Python APIs to support listener registration for monitoring and reacting to dark mode state changes. Document usage in APIs. --- doc/source/apis.rst | 30 ++++++++++ .../java/org/kivy/android/PythonActivity.java | 23 ++++++++ .../java/org/kivy/android/PythonActivity.java | 23 ++++++++ .../java/org/kivy/android/PythonActivity.java | 23 ++++++++ .../java/org/kivy/android/PythonActivity.java | 23 ++++++++ .../recipes/android/src/android/darkmode.py | 57 +++++++++++++++++++ 6 files changed, 179 insertions(+) create mode 100644 pythonforandroid/recipes/android/src/android/darkmode.py diff --git a/doc/source/apis.rst b/doc/source/apis.rst index c9e30699ce..0f139c5192 100644 --- a/doc/source/apis.rst +++ b/doc/source/apis.rst @@ -388,6 +388,36 @@ This can be used to prevent errors like: Because the python function is called from the PythonActivity thread, you need to be careful about your own calls. +Handling DarkMode +~~~~~~~~~~~~~~~~~ + +The ``android.darkmode`` module provides functionality to detect and respond to +system dark mode changes on Android devices. + +You can set up a listener to monitor dark mode state changes using the +``set_dark_mode_listener`` function:: + + from android.darkmode import set_dark_mode_listener + + def on_dark_mode_changed(is_dark_mode): + if is_dark_mode: + print('Dark mode is now enabled') + # Update your app's theme to dark mode + else: + print('Dark mode is now disabled') + # Update your app's theme to light mode + + # Register the listener + set_dark_mode_listener(on_dark_mode_changed) + +To remove the listener, simply pass ``None``:: + + set_dark_mode_listener(None) + +The callback function receives a single boolean parameter ``is_dark_mode`` that +indicates whether dark mode is currently enabled (``True``) or disabled (``False``). + + Advanced Android API use ------------------------ diff --git a/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonActivity.java index 01bdd96805..bffe3cef71 100644 --- a/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonActivity.java @@ -3,6 +3,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Bundle; import android.os.PowerManager; import android.os.SystemClock; @@ -238,4 +239,26 @@ public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } + + public interface DarkModeListener { + void onDarkModeChanged(boolean isDarkMode); + } + + private DarkModeListener darkModeListener = null; + + public void setDarkModeListener(DarkModeListener listener) { + darkModeListener = listener; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES; + + if (darkModeListener != null) { + darkModeListener.onDarkModeChanged(isDarkMode); + } + + super.onConfigurationChanged(newConfig); + } } diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java index 04d3eee30a..4b57f0ca8a 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -620,6 +621,28 @@ public void requestPermissions(String[] permissions) { requestPermissionsWithRequestCode(permissions, 1); } + public interface DarkModeListener { + void onDarkModeChanged(boolean isDarkMode); + } + + private DarkModeListener darkModeListener = null; + + public void setDarkModeListener(DarkModeListener listener) { + darkModeListener = listener; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES; + + if (darkModeListener != null) { + darkModeListener.onDarkModeChanged(isDarkMode); + } + + super.onConfigurationChanged(newConfig); + } + public static void changeKeyboard(int inputType) { if (SDLActivity.keyboardInputType != inputType) { SDLActivity.keyboardInputType = inputType; diff --git a/pythonforandroid/bootstraps/sdl3/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl3/build/src/main/java/org/kivy/android/PythonActivity.java index 8feed58ee4..34bb10236a 100644 --- a/pythonforandroid/bootstraps/sdl3/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl3/build/src/main/java/org/kivy/android/PythonActivity.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -619,6 +620,28 @@ public void requestPermissions(String[] permissions) { requestPermissionsWithRequestCode(permissions, 1); } + public interface DarkModeListener { + void onDarkModeChanged(boolean isDarkMode); + } + + private DarkModeListener darkModeListener = null; + + public void setDarkModeListener(DarkModeListener listener) { + darkModeListener = listener; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES; + + if (darkModeListener != null) { + darkModeListener.onDarkModeChanged(isDarkMode); + } + + super.onConfigurationChanged(newConfig); + } + public static void changeKeyboard(int inputType) { /* if (SDLActivity.keyboardInputType != inputType){ diff --git a/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java index 9ad9503a6f..55f1e2e3a4 100644 --- a/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; @@ -547,6 +548,28 @@ public void requestPermissionsWithRequestCode(String[] permissions, int requestC public void requestPermissions(String[] permissions) { requestPermissionsWithRequestCode(permissions, 1); } + + public interface DarkModeListener { + void onDarkModeChanged(boolean isDarkMode); + } + + private DarkModeListener darkModeListener = null; + + public void setDarkModeListener(DarkModeListener listener) { + darkModeListener = listener; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; + boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES; + + if (darkModeListener != null) { + darkModeListener.onDarkModeChanged(isDarkMode); + } + + super.onConfigurationChanged(newConfig); + } } class PythonMain implements Runnable { diff --git a/pythonforandroid/recipes/android/src/android/darkmode.py b/pythonforandroid/recipes/android/src/android/darkmode.py new file mode 100644 index 0000000000..4765f3ec6f --- /dev/null +++ b/pythonforandroid/recipes/android/src/android/darkmode.py @@ -0,0 +1,57 @@ +from typing import Callable + +from jnius import PythonJavaClass, java_method, autoclass +from android.config import ACTIVITY_CLASS_NAME, ACTIVITY_CLASS_NAMESPACE + +_listener = None + + +class DarkModeListener(PythonJavaClass): + """ + A listener class for detecting and handling dark mode changes. + + This class implements the `DarkModeListener` interface in a Python-Java + hybrid context through Kivy Android functionality. It listens for changes + in the system's dark mode settings and executes a callback upon detecting + a change. + + Attributes: + on_dark_mode_changed (Callable[[bool], None]): A callback function to + handle the event when dark mode status changes. The callback + receives a single parameter `is_dark_mode`, which is a boolean + indicating whether dark mode is currently enabled. + """ + __javacontext__ = "app" + __javainterfaces__ = ["org/kivy/android/PythonActivity$DarkModeListener"] + + def __init__(self, on_dark_mode_changed: Callable[[bool], None]): + self.on_dark_mode_changed = on_dark_mode_changed + + @java_method("(Z)V") + def onDarkModeChanged(self, is_dark_mode): + self.on_dark_mode_changed(is_dark_mode) + + +def set_dark_mode_listener(on_dark_mode_changed: Callable[[bool], None] | None) -> None: + """ + Sets a listener to monitor changes in the dark mode state. + + This function assigns a provided callback to handle changes in the + dark mode settings. The callback will be invoked with a boolean + argument indicating the current dark mode state. + + Args: + on_dark_mode_changed: A callable that accepts a single boolean + parameter indicating whether dark mode is active. + + Returns: + None + """ + global _listener + activity = autoclass(ACTIVITY_CLASS_NAME).mActivity + if on_dark_mode_changed: + _listener = DarkModeListener(on_dark_mode_changed) + activity.setDarkModeListener(_listener) + else: + activity.setDarkModeListener(on_dark_mode_changed) + _listener = None From 27460514a2bb4f586eb16435a7b4c25b4de61629 Mon Sep 17 00:00:00 2001 From: Kenechukwu Akubue Date: Sun, 26 Apr 2026 20:28:39 +0100 Subject: [PATCH 2/2] Refactor `DarkModeListener` to use dynamic activity class namespace and standardize method signature formatting --- pythonforandroid/recipes/android/src/android/darkmode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/recipes/android/src/android/darkmode.py b/pythonforandroid/recipes/android/src/android/darkmode.py index 4765f3ec6f..6e06662de8 100644 --- a/pythonforandroid/recipes/android/src/android/darkmode.py +++ b/pythonforandroid/recipes/android/src/android/darkmode.py @@ -22,12 +22,12 @@ class DarkModeListener(PythonJavaClass): indicating whether dark mode is currently enabled. """ __javacontext__ = "app" - __javainterfaces__ = ["org/kivy/android/PythonActivity$DarkModeListener"] + __javainterfaces__ = [ACTIVITY_CLASS_NAMESPACE + '$DarkModeListener'] def __init__(self, on_dark_mode_changed: Callable[[bool], None]): self.on_dark_mode_changed = on_dark_mode_changed - @java_method("(Z)V") + @java_method('(Z)V') def onDarkModeChanged(self, is_dark_mode): self.on_dark_mode_changed(is_dark_mode)