Skip to content

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.

Point to needed files

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

Add the receiver in p4a.hook

  • Change package to your buildozer.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")

Broadcast Listener

  • 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);
    }

    }
}

Python Listener

  • 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.

Help

  • Buttons grayed out? You forgot the p4a.hook step that edits AndroidManifest.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 — add android:process).
  • ClassNotFoundException? The Java package line and receiver_name/get_package_name() must all agree.

Working Sample App: wallpaper carousel app

Clone this wiki locally