Skip to content

Commit eec8c0a

Browse files
kengoonkuzeyron
andauthored
Add dark mode listener support across all Android bootstraps (#3306)
* 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. * Refactor `DarkModeListener` to use dynamic activity class namespace and standardize method signature formatting --------- Co-authored-by: Mathias Lindström <8863149+kuzeyron@users.noreply.github.com>
1 parent 7c6e6f3 commit eec8c0a

6 files changed

Lines changed: 179 additions & 0 deletions

File tree

doc/source/apis.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,36 @@ This can be used to prevent errors like:
388388
Because the python function is called from the PythonActivity thread, you
389389
need to be careful about your own calls.
390390

391+
Handling DarkMode
392+
~~~~~~~~~~~~~~~~~
393+
394+
The ``android.darkmode`` module provides functionality to detect and respond to
395+
system dark mode changes on Android devices.
396+
397+
You can set up a listener to monitor dark mode state changes using the
398+
``set_dark_mode_listener`` function::
399+
400+
from android.darkmode import set_dark_mode_listener
401+
402+
def on_dark_mode_changed(is_dark_mode):
403+
if is_dark_mode:
404+
print('Dark mode is now enabled')
405+
# Update your app's theme to dark mode
406+
else:
407+
print('Dark mode is now disabled')
408+
# Update your app's theme to light mode
409+
410+
# Register the listener
411+
set_dark_mode_listener(on_dark_mode_changed)
412+
413+
To remove the listener, simply pass ``None``::
414+
415+
set_dark_mode_listener(None)
416+
417+
The callback function receives a single boolean parameter ``is_dark_mode`` that
418+
indicates whether dark mode is currently enabled (``True``) or disabled (``False``).
419+
420+
391421

392422
Advanced Android API use
393423
------------------------

pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonActivity.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import android.content.Context;
44
import android.content.Intent;
55
import android.content.pm.PackageManager;
6+
import android.content.res.Configuration;
67
import android.os.Bundle;
78
import android.os.PowerManager;
89
import android.os.SystemClock;
@@ -233,4 +234,26 @@ public static void _do_start_service(
233234
public static void stop_service() {
234235
PythonServiceIntent.stop(PythonActivity.mActivity, PythonService.class);
235236
}
237+
238+
public interface DarkModeListener {
239+
void onDarkModeChanged(boolean isDarkMode);
240+
}
241+
242+
private DarkModeListener darkModeListener = null;
243+
244+
public void setDarkModeListener(DarkModeListener listener) {
245+
darkModeListener = listener;
246+
}
247+
248+
@Override
249+
public void onConfigurationChanged(Configuration newConfig) {
250+
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
251+
boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES;
252+
253+
if (darkModeListener != null) {
254+
darkModeListener.onDarkModeChanged(isDarkMode);
255+
}
256+
257+
super.onConfigurationChanged(newConfig);
258+
}
236259
}

pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.content.Intent;
66
import android.content.pm.ActivityInfo;
77
import android.content.pm.PackageManager;
8+
import android.content.res.Configuration;
89
import android.content.res.Resources.NotFoundException;
910
import android.graphics.Bitmap;
1011
import android.graphics.BitmapFactory;
@@ -615,6 +616,28 @@ public void requestPermissions(String[] permissions) {
615616
requestPermissionsWithRequestCode(permissions, 1);
616617
}
617618

619+
public interface DarkModeListener {
620+
void onDarkModeChanged(boolean isDarkMode);
621+
}
622+
623+
private DarkModeListener darkModeListener = null;
624+
625+
public void setDarkModeListener(DarkModeListener listener) {
626+
darkModeListener = listener;
627+
}
628+
629+
@Override
630+
public void onConfigurationChanged(Configuration newConfig) {
631+
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
632+
boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES;
633+
634+
if (darkModeListener != null) {
635+
darkModeListener.onDarkModeChanged(isDarkMode);
636+
}
637+
638+
super.onConfigurationChanged(newConfig);
639+
}
640+
618641
public static void changeKeyboard(int inputType) {
619642
if (SDLActivity.keyboardInputType != inputType) {
620643
SDLActivity.keyboardInputType = inputType;

pythonforandroid/bootstraps/sdl3/build/src/main/java/org/kivy/android/PythonActivity.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.content.Intent;
66
import android.content.pm.ActivityInfo;
77
import android.content.pm.PackageManager;
8+
import android.content.res.Configuration;
89
import android.content.res.Resources.NotFoundException;
910
import android.graphics.Bitmap;
1011
import android.graphics.BitmapFactory;
@@ -614,6 +615,28 @@ public void requestPermissions(String[] permissions) {
614615
requestPermissionsWithRequestCode(permissions, 1);
615616
}
616617

618+
public interface DarkModeListener {
619+
void onDarkModeChanged(boolean isDarkMode);
620+
}
621+
622+
private DarkModeListener darkModeListener = null;
623+
624+
public void setDarkModeListener(DarkModeListener listener) {
625+
darkModeListener = listener;
626+
}
627+
628+
@Override
629+
public void onConfigurationChanged(Configuration newConfig) {
630+
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
631+
boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES;
632+
633+
if (darkModeListener != null) {
634+
darkModeListener.onDarkModeChanged(isDarkMode);
635+
}
636+
637+
super.onConfigurationChanged(newConfig);
638+
}
639+
617640
public static void changeKeyboard(int inputType) {
618641
/*
619642
if (SDLActivity.keyboardInputType != inputType){

pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import android.content.DialogInterface;
77
import android.content.Intent;
88
import android.content.pm.PackageManager;
9+
import android.content.res.Configuration;
910
import android.graphics.Bitmap;
1011
import android.graphics.BitmapFactory;
1112
import android.graphics.Color;
@@ -542,6 +543,28 @@ public void requestPermissionsWithRequestCode(String[] permissions, int requestC
542543
public void requestPermissions(String[] permissions) {
543544
requestPermissionsWithRequestCode(permissions, 1);
544545
}
546+
547+
public interface DarkModeListener {
548+
void onDarkModeChanged(boolean isDarkMode);
549+
}
550+
551+
private DarkModeListener darkModeListener = null;
552+
553+
public void setDarkModeListener(DarkModeListener listener) {
554+
darkModeListener = listener;
555+
}
556+
557+
@Override
558+
public void onConfigurationChanged(Configuration newConfig) {
559+
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
560+
boolean isDarkMode = currentNightMode == Configuration.UI_MODE_NIGHT_YES;
561+
562+
if (darkModeListener != null) {
563+
darkModeListener.onDarkModeChanged(isDarkMode);
564+
}
565+
566+
super.onConfigurationChanged(newConfig);
567+
}
545568
}
546569

547570
class PythonMain implements Runnable {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import Callable
2+
3+
from jnius import PythonJavaClass, java_method, autoclass
4+
from android.config import ACTIVITY_CLASS_NAME, ACTIVITY_CLASS_NAMESPACE
5+
6+
_listener = None
7+
8+
9+
class DarkModeListener(PythonJavaClass):
10+
"""
11+
A listener class for detecting and handling dark mode changes.
12+
13+
This class implements the `DarkModeListener` interface in a Python-Java
14+
hybrid context through Kivy Android functionality. It listens for changes
15+
in the system's dark mode settings and executes a callback upon detecting
16+
a change.
17+
18+
Attributes:
19+
on_dark_mode_changed (Callable[[bool], None]): A callback function to
20+
handle the event when dark mode status changes. The callback
21+
receives a single parameter `is_dark_mode`, which is a boolean
22+
indicating whether dark mode is currently enabled.
23+
"""
24+
__javacontext__ = "app"
25+
__javainterfaces__ = [ACTIVITY_CLASS_NAMESPACE + '$DarkModeListener']
26+
27+
def __init__(self, on_dark_mode_changed: Callable[[bool], None]):
28+
self.on_dark_mode_changed = on_dark_mode_changed
29+
30+
@java_method('(Z)V')
31+
def onDarkModeChanged(self, is_dark_mode):
32+
self.on_dark_mode_changed(is_dark_mode)
33+
34+
35+
def set_dark_mode_listener(on_dark_mode_changed: Callable[[bool], None] | None) -> None:
36+
"""
37+
Sets a listener to monitor changes in the dark mode state.
38+
39+
This function assigns a provided callback to handle changes in the
40+
dark mode settings. The callback will be invoked with a boolean
41+
argument indicating the current dark mode state.
42+
43+
Args:
44+
on_dark_mode_changed: A callable that accepts a single boolean
45+
parameter indicating whether dark mode is active.
46+
47+
Returns:
48+
None
49+
"""
50+
global _listener
51+
activity = autoclass(ACTIVITY_CLASS_NAME).mActivity
52+
if on_dark_mode_changed:
53+
_listener = DarkModeListener(on_dark_mode_changed)
54+
activity.setDarkModeListener(_listener)
55+
else:
56+
activity.setDarkModeListener(on_dark_mode_changed)
57+
_listener = None

0 commit comments

Comments
 (0)