Skip to content

How to Use Buttons to Communicate with Service

Fabian edited this page Mar 17, 2026 · 4 revisions

For Communicating with Service from notification Buttons Using python-osc

Get Permissions and Point to needed files

# buildozer.spec
android.add_src = src
p4a.hook = p4a/hook.py
services = Wallpapercarousel:./services/wallpaper.py:foreground
requirements = python3, kivy, pyjnius, android_notify==1.60.10, python-osc
android.gradle_dependencies = androidx.core:core-ktx:1.12.0
android.permissions = INTERNET, POST_NOTIFICATIONS

Insert receiver and service type to XML through p4a.hook

  • Change package variable with values in your buildozer.spec -> "package.domain.package.name"

  • Using actions ACTION_RESUME, ACTION_PAUSE and ACTION_STOP, Note Actions can actually be named anything.

# p4a/hook.py
from pathlib import Path
from pythonforandroid.toolchain import ToolchainCL #type: ignore

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")

    package = "org.wally.waller"
    service_name="Wallpapercarousel"
    foreground_type="dataSync"
    target = f'android:name="{package}.Service{service_name.capitalize()}"'

    # Inject foregroundServiceType
    pos = old_manifest.find(target)

    if pos != -1:
        end = old_manifest.find("/>", pos)
        old_manifest = (old_manifest[:end] + f'android:foregroundServiceType="{foreground_type}"' + old_manifest[end:])
        print(f"Successfully Added foregroundServiceType to Service{service_name}")

    # Your custom receiver XML
    receiver_name = "CarouselReceiver"
    receiver_xml = f'''
    <receiver android:name="{package}.{receiver_name}"
              android:enabled="true"
              android:exported="false">
        <intent-filter>
            <action android:name="ACTION_RESUME" />
            <action android:name="ACTION_PAUSE" />
            <action android:name="ACTION_STOP" />
        </intent-filter>
    </receiver>
    '''

    # Insert before the closing </application>
    new_manifest = old_manifest.replace('</application>', f'{receiver_xml}\n</application>')

    manifest_file.write_text(new_manifest, encoding="utf-8")

    print(new_manifest)
    if old_manifest != new_manifest:
        print("Receiver added successfully")
    else:
        print("Failed to add receiver")

Start Service From App

In main.py get free port and send to Service

import socket
from android import mActivity #type: ignore
from jnius import autoclass

def get_free_port():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind(("", 0))  # bind to a random free port
    port = s.getsockname()[1]
    s.close()
    return port

port = get_free_port()
context = mActivity.getApplicationContext()
service_name = "Wallpapercarousel"
service = autoclass(context.getPackageName() + '.Service' + service_name.capitalize())
service.start(mActivity, str(port))

In Service Write Port and Start Server

write the port for java receiver to see.

#./services/wallpaper.py
import os, threading, traceback, time
from jnius import autoclass

from android_notify import Notification
from android_notify.config import get_python_service

from pythonosc import dispatcher, osc_server

BuildVersion = autoclass("android.os.Build$VERSION")
ServiceInfo = autoclass("android.content.pm.ServiceInfo")


# get sent port from main UI
def get_service_port():
    port = 5006
    try:
        port = int(os.environ.get('PYTHON_SERVICE_ARGUMENT', '5006'))
    except (TypeError, ValueError):
        print("python getting PORT!")
    return port


# Start foreground service
service = get_python_service()
service_port = get_service_port()
foreground_type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC if BuildVersion.SDK_INT >= 30 else 0

notification = Notification(title="Hey From Service", name="i_have_a_name")
notification.setData({"service_port": service_port}) # Save for JAVA
notification.addButton(text="Resume", receiver_name="CarouselReceiver", action="ACTION_RESUME")
notification.addButton(text="Pause", receiver_name="CarouselReceiver", action="ACTION_PAUSE")
notification.addButton(text="Stop", receiver_name="CarouselReceiver", action="ACTION_STOP")

builder = notification.fill_args()
service.startForeground(notification.id, builder.build(), foreground_type)
service.setAutoRestartService(True)  # auto-restart if killed

class MyWallpaperReceiver:
    def __init__(self):
        self.live = True
        self.paused = False
        self.lock = threading.Lock()

        threading.Thread(target=self.heart, daemon=True).start()

    def heart(self):
        start = time.time()
        fmt = lambda s: f"{int(s // 3600)}h {int((s % 3600) // 60)}m {int(s % 60)}s"

        while self.live:
            with self.lock:
                paused = self.paused

            if not paused:
                elapsed = time.time() - start
                notification.updateTitle(f"Running for {fmt(elapsed)}")

            time.sleep(1)

    def stop(self, *args):
        self.live = False
        print("python service stop args:", args)
        service.stopSelf()

    def pause(self, *args):
        with self.lock:
            self.paused = True
        print("python service pause data received:", args)
        notification.updateTitle("Carousel Paused")

    def resume(self, *args):
        with self.lock:
            self.paused = False
        print("python service resume data received:", args)
        notification.updateTitle("Carousel Resumed")



myWallpaperReceiver = MyWallpaperReceiver()
myDispatcher = dispatcher.Dispatcher()

myDispatcher.map("/pause", myWallpaperReceiver.pause)
myDispatcher.map("/resume", myWallpaperReceiver.resume)
myDispatcher.map("/stop", myWallpaperReceiver.stop)

server = osc_server.ThreadingOSCUDPServer(("0.0.0.0", service_port), myDispatcher)

try:
    server.serve_forever()
except Exception as e:
    print("python Service Main loop Failed:", e)
    traceback.print_exc()
    # Avoiding process is bad java.lang.SecurityException

Sending data to service from Java Receiver

// src/CarouselReceiver.java

package org.wally.waller;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class CarouselReceiver extends BroadcastReceiver {

    private static final String TAG = "CarouselReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Toast.makeText(context, "Action received: " + action, Toast.LENGTH_LONG).show();

        // Read port sent by Service
        int port = 5006; // default
        String portStr = intent.getStringExtra("service_port");
        if (portStr != null && !portStr.isEmpty()) {
            try {
                int port = Integer.parseInt(portStr.trim());
                Log.d(TAG, "Service port obtained from Intent: " + port);
            } catch (NumberFormatException e) {
                Log.e(TAG, "Invalid service_port in Intent: " + portStr + ", using fallback", e);
            }
        }else{
            Log.e(TAG, "Error reading port from intent, using default port: " + port, e);
        }

        // Determine OSC message
        String oscAddress = "";
        String oscArg = "";
        if ("ACTION_STOP".equals(action)) {
            oscAddress = "/stop";
            oscArg = "hello1 frm java STOP";
        } else if ("ACTION_RESUME".equals(action)) {
            oscAddress = "/resume";
            oscArg = "hello frm java RESUME";
        } else if ("ACTION_PAUSE".equals(action)) {
            oscAddress = "/pause";
            oscArg = "hello frm java PAUSE";
        } else {
            Toast.makeText(context, "Unknown action: " + action, Toast.LENGTH_SHORT).show();
            Log.w(TAG, "Unknown action from python: " + action);
            return;
        }

        final int finalPort = port;
        final String finalAddress = oscAddress;
        final String finalArg = oscArg;

        new Thread(() -> {
            try {
                sendOscMessage("127.0.0.1", finalPort, finalAddress, finalArg);
                Log.d(TAG, "OSC message sent to python: " + finalAddress + " " + finalArg);
            } catch (Exception e) {
                Log.e(TAG, "Failed to send OSC message to python", e);
            }
        }).start();
    }

    private void sendOscMessage(String host, int port, String address, String arg) throws Exception {
        InetAddress serverAddr = InetAddress.getByName(host);
        DatagramSocket socket = new DatagramSocket();

        byte[] addrBytes = (address + "\0").getBytes("US-ASCII");
        // OSC messages are padded to 4 bytes, simple approach:
        int padding = 4 - (addrBytes.length % 4);
        byte[] paddedAddr = new byte[addrBytes.length + padding];
        System.arraycopy(addrBytes, 0, paddedAddr, 0, addrBytes.length);

        // Arguments
        String typeTag = ",s\0\0"; // string argument
        byte[] typeBytes = typeTag.getBytes("US-ASCII");

        byte[] argBytes = (arg + "\0").getBytes("US-ASCII");
        int argPadding = 4 - (argBytes.length % 4);
        byte[] paddedArg = new byte[argBytes.length + argPadding];
        System.arraycopy(argBytes, 0, paddedArg, 0, argBytes.length);

        // Combine all
        byte[] message = new byte[paddedAddr.length + typeBytes.length + paddedArg.length];
        System.arraycopy(paddedAddr, 0, message, 0, paddedAddr.length);
        System.arraycopy(typeBytes, 0, message, paddedAddr.length, typeBytes.length);
        System.arraycopy(paddedArg, 0, message, paddedAddr.length + typeBytes.length, paddedArg.length);

        DatagramPacket packet = new DatagramPacket(message, message.length, serverAddr, port);
        socket.send(packet);
        socket.close();
    }
}