Skip to content
Snippets Groups Projects
Verified Commit cf1ca4d0 authored by Mikolai Gütschow's avatar Mikolai Gütschow
Browse files

use native libNotify to show notifications on Linux systems

parent a56e0603
No related branches found
No related tags found
1 merge request!214Show notification and play sound for new messages on Linux
......@@ -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
}
......@@ -16,43 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.briarproject.briar.desktop;
package org.briarproject.briar.desktop.notification
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);
interface NotificationProvider {
val available: Boolean
fun init()
fun uninit()
fun notifyPrivateMessages(num: Int)
}
System.out.println("Sending the notification");
LibNotify.INSTANCE.notify_notification_show(notification, null);
object StubNotificationProvider : NotificationProvider {
override val available: Boolean
get() = false
System.out.println("Waiting a few seconds");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Done");
}
override fun init() {}
override fun uninit() {}
override fun notifyPrivateMessages(num: Int) {}
}
/*
* 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
}
}
/*
* 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()
}
}
......@@ -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()
}
}
......
......@@ -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
......
......@@ -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 {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment