Create the following file ~/.local/bin/wsl-toast-daemon:
#!/usr/bin/env python3
# wsl-toast-daemon: implements org.freedesktop.Notifications on the session bus
# and forwards each notification as a Windows toast (via powershell.exe WinRT).
# Waydroid's session auto-detects this service at start and forwards Android
# notifications to it. Must run in a WSL session with Windows interop alive.
import os
import subprocess
import threading
import time
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
PS = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"
# PowerShell's registered AppUserModelID — toasts from unregistered ids are dropped
PS_SCRIPT = r"""
$t=[System.Security.SecurityElement]::Escape($env:TOAST_TITLE)
$b=[System.Security.SecurityElement]::Escape($env:TOAST_BODY)
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime] | Out-Null
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml("<toast><visual><binding template='ToastGeneric'><text>$t</text><text>$b</text></binding></visual></toast>")
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe').Show($toast)
"""
def show_toast(title, body):
env = dict(os.environ)
env["TOAST_TITLE"] = title[:100] or "Waydroid"
env["TOAST_BODY"] = body[:400]
env["WSLENV"] = (env.get("WSLENV", "") + ":TOAST_TITLE/w:TOAST_BODY/w").lstrip(":")
subprocess.run(
[PS, "-NoProfile", "-NonInteractive", "-Command", PS_SCRIPT],
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30,
)
class NotificationService(dbus.service.Object):
IFACE = "org.freedesktop.Notifications"
def __init__(self, bus):
name = dbus.service.BusName(self.IFACE, bus)
super().__init__(name, "/org/freedesktop/Notifications")
self.counter = 0
self.last_toast = {}
@dbus.service.method(IFACE, in_signature="susssasa{sv}i", out_signature="u")
def Notify(self, app_name, replaces_id, app_icon, summary, body, actions, hints, timeout):
self.counter += 1
app, title, text = str(app_name), str(summary), str(body)
decision = "toast"
if not title and not text:
# Empty = either a group-summary echo (arrives right after a real
# toast — suppress) or a custom-layout notification that carries no
# extractable text
if time.monotonic() - self.last_toast.get(app, 0) < 6:
decision = "skip-echo"
else:
title, text = app, "New notification"
decision = "toast-generic"
with open("/tmp/toast-daemon.log", "a") as f:
f.write(f"app={app_name!r} summary={summary!r} body={body!r} -> {decision}\n")
if decision == "skip-echo":
return self.counter
self.last_toast[app] = time.monotonic()
threading.Thread(
target=show_toast, args=(title or app, text), daemon=True
).start()
return self.counter
@dbus.service.method(IFACE, in_signature="u")
def CloseNotification(self, notification_id):
pass
@dbus.service.method(IFACE, out_signature="as")
def GetCapabilities(self):
return ["body"]
@dbus.service.method(IFACE, out_signature="ssss")
def GetServerInformation(self):
return ("wsl-toast-daemon", pwd.getpwuid(os.getuid()).pw_name, "1.0", "1.2")
@dbus.service.signal(IFACE, signature="uu")
def NotificationClosed(self, notification_id, reason):
pass
@dbus.service.signal(IFACE, signature="us")
def ActionInvoked(self, notification_id, action_key):
pass
if __name__ == "__main__":
DBusGMainLoop(set_as_default=True)
NotificationService(dbus.SessionBus())
GLib.MainLoop().run()
Make it executable chmod +x ~/.local/bin/wsl-toast-daemon.
Create this other executable script launcher in /.local/bin/waydroid-up, and make sure it is in the PATH:
#!/bin/sh
# Daily Waydroid launcher: Windows toast notifications + session.
# Run from an interactive terminal and keep it open (Windows interop lives here).
pgrep -f wsl-toast-daemon >/dev/null || "$HOME/.local/bin/wsl-toast-daemon" &
exec waydroid session start
From now instead of using waydroid session start use waydroid-up which will launch the wsl-toast-daemon automatically on each session.