Why Blueman Notifications Stay Forever

For many years, Puppy Linux had no Bluetooth support. But lately, a user has complained about (the recently added) Blueman: it shows a notification, which don't disappear until you restart X. For some reason, Wi-Fi and Bluetooth are super unreliable in this apartment (and I can't wait until we finally move to our new apartment in the summer), so I constantly have to close these distracting and useless notifications about connections and disconnections. I didn't have to do anything to reproduce the problem.

At first, I had no idea why these notifications appear in first place, because Puppy Linux doesn't have desktop notifications. At least, unless you install a notification daemon. Therefore, I took a look in Blueman's configuration (via its GUI), and found out that the ConnectionNotifier plugin is enabled by default.

I haven't found a clean way to disable it, and Puppy doesn't have desktop notifications anyway, so I "fixed" the problem by deleting this plugin:

# Puppy traditionally doesn't have desktop notifications, and Blueman's internal notifications mechanism doesn't have timeout
rm -f usr/lib/python3/dist-packages/blueman/plugins/applet/ConnectionNotifier.py 

https://github.com/puppylinux-woof-CE/woof-CE/pull/3082

The fix seemed to work, probably because I already had desktop notification-daemon installed (and configured as a D-Bus service) at this point. I was reluctantly working on desktop notifications support in Puppy (as plan B), and lost all my hope.

When I took a second look at the problem, I found out that Blueman has an internal implementation of desktop notifications:

notification-daemon

    if forced_fallback or 'body' not in caps or (actions and 'actions' not in caps):
        # Use fallback in the case:
        # * user does not want to use a notification daemon
        # * the notification daemon is not available
        # * we have to show actions and the notification daemon does not provide them
        klass: Type[Union[_NotificationBubble, _NotificationDialog]] = _NotificationDialog
    else:
        klass = _NotificationBubble

Notification.py, line 270

These notifications, shown by Blueman itself, don't disappear after some time. Confusingly, the constructor of this class has a timeout parameter:

class _NotificationDialog(Gtk.MessageDialog):
    def __init__(self, summary: str, message: str, _timeout: int = -1,

https://github.com/blueman-project/blueman/blob/ac26e5eaab7cf4be5f43ed9c658427cfa05780a7/blueman/gui/Notification.py#L29

However, this parameter is ignored and not even saved aside. It exists because Blueman has two classes: one for proper notifications, and another for these hacky notifications, which don't disappear.

Clearly, adding notification-daemon to Puppy, to get the real thing working, is not an option:

So, what do we do? On the one hand, Blueman's notifications are buggy and I wish Blueman didn't have this fallback. On the other hand, adding proper support for proper desktop notifications is not an option. The solution for this problem with Blueman has to cover other applications, too.

(There is no third solution of replacing Blueman with something else: it's written in Python, but it's still smaller and lighter than the alternatives. Bluetooth support is crucial, especially for families that struggle to find a working computer for every school-age child.)

After some digging and a quick look in the source code of 3 implementations of desktop notifications, I realized they all follow a specification, and it's not that complicated. Gemini taught me that not all specifications are evil and over-engineered, although I think pretty much everything related to D-Bus is over-engineered. But this specification is not too bad:

Desktop Notifications Specification

When Blueman decides whether or not to use the bad fallback implementation, it asks the desktop notifications daemon what capabilities it offers. Then, it chooses the daemon's magical ability to show notifications if it supports 'body' and 'actions':

        caps = bus.call_sync('org.freedesktop.Notifications', '/org/freedesktop/Notifications',
                             'org.freedesktop.Notifications', 'GetCapabilities', None, None,
                             Gio.DBusCallFlags.NONE, -1, None).unpack()[0]

    ...

    if forced_fallback or 'body' not in caps or (actions and 'actions' not in caps):

Notification.py, line 263

Notification.py, line 269

Therefore, I wrote my own "notifications daemon" in 58 CLOC, which implements only the GetCapabilities method:

static gboolean
on_get_capabilities (OrgFreedesktopNotifications *object,
                     GDBusMethodInvocation       *invocation,
                     gpointer                    user_data)
{
  static const gchar *capabilities[] = {"body", "actions", NULL};
  org_freedesktop_notifications_complete_get_capabilities (object, invocation, capabilities);
  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

https://github.com/puppylinux-woof-CE/woof-CE/pull/3093

It tells Blueman exactly what it wants to hear, making it choose the notifications daemon over its built-in implementation. Then, when Blueman tries to show a notification, the Notify method is missing and no notification is shown. Luckily, Blueman doesn't activate its fallback implementation when the daemon fails.

This reminds me of the early Devuan days, when I implemented a "stub" D-Bus service that implements one logind method to make Xfce work without systemd. Later, we upstreamed a patch that made logind an optional dependency, as it should be, and everything is fine since then: this hard dependency was introduced by mistake, and it's good to have allies in the *BSD space, who have a shared interest in a systemd-agnostic Xfce.

xfce4-power-manager patch

Maybe I should contribute a Blueman patch which implements timeout or allows the user to disable notifications of either kind, but I need to think about this first. I don't think anyone uses the fallback implementation, but I want a minimal solution that won't upset anyone.