NotificationManager: use GIO instead of libportal

GIO's notification implementation makes the code more readable and has
the advantage of supporting multiple notification specifications.

By default we will now use freedesktop notifications when running
without `.desktop` file and XDG-portal notifications when running
with `.desktop` file.

To prevent dynamic notification updates from arriving in wrong order at
the desktop environment, we need to manually queue them up and make sure
that there is at least 200ms delay between updates.
This commit is contained in:
Julian Winkler
2025-07-04 16:41:48 +02:00
parent 9de08ab922
commit 3708cc990e
4 changed files with 135 additions and 97 deletions

View File

@@ -1,5 +1,5 @@
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <libportal/portal.h> #include <gio/gdesktopappinfo.h>
#include "../defines.h" #include "../defines.h"
#include "../util.h" #include "../util.h"
@@ -11,86 +11,136 @@
#define MPRIS_BUS_NAME_PREFIX "org.mpris.MediaPlayer2." #define MPRIS_BUS_NAME_PREFIX "org.mpris.MediaPlayer2."
#define MPRIS_OBJECT_NAME "/org/mpris/MediaPlayer2" #define MPRIS_OBJECT_NAME "/org/mpris/MediaPlayer2"
static XdpPortal *portal = NULL; /* ongoing notifications to be removed when the app is closed */
static GHashTable *ongoing_notifications = NULL; static GHashTable *ongoing_notifications = NULL;
JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder(JNIEnv *env, jobject this) /* We queue up notification updates in pending_notifications to make sure that there is at least 200ms
delay between consecutive updates. This prevents dynamic notification updated from arriving in wrong
order at the desktop environment */
static GHashTable *pending_notifications = NULL;
static GMutex pending_notifications_mutex = {0};
static GSource *send_notifcation_timer = NULL;
static gboolean send_notifcation_func(GSource *send_notifcation_timer, GSourceFunc callback, gpointer user_data)
{ {
return _INTPTR(g_variant_builder_new(G_VARIANT_TYPE("aa{sv}"))); printf("Sending notifications\n");
GApplication *app = g_application_get_default();
GHashTableIter iter;
gpointer key, value;
gboolean notification_sent = FALSE;
g_mutex_lock(&pending_notifications_mutex);
g_hash_table_iter_init(&iter, pending_notifications);
while (g_hash_table_iter_next(&iter, &key, &value)){
char *id_string = g_strdup_printf("%d", GPOINTER_TO_INT(key));
if (value)
g_application_send_notification(app, id_string, value);
else
g_application_withdraw_notification(app, id_string);
g_free(id_string);
g_hash_table_iter_remove(&iter);
notification_sent = TRUE;
}
g_mutex_unlock(&pending_notifications_mutex);
if (notification_sent)
g_source_set_ready_time(send_notifcation_timer, g_source_get_time(send_notifcation_timer) + 200000L); // 200ms
else
g_source_set_ready_time(send_notifcation_timer, -1);
return G_SOURCE_CONTINUE;
}
static GSourceFuncs send_notifcation_funcs = {
.dispatch = send_notifcation_func,
};
static void unref_nullsafe(void *data) {
if (data)
g_object_unref(data);
} }
static GVariant *serialize_intent(JNIEnv *env, jint type, jstring action_jstr, jstring className_jstr) static void notification_action(GSimpleAction *action, GVariant* parameter, gpointer user_data)
{ {
const char *action = action_jstr ? (*env)->GetStringUTFChars(env, action_jstr, NULL) : NULL; printf("notification_action\n");
const char *className = className_jstr ? (*env)->GetStringUTFChars(env, className_jstr, NULL) : NULL;
GVariant *intent = g_variant_new("(iss)", type, action ?: "", className ?: "");
if (action_jstr) (*env)->ReleaseStringUTFChars(env, action_jstr, action);
if (className_jstr) (*env)->ReleaseStringUTFChars(env, className_jstr, className);
return intent;
}
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction(JNIEnv *env, jobject this, jlong builder_ptr, jstring name_jstr, jint type, jstring action, jstring className)
{
GVariantBuilder *builder = _PTR(builder_ptr);
g_variant_builder_open(builder, G_VARIANT_TYPE("a{sv}"));
if (name_jstr) {
const char *name = (*env)->GetStringUTFChars(env, name_jstr, NULL);
g_variant_builder_add(builder, "{sv}", "label", g_variant_new_string(name));
(*env)->ReleaseStringUTFChars(env, name_jstr, name);
}
g_variant_builder_add(builder, "{sv}", "action", g_variant_new_string("button-action"));
g_variant_builder_add(builder, "{sv}", "target", serialize_intent(env, type, action, className));
g_variant_builder_close(builder);
}
static void notification_action_invoked(XdpPortal *portal, gchar *id_str, gchar *action, GVariant *parameter, gpointer user_data)
{
int id = atoi(id_str);
int type; int type;
const char *actionName; const char *actionName;
const char *className; const char *className;
GVariant *target; const char *data;
JNIEnv *env = get_jni_env(); JNIEnv *env = get_jni_env();
GVariantIter *iter = g_variant_iter_new(parameter); g_variant_get(parameter, "(isss)", &type, &actionName, &className, &data);
g_variant_iter_next(iter, "v", &target); jmethodID notificationActionCallback = _STATIC_METHOD((*env)->FindClass(env, "android/app/NotificationManager"), "notificationActionCallback", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
g_variant_get(target, "(iss)", &type, &actionName, &className); (*env)->CallStaticVoidMethod(env, (*env)->FindClass(env, "android/app/NotificationManager"), notificationActionCallback, type, _JSTRING(actionName), _JSTRING(className), _JSTRING(data));
jmethodID notificationActionCallback = _STATIC_METHOD((*env)->FindClass(env, "android/app/NotificationManager"), "notificationActionCallback", "(IILjava/lang/String;Ljava/lang/String;)V");
(*env)->CallStaticVoidMethod(env, (*env)->FindClass(env, "android/app/NotificationManager"), notificationActionCallback, id, type, _JSTRING(actionName), _JSTRING(className));
g_variant_iter_free(iter);
g_variant_unref(target);
} }
// gnome session locks up when we send notification update before last update was processed static void queue_notification(int id, GNotification *notification) {
static int callback_pending = 0; g_mutex_lock(&pending_notifications_mutex);
static void natification_callback(GObject* source_object, GAsyncResult* res, gpointer data) if (!pending_notifications) {
{ pending_notifications = g_hash_table_new_full(NULL, NULL, NULL, unref_nullsafe);
callback_pending = 0; send_notifcation_timer = g_source_new(&send_notifcation_funcs, sizeof(GSource));
} g_source_attach(send_notifcation_timer, NULL);
GApplication *app = g_application_get_default();
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotification(JNIEnv *env, jobject this, jlong builder_ptr, jint id, jstring title_jstr, jstring text_jstr, jstring icon_jstr, jboolean ongoing, jint type, jstring action, jstring className) gchar *desktop_id = g_strdup_printf("%s.desktop", g_application_get_application_id(app));
{ GDesktopAppInfo *info = g_desktop_app_info_new(desktop_id);
if (callback_pending) { if (!info) // some desktop environments don't allow XDG-portal notifications without a desktop file
return; setenv("GNOTIFICATION_BACKEND", "freedesktop", 0);
} else
if (!portal) { g_object_unref(info);
portal = xdp_portal_new(); g_free(desktop_id);
g_signal_connect(portal, "notification-action-invoked", G_CALLBACK(notification_action_invoked), NULL); GSimpleAction *action = g_simple_action_new("notificationaction", g_variant_type_new("(isss)"));
g_signal_connect(action, "activate", G_CALLBACK(notification_action), app);
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action));
g_object_unref(action);
ongoing_notifications = g_hash_table_new(NULL, NULL); ongoing_notifications = g_hash_table_new(NULL, NULL);
} }
g_hash_table_insert(pending_notifications, GINT_TO_POINTER(id), notification);
g_mutex_unlock(&pending_notifications_mutex);
if (g_source_get_ready_time(send_notifcation_timer) == -1) {
g_source_set_ready_time(send_notifcation_timer, 0); // immediately
}
}
GVariantBuilder *builder = _PTR(builder_ptr); JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder(JNIEnv *env, jobject this)
GVariant *buttons = g_variant_builder_end(builder); {
return _INTPTR(g_notification_new(""));
}
static GVariant *serialize_intent(JNIEnv *env, jint type, jstring action_jstr, jstring className_jstr, jstring data_jstr)
{
const char *action = action_jstr ? (*env)->GetStringUTFChars(env, action_jstr, NULL) : NULL;
const char *className = className_jstr ? (*env)->GetStringUTFChars(env, className_jstr, NULL) : NULL;
const char *data = data_jstr ? (*env)->GetStringUTFChars(env, data_jstr, NULL) : NULL;
GVariant *intent = g_variant_new("(isss)", type, action ?: "", className ?: "", data ?: "");
if (action_jstr) (*env)->ReleaseStringUTFChars(env, action_jstr, action);
if (className_jstr) (*env)->ReleaseStringUTFChars(env, className_jstr, className);
if (data_jstr) (*env)->ReleaseStringUTFChars(env, data_jstr, data);
return intent;
}
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction(JNIEnv *env, jobject this, jlong builder_ptr, jstring name_jstr, jint type, jstring action, jstring className, jstring data)
{
GNotification *notification = _PTR(builder_ptr);
const char *name = "";
if (name_jstr) {
name = (*env)->GetStringUTFChars(env, name_jstr, NULL);
}
g_notification_add_button_with_target_value(notification, name, "app.notificationaction", serialize_intent(env, type, action, className, data));
if (name_jstr) {
(*env)->ReleaseStringUTFChars(env, name_jstr, name);
}
}
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotification(JNIEnv *env, jobject this, jlong builder_ptr, jint id, jstring title_jstr, jstring text_jstr, jstring icon_jstr, jboolean ongoing, jint type, jstring action, jstring className, jstring data)
{
GNotification *notification = _PTR(builder_ptr);
g_variant_builder_init(builder, G_VARIANT_TYPE("a{sv}"));
if (title_jstr) { if (title_jstr) {
const char *title = (*env)->GetStringUTFChars(env, title_jstr, NULL); const char *title = (*env)->GetStringUTFChars(env, title_jstr, NULL);
g_variant_builder_add(builder, "{sv}", "title", g_variant_new_string(title)); g_notification_set_title(notification, title);
(*env)->ReleaseStringUTFChars(env, title_jstr, title); (*env)->ReleaseStringUTFChars(env, title_jstr, title);
} }
if (text_jstr) { if (text_jstr) {
const char *text = (*env)->GetStringUTFChars(env, text_jstr, NULL); const char *text = (*env)->GetStringUTFChars(env, text_jstr, NULL);
g_variant_builder_add(builder, "{sv}", "body", g_variant_new_string(text)); g_notification_set_body(notification, text);
(*env)->ReleaseStringUTFChars(env, text_jstr, text); (*env)->ReleaseStringUTFChars(env, text_jstr, text);
} }
if (icon_jstr) { if (icon_jstr) {
@@ -100,41 +150,28 @@ JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotificati
GMappedFile *icon_file = g_mapped_file_new(icon_path_full, FALSE, NULL); GMappedFile *icon_file = g_mapped_file_new(icon_path_full, FALSE, NULL);
GBytes *icon_bytes = g_mapped_file_get_bytes(icon_file); GBytes *icon_bytes = g_mapped_file_get_bytes(icon_file);
GIcon *icon = g_bytes_icon_new(icon_bytes); GIcon *icon = g_bytes_icon_new(icon_bytes);
GVariant *icon_serialized = g_icon_serialize(icon); g_notification_set_icon(notification, icon);
g_variant_builder_add(builder, "{sv}", "icon", icon_serialized);
g_variant_unref(icon_serialized);
g_object_unref(icon); g_object_unref(icon);
g_bytes_unref(icon_bytes); g_bytes_unref(icon_bytes);
g_mapped_file_unref(icon_file); g_mapped_file_unref(icon_file);
g_free(icon_path_full); g_free(icon_path_full);
(*env)->ReleaseStringUTFChars(env, icon_jstr, icon_path); (*env)->ReleaseStringUTFChars(env, icon_jstr, icon_path);
} }
g_variant_builder_add(builder, "{sv}", "default-action", g_variant_new_string("default-action")); g_notification_set_default_action_and_target_value(notification, "app.notificationaction", serialize_intent(env, type, action, className, data));
g_variant_builder_add(builder, "{sv}", "default-action-target", serialize_intent(env, type, action, className)); queue_notification(id, notification);
g_variant_builder_add(builder, "{sv}", "buttons", buttons);
GVariant *variant = g_variant_builder_end(builder);
g_variant_builder_unref(builder);
char *id_string = g_strdup_printf("%d", id);
xdp_portal_remove_notification(portal, id_string);
callback_pending = 1;
xdp_portal_add_notification(portal, id_string, variant, XDP_NOTIFICATION_FLAG_NONE, NULL, natification_callback, NULL);
g_free(id_string);
if (ongoing) if (ongoing)
g_hash_table_add(ongoing_notifications, GINT_TO_POINTER(id)); g_hash_table_add(ongoing_notifications, GINT_TO_POINTER(id));
} }
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeCancel(JNIEnv *env, jobject this, jint id) JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeCancel(JNIEnv *env, jobject this, jint id)
{ {
char *id_string = g_strdup_printf("%d", id); queue_notification(id, NULL);
if (portal)
xdp_portal_remove_notification(portal, id_string);
g_free(id_string);
} }
static void remove_ongoing_notification(gpointer key, gpointer value, gpointer user_data) static void remove_ongoing_notification(gpointer key, gpointer value, gpointer user_data)
{ {
char *id_string = g_strdup_printf("%d", GPOINTER_TO_INT(key)); char *id_string = g_strdup_printf("%d", GPOINTER_TO_INT(key));
xdp_portal_remove_notification(portal, id_string); g_application_withdraw_notification(g_application_get_default(), id_string);
g_free(id_string); g_free(id_string);
} }

View File

@@ -18,18 +18,18 @@ JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder
/* /*
* Class: android_app_NotificationManager * Class: android_app_NotificationManager
* Method: nativeAddAction * Method: nativeAddAction
* Signature: (JLjava/lang/String;ILjava/lang/String;Ljava/lang/String;)V * Signature: (JLjava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
*/ */
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction
(JNIEnv *, jobject, jlong, jstring, jint, jstring, jstring); (JNIEnv *, jobject, jlong, jstring, jint, jstring, jstring, jstring);
/* /*
* Class: android_app_NotificationManager * Class: android_app_NotificationManager
* Method: nativeShowNotification * Method: nativeShowNotification
* Signature: (JILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/String;Ljava/lang/String;)V * Signature: (JILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
*/ */
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotification JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotification
(JNIEnv *, jobject, jlong, jint, jstring, jstring, jstring, jboolean, jint, jstring, jstring); (JNIEnv *, jobject, jlong, jint, jstring, jstring, jstring, jboolean, jint, jstring, jstring, jstring);
/* /*
* Class: android_app_NotificationManager * Class: android_app_NotificationManager

View File

@@ -1,15 +1,13 @@
package android.app; package android.app;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import android.app.Notification.MediaStyle; import android.app.Notification.MediaStyle;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@@ -17,9 +15,6 @@ public class NotificationManager {
private static int mpris_notification_id = -1; private static int mpris_notification_id = -1;
// store Intents in map, as long as Parcelable serialization is not yet implemented
private static Map<Integer, Intent> intents = new HashMap<Integer, Intent>();
public void cancelAll() {} public void cancelAll() {}
public void notify(String tag, int id, Notification notification) { public void notify(String tag, int id, Notification notification) {
@@ -37,23 +32,26 @@ public class NotificationManager {
int intentType = -1; int intentType = -1;
String actionName = null; String actionName = null;
String className = null; String className = null;
String data = null;
if (action.intent != null) { if (action.intent != null) {
intentType = action.intent.type; intentType = action.intent.type;
actionName = action.intent.intent.getAction(); actionName = action.intent.intent.getAction();
className = action.intent.intent.getComponent() != null ? action.intent.intent.getComponent().getClassName() : null; className = action.intent.intent.getComponent() != null ? action.intent.intent.getComponent().getClassName() : null;
data = action.intent.intent.getData() != null ? action.intent.intent.getData().toString() : null;
} }
nativeAddAction(builder, action.title, intentType, actionName, className); nativeAddAction(builder, action.title, intentType, actionName, className, data);
} }
int intentType = -1; int intentType = -1;
String actionName = null; String actionName = null;
String className = null; String className = null;
String data = null;
if (notification.intent != null) { if (notification.intent != null) {
intentType = notification.intent.type; intentType = notification.intent.type;
actionName = notification.intent.intent.getAction(); actionName = notification.intent.intent.getAction();
className = notification.intent.intent.getComponent() != null ? notification.intent.intent.getComponent().getClassName() : null; className = notification.intent.intent.getComponent() != null ? notification.intent.intent.getComponent().getClassName() : null;
intents.put(id, notification.intent.intent); data = notification.intent.intent.getData() != null ? notification.intent.intent.getData().toString() : null;
} }
nativeShowNotification(builder, id, notification.title, notification.text, notification.iconPath, notification.ongoing, intentType, actionName, className); nativeShowNotification(builder, id, notification.title, notification.text, notification.iconPath, notification.ongoing, intentType, actionName, className, data);
} }
public void notify(int id, Notification notification) { public void notify(int id, Notification notification) {
@@ -80,17 +78,15 @@ public class NotificationManager {
cancel(null, id); cancel(null, id);
} }
protected static void notificationActionCallback(int id, int intentType, String action, String className) { protected static void notificationActionCallback(int intentType, String action, String className, String data) {
Context context = Context.this_application; Context context = Context.this_application;
action = "".equals(action) ? null : action; action = "".equals(action) ? null : action;
className = "".equals(className) ? null : className; className = "".equals(className) ? null : className;
Intent intent = intents.remove(id); data = "".equals(data) ? null : data;
if (intent == null || !Objects.equals(action, intent.getAction()) || !Objects.equals(className, intent.getComponent() == null ? null : intent.getComponent().getClassName())) { Intent intent = new Intent(action, data != null ? Uri.parse(data) : null);
intent = new Intent(action);
if (className != null) { if (className != null) {
intent.setComponent(new ComponentName(context, className)); intent.setComponent(new ComponentName(context, className));
} }
}
if (intentType == 0) { // type Activity if (intentType == 0) { // type Activity
context.startActivity(intent); context.startActivity(intent);
} else if (intentType == 1) { // type Service } else if (intentType == 1) { // type Service
@@ -103,8 +99,8 @@ public class NotificationManager {
public void createNotificationChannel(NotificationChannel channel) {} public void createNotificationChannel(NotificationChannel channel) {}
protected native long nativeInitBuilder(); protected native long nativeInitBuilder();
protected native void nativeAddAction(long builder, String title, int intentType, String action, String className); protected native void nativeAddAction(long builder, String title, int intentType, String action, String className, String data);
protected native void nativeShowNotification(long builder, int id, String title, String text, String iconPath, boolean ongoing, int intentType, String action, String className); protected native void nativeShowNotification(long builder, int id, String title, String text, String iconPath, boolean ongoing, int intentType, String action, String className, String data);
protected native void nativeShowMPRIS(String packageName, String identiy); protected native void nativeShowMPRIS(String packageName, String identiy);
protected native void nativeCancel(int id); protected native void nativeCancel(int id);
protected native void nativeCancelMPRIS(); protected native void nativeCancelMPRIS();

View File

@@ -780,7 +780,12 @@ int main(int argc, char **argv)
callback_data->extra_jvm_options = NULL; callback_data->extra_jvm_options = NULL;
callback_data->extra_string_keys = NULL; callback_data->extra_string_keys = NULL;
app = gtk_application_new("com.example.demo_application", G_APPLICATION_NON_UNIQUE | G_APPLICATION_HANDLES_OPEN | G_APPLICATION_CAN_OVERRIDE_APP_ID); bool has_app_id = false;
for (int i = 1; i < argc; i++) {
if (strncmp(argv[i], "--gapplication-app-id", sizeof("--gapplication-app-id")-1) == 0)
has_app_id = true;
}
app = gtk_application_new("com.example.demo_application", (has_app_id ? 0 : G_APPLICATION_NON_UNIQUE) | G_APPLICATION_HANDLES_OPEN | G_APPLICATION_CAN_OVERRIDE_APP_ID);
// cmdline related setup // cmdline related setup
init_cmd_parameters(G_APPLICATION(app), callback_data); init_cmd_parameters(G_APPLICATION(app), callback_data);
@@ -789,8 +794,8 @@ int main(int argc, char **argv)
g_signal_connect(app, "activate", G_CALLBACK(activate), callback_data); g_signal_connect(app, "activate", G_CALLBACK(activate), callback_data);
g_signal_connect(app, "open", G_CALLBACK(open), callback_data); g_signal_connect(app, "open", G_CALLBACK(open), callback_data);
status = g_application_run(G_APPLICATION(app), argc, argv); status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
remove_ongoing_notifications(); remove_ongoing_notifications();
g_object_unref(app);
return status; return status;
} }