Repository navigation
Routing Broadcast listener to Python
Fabian edited this page Oct 13, 2026
·
12 revisions
To Run your Python code from a Notification Button without opening the app and
without python-osc.
The Java receiver forwards the tap straight to a Python
listener object in the same process.
Side Note: The older Communicate with Service page can do the same job by sending OSC/UDP to the service.
Let's say your service name is Wallpapercarousel and your service file name is wallpaper.py
# buildozer.spec
android.add_src = src
p4a.hook = p4a/hook.py
services = Wallpapercarousel:./wallpaper.py:foreground:foregroundServiceType=specialUse
requirements = python3, kivy, pyjnius, android-notify>=1.60.6
android.permissions = FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE, POST_NOTIFICATIONS- Change
packageto yourbuildozer.spec->"package.domain.package.name".
# p4a/hook.py
from pathlib import Path
from pythonforandroid.toolchain import ToolchainCL
package = "org.wally.waller"
service_name = "Wallpapercarousel"
java_receiver_name = "CarouselReceiver"
def after_apk_build(toolchain: ToolchainCL):
manifest_file = Path(toolchain._dist.dist_dir) / "src" / "main" / "AndroidManifest.xml"
old_manifest = manifest_file.read_text(encoding="utf-8")
receiver_xml = f'''
<receiver android:name="{package}.{java_receiver_name}"
android:process=":service_{service_name}"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="ACTION_SKIP" />
<action android:name="ACTION_STOP" />
</intent-filter>
</receiver>
'''
new_manifest = old_manifest.replace('</application>', f'{receiver_xml}\n</application>')
manifest_file.write_text(new_manifest, encoding="utf-8")
print("Receiver added" if old_manifest != new_manifest else "Failed to add receiver")- Change the first line to your
buildozer.spec->"package.domain.package.name".
// src/CarouselReceiver.java
package org.wally.waller;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class CarouselReceiver extends BroadcastReceiver {
private static final String TAG = "CarouselReceiver";
// Python implements this and hands over a proxy via setListener().
public interface CarouselButtonListener {
void onSkip();
void onStop();
}
private static CarouselButtonListener listener;
public static void setListener(CarouselButtonListener l) {
listener = l;
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
Log.d(TAG, "Action received: " + action);
final CarouselButtonListener current = listener;
if (current == null) {
Log.w(TAG, "No Python listener registered");
return;
}
switch (action) {
case "ACTION_STOP":
current.onStop();
break;
case "ACTION_SKIP":
current.onSkip();
break;
default:
Log.w(TAG, "Unknown action from notification: " + action);
}
}
}- Use
get_package_name()instead of typing the package, it returns the package the app was actually built with to avoid typing errors. - Keep a strong reference to the listener (a module global). A jnius proxy that only Java holds can be garbage-collected and the calls silently stop.
# ./wallpaper.py
import time
from jnius import PythonJavaClass, java_method, autoclass
from android_notify import Notification
from android_notify.config import get_package_name, get_python_service
from android_notify.internal.java_classes import BuildVersion
java_receiver_name = "CarouselReceiver"
_package = get_package_name()
_listener = None # strong ref
class CarouselButtonListener(PythonJavaClass):
__javainterfaces__ = [f'{_package.replace(".", "/")}/{java_receiver_name}$CarouselButtonListener']
__javacontext__ = 'app'
@java_method('()V')
def onSkip(self):
notification.updateMessage("Python got: SKIP")
@java_method('()V')
def onStop(self):
notification.updateMessage("Python got: STOP")
def register_carousel_button_listener():
global _listener
CarouselReceiver = autoclass(f'{_package}.{java_receiver_name}')
_listener = CarouselButtonListener()
CarouselReceiver.setListener(_listener)
# Register it **from inside the service**, before you show the notification:
register_carousel_button_listener()
notification = Notification(title="Carousel")
notification.addButton(text="Skip", receiver_name=java_receiver_name, action="ACTION_SKIP")
notification.addButton(text="Stop", receiver_name=java_receiver_name, action="ACTION_STOP")
builder = notification.fill_args()
foreground_type = autoclass("android.content.pm.ServiceInfo").FOREGROUND_SERVICE_TYPE_SPECIAL_USE if BuildVersion.SDK_INT >= 34 else 0
service = get_python_service()
service.startForeground(notification.id, builder.build(), foreground_type)
fmt = lambda s: f"{int(s//3600)}h {int((s%3600)//60)}m {int(s%60)}s"
start = time.time()
END_TIME = 6 * 3600
while True:
elapsed = time.time() - start
if elapsed >= END_TIME:
notification.updateTitle(f"Total runtime {fmt(elapsed)}")
break
notification.updateTitle(f"Running for {fmt(elapsed)}")
time.sleep(1)receiver_name resolves to <package>.CarouselReceiver, so the Java class name must match.
- Buttons grayed out? You forgot the
p4a.hookstep that editsAndroidManifest.xml. - Nothing in Python? The tap was dropped. Check logcat for
No Python listener registered(listener not set yet, or the receiver is running in a different process than your listener — addandroid:process). -
ClassNotFoundException? The Javapackageline andreceiver_name/get_package_name()must all agree.