From cf1ca4d02a14e0fcd55eeec8de3c8fc3d621d876 Mon Sep 17 00:00:00 2001 From: ialokim <ialokim@mailbox.org> Date: Sun, 29 May 2022 14:16:36 +0200 Subject: [PATCH] use native libNotify to show notifications on Linux systems --- .../desktop/TestNativeNotifications.java | 58 ----- .../briar/desktop/DesktopModule.kt | 8 + .../notification/NotificationProvider.kt | 35 +++ .../linux/LibnotifyNotificationProvider.kt | 235 ++++++++++++++++++ .../linux/TestNativeNotifications.kt | 28 +++ .../briarproject/briar/desktop/ui/BriarUi.kt | 27 +- .../resources/strings/BriarDesktop.properties | 3 + .../briar/desktop/DesktopTestModule.kt | 8 + 8 files changed, 340 insertions(+), 62 deletions(-) delete mode 100644 briar-desktop/src/main/java/org/briarproject/briar/desktop/TestNativeNotifications.java create mode 100644 briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt create mode 100644 briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt create mode 100644 briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt diff --git a/briar-desktop/src/main/java/org/briarproject/briar/desktop/TestNativeNotifications.java b/briar-desktop/src/main/java/org/briarproject/briar/desktop/TestNativeNotifications.java deleted file mode 100644 index 4fa6efac44..0000000000 --- a/briar-desktop/src/main/java/org/briarproject/briar/desktop/TestNativeNotifications.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package org.briarproject.briar.desktop; - -import com.sun.jna.Library; -import com.sun.jna.Native; -import com.sun.jna.Pointer; - -public class TestNativeNotifications { - - public interface LibNotify extends Library { - LibNotify INSTANCE = Native.load("notify", LibNotify.class); - - boolean notify_init(String appName); - - Pointer notify_notification_new(String summary, String body, - String icon); - - boolean notify_notification_show(Pointer notification, Pointer error); - } - - public static void main(String[] args) { - System.out.println("Initializing libnotify"); - LibNotify.INSTANCE.notify_init("jna sandbox"); - - System.out.println("Creating a notification"); - Pointer notification = LibNotify.INSTANCE.notify_notification_new( - "Hey there", "You've got 13 new messages", - null); - - System.out.println("Sending the notification"); - LibNotify.INSTANCE.notify_notification_show(notification, null); - - System.out.println("Waiting a few seconds"); - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("Done"); - } -} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt index e8702cea23..b406225863 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt @@ -52,6 +52,9 @@ import org.briarproject.bramble.util.OsUtils.isMac import org.briarproject.briar.attachment.AttachmentModule import org.briarproject.briar.desktop.attachment.media.ImageCompressor import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl +import org.briarproject.briar.desktop.notification.NotificationProvider +import org.briarproject.briar.desktop.notification.StubNotificationProvider +import org.briarproject.briar.desktop.notification.linux.LibnotifyNotificationProvider import org.briarproject.briar.desktop.settings.UnencryptedSettings import org.briarproject.briar.desktop.settings.UnencryptedSettingsImpl import org.briarproject.briar.desktop.threading.BriarExecutors @@ -173,4 +176,9 @@ internal class DesktopModule( internal fun provideImageCompressor(imageCompressor: ImageCompressorImpl): ImageCompressor { return imageCompressor } + + @Provides + @Singleton + internal fun provideNotificationProvider(): NotificationProvider = + if (isLinux()) LibnotifyNotificationProvider else StubNotificationProvider } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt new file mode 100644 index 0000000000..cd844f9167 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt @@ -0,0 +1,35 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.notification + +interface NotificationProvider { + val available: Boolean + fun init() + fun uninit() + fun notifyPrivateMessages(num: Int) +} + +object StubNotificationProvider : NotificationProvider { + override val available: Boolean + get() = false + + override fun init() {} + override fun uninit() {} + override fun notifyPrivateMessages(num: Int) {} +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt new file mode 100644 index 0000000000..cb5617ac2f --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt @@ -0,0 +1,235 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.briarproject.briar.desktop.notification.linux + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer +import mu.KotlinLogging +import org.briarproject.briar.desktop.notification.NotificationProvider +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP +import org.briarproject.briar.desktop.utils.KLoggerUtils.e +import org.briarproject.briar.desktop.utils.KLoggerUtils.i + +object LibnotifyNotificationProvider : NotificationProvider { + + private val LOG = KotlinLogging.logger {} + + private var isAvailable: Boolean = false + + private lateinit var libNotify: LibNotify + + override val available: Boolean + get() = isAvailable + + override fun init() { + try { + libNotify = Native.load("libnotify.so.4", LibNotify::class.java) // NON-NLS + } catch (err: UnsatisfiedLinkError) { + LOG.e { "unable to load libnotify" } + return + } + + isAvailable = libNotify.notify_init(i18n("main.title")) + if (!isAvailable) { + LOG.e { "unable to initialize libnotify" } + return + } + + // print notification server capabilities + val list = libNotify.notify_get_server_caps() + val capabilities = buildList { + for (i in 0 until libNotify.g_list_length(list)) { + add(libNotify.g_list_nth_data(list, i).getString(0)) + } + } + LOG.i { "Notification server capabilities: " + capabilities.joinToString() } + } + + override fun uninit() { + if (!isAvailable) return + + libNotify.notify_uninit() + isAvailable = false + } + + override fun notifyPrivateMessages(num: Int) { + if (!isAvailable) return + + /** + * summary + * + * This is a single line overview of the notification. + * For instance, "You have mail" or "A friend has come online". + * It should generally not be longer than 40 characters, though this is not a requirement, + * and server implementations should word wrap if necessary. + * The summary must be encoded using UTF-8. + */ + // todo: we could use body instead with markup (where supported) + val text = i18nP("notifications.message.private", num) + val notification = libNotify.notify_notification_new(text, null, null) + + /** + * desktop-entry + * + * This specifies the name of the desktop filename representing the calling program. + * This should be the same as the prefix used for the application's .desktop file. + * An example would be "rhythmbox" from "rhythmbox.desktop". + * This can be used by the daemon to retrieve the correct icon for the application, for logging purposes, etc. + */ + // todo: desktop file usually not present for jar file, provide app_icon/image instead? + libNotify.notify_notification_set_desktop_entry(notification, "org.briarproject.Briar") + + /** + * suppress-sound + * + * Causes the server to suppress playing any sounds, if it has that ability. + * This is usually set when the client itself is going to play its own sound. + */ + libNotify.notify_notification_set_suppress_sound(notification, true) + + /** + * category + * + * The type of notification this is: A received instant message notification. + */ + libNotify.notify_notification_set_category(notification, "im.received") + + if (!libNotify.notify_notification_show(notification, null)) { + // todo: error handling + LOG.e { "error while sending notification via libnotify" } + } + } + + /** + * Functions as defined in the source code at + * https://www.freedesktop.org/software/gstreamer-sdk/data/docs/latest/glib/glib-GVariant.html + * https://www.freedesktop.org/software/gstreamer-sdk/data/docs/latest/glib/glib-Doubly-Linked-Lists.html + * https://gitlab.gnome.org/GNOME/libnotify/-/tree/master/libnotify + */ + @Suppress("FunctionName") + private interface LibNotify : Library { + fun g_list_length(list: Pointer): Int + fun g_list_nth_data(list: Pointer, n: Int): Pointer + + /** + * Creates a new boolean GVariant instance -- either TRUE or FALSE. + * + * @param value a gboolean value + * + * @return a floating reference to a new boolean GVariant instance. [transfer none] + * + * @since 2.24 + */ + fun g_variant_new_boolean(value: Boolean): Pointer + + /** + * Creates a string GVariant with the contents of [string]. + * + * @param string a normal utf8 nul-terminated string + * + * @return a floating reference to a new string GVariant instance. [transfer none] + * + * @since 2.24 + */ + fun g_variant_new_string(string: String): Pointer + + /** + * Initialize libnotify. This must be called before any other functions. + * + * @param app_name The name of the application initializing libnotify. + * + * @return true if successful, or false on error. + */ + fun notify_init(app_name: String): Boolean + + /** + * Uninitialize libnotify. + * + * This should be called when the program no longer needs libnotify for + * the rest of its lifecycle, typically just before exiting. + */ + fun notify_uninit() + + /** + * Synchronously queries the server for its capabilities and returns them in a #GList. + * + * @return [Pointer] to a #GList of server capability strings. Free + * the list elements with g_free() and the list itself with g_list_free(). + */ + fun notify_get_server_caps(): Pointer + + /** + * Creates a new #NotifyNotification. The summary text is required, but + * all other parameters are optional. + * + * @param summary The required summary text. + * @param body The optional body text. + * @param icon The optional icon theme icon name or filename. + * + * @return [Pointer] to the new #NotifyNotification. + */ + fun notify_notification_new(summary: String, body: String?, icon: String?): Pointer + + /** + * Tells the notification server to display the notification on the screen. + * + * @param notification [Pointer] to the notification. + * @param error The returned error information. + * + * @return true if successful. On error, this will return false and set [error]. + */ + fun notify_notification_show(notification: Pointer, error: Pointer?): Boolean + + /** + * Sets a hint for [key] with value [value]. If [value] is null, + * a previously set hint for [key] is unset. + * + * If [value] is floating, it is consumed. + * + * @param notification [Pointer] to a #NotifyNotification + * @param key the hint key + * @param value [Pointer] to hint value as GVariant, or null to unset the hint + * + * @since 0.6 + */ + fun notify_notification_set_hint(notification: Pointer, key: String, value: Pointer?) + + /** + * Sets the category of this notification. This can be used by the + * notification server to filter or display the data in a certain way. + * + * @param notification [Pointer] to the notification. + * @param category The category. + */ + fun notify_notification_set_category(notification: Pointer, category: String) + } + + @Suppress("FunctionName") + private fun LibNotify.notify_notification_set_desktop_entry(notification: Pointer, desktopEntry: String) { + val string = g_variant_new_string(desktopEntry) + notify_notification_set_hint(notification, "desktop-entry", string) // NON-NLS + } + + @Suppress("FunctionName") + private fun LibNotify.notify_notification_set_suppress_sound(notification: Pointer, suppressSound: Boolean) { + val bool = g_variant_new_boolean(suppressSound) + notify_notification_set_hint(notification, "suppress-sound", bool) // NON-NLS + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt new file mode 100644 index 0000000000..3f145ae5ae --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt @@ -0,0 +1,28 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.briarproject.briar.desktop.notification.linux + +fun main() { + LibnotifyNotificationProvider.apply { + init() + + notifyPrivateMessages(9) + + uninit() + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index cb7000b270..771eb54199 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -48,9 +48,11 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RU import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent import org.briarproject.briar.desktop.DesktopFeatureFlags +import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent import org.briarproject.briar.desktop.expiration.ExpirationBanner import org.briarproject.briar.desktop.login.ErrorScreen import org.briarproject.briar.desktop.login.StartupScreen +import org.briarproject.briar.desktop.notification.NotificationProvider import org.briarproject.briar.desktop.settings.UnencryptedSettings import org.briarproject.briar.desktop.settings.UnencryptedSettings.Theme.AUTO import org.briarproject.briar.desktop.settings.UnencryptedSettings.Theme.DARK @@ -97,6 +99,7 @@ constructor( private val unencryptedSettings: UnencryptedSettings, private val featureFlags: FeatureFlags, private val desktopFeatureFlags: DesktopFeatureFlags, + private val notificationProvider: NotificationProvider, ) : BriarUi { private var screenState by mutableStateOf( @@ -136,11 +139,25 @@ constructor( .toAwtImage(LocalDensity.current, LocalLayoutDirection.current, Size(32f, 32f)) DisposableEffect(Unit) { + // todo: hard-coded messageCount doesn't account for unread messages on application start + // also see https://code.briarproject.org/briar/briar-desktop/-/issues/133 + var messageCount = 0 + val eventListener = EventListener { e -> - if (e is LifecycleEvent && e.lifecycleState == RUNNING) - screenState = MAIN - if (e is ConversationMessageReceivedEvent<*> && !focusState.focused) { - window.iconImage = iconBadge + when (e) { + is LifecycleEvent -> + if (e.lifecycleState == RUNNING) screenState = MAIN + is ConversationMessageReceivedEvent<*> -> { + messageCount++ + if (!focusState.focused) { + window.iconImage = iconBadge + notificationProvider.notifyPrivateMessages(messageCount) + } + } + is ConversationMessagesReadEvent -> { + messageCount -= e.count + if (messageCount < 0) messageCount = 0 + } } } val focusListener = object : WindowFocusListener { @@ -154,12 +171,14 @@ constructor( } } + notificationProvider.init() eventBus.addListener(eventListener) window.addWindowFocusListener(focusListener) onDispose { eventBus.removeListener(eventListener) window.removeWindowFocusListener(focusListener) + notificationProvider.uninit() } } diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index 6f33e168bf..c460e74194 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -250,6 +250,9 @@ expiration.banner.part1.zero=This is a test version of Briar that will expire to expiration.banner.part1.nozero={0, plural, one {This is a test version of Briar that will expire tomorrow.} other {This is a test version of Briar that will expire in {0} days.}} expiration.banner.part2=Please update to a newer version in time. +# Notification +notifications.message.private={0, plural, one {New private message.} other {{0} new private messages.}} + # Settings settings.title=Settings diff --git a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt index 7145cb8015..6fb2ca9fbc 100644 --- a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt +++ b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt @@ -53,6 +53,9 @@ import org.briarproject.briar.api.test.TestAvatarCreator import org.briarproject.briar.attachment.AttachmentModule import org.briarproject.briar.desktop.attachment.media.ImageCompressor import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl +import org.briarproject.briar.desktop.notification.NotificationProvider +import org.briarproject.briar.desktop.notification.StubNotificationProvider +import org.briarproject.briar.desktop.notification.linux.LibnotifyNotificationProvider import org.briarproject.briar.desktop.settings.UnencryptedSettings import org.briarproject.briar.desktop.settings.UnencryptedSettingsImpl import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator @@ -180,6 +183,11 @@ internal class DesktopTestModule( return imageCompressor } + @Provides + @Singleton + internal fun provideNotificationProvider(): NotificationProvider = + if (isLinux()) LibnotifyNotificationProvider else StubNotificationProvider + @Provides @Singleton internal fun provideTestAvatarCreator(testAvatarCreator: TestAvatarCreatorImpl): TestAvatarCreator { -- GitLab