From 3708cc990efc9ca1dea39349fb1466550e0edf66 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Fri, 4 Jul 2025 16:41:48 +0200 Subject: [PATCH] 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. --- .../app/android_app_NotificationManager.c | 183 +++++++++++------- .../android_app_NotificationManager.h | 8 +- .../android/app/NotificationManager.java | 32 ++- src/main-executable/main.c | 9 +- 4 files changed, 135 insertions(+), 97 deletions(-) diff --git a/src/api-impl-jni/app/android_app_NotificationManager.c b/src/api-impl-jni/app/android_app_NotificationManager.c index 792fbad4..fe05424f 100644 --- a/src/api-impl-jni/app/android_app_NotificationManager.c +++ b/src/api-impl-jni/app/android_app_NotificationManager.c @@ -1,5 +1,5 @@ #include -#include +#include #include "../defines.h" #include "../util.h" @@ -11,86 +11,136 @@ #define MPRIS_BUS_NAME_PREFIX "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; -JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder(JNIEnv *env, jobject this) -{ - return _INTPTR(g_variant_builder_new(G_VARIANT_TYPE("aa{sv}"))); -} +/* 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 GVariant *serialize_intent(JNIEnv *env, jint type, jstring action_jstr, jstring className_jstr) +static gboolean send_notifcation_func(GSource *send_notifcation_timer, GSourceFunc callback, gpointer user_data) { - const char *action = action_jstr ? (*env)->GetStringUTFChars(env, action_jstr, NULL) : NULL; - 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; -} + printf("Sending notifications\n"); + GApplication *app = g_application_get_default(); + GHashTableIter iter; + gpointer key, value; + gboolean notification_sent = FALSE; -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_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_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); + 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 void notification_action_invoked(XdpPortal *portal, gchar *id_str, gchar *action, GVariant *parameter, gpointer user_data) +static void notification_action(GSimpleAction *action, GVariant* parameter, gpointer user_data) { - int id = atoi(id_str); + printf("notification_action\n"); int type; const char *actionName; const char *className; - GVariant *target; + const char *data; JNIEnv *env = get_jni_env(); - GVariantIter *iter = g_variant_iter_new(parameter); - g_variant_iter_next(iter, "v", &target); - g_variant_get(target, "(iss)", &type, &actionName, &className); - 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); + g_variant_get(parameter, "(isss)", &type, &actionName, &className, &data); + jmethodID notificationActionCallback = _STATIC_METHOD((*env)->FindClass(env, "android/app/NotificationManager"), "notificationActionCallback", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + (*env)->CallStaticVoidMethod(env, (*env)->FindClass(env, "android/app/NotificationManager"), notificationActionCallback, type, _JSTRING(actionName), _JSTRING(className), _JSTRING(data)); } -// gnome session locks up when we send notification update before last update was processed -static int callback_pending = 0; -static void natification_callback(GObject* source_object, GAsyncResult* res, gpointer data) -{ - callback_pending = 0; -} - -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) -{ - if (callback_pending) { - return; - } - if (!portal) { - portal = xdp_portal_new(); - g_signal_connect(portal, "notification-action-invoked", G_CALLBACK(notification_action_invoked), NULL); +static void queue_notification(int id, GNotification *notification) { + g_mutex_lock(&pending_notifications_mutex); + if (!pending_notifications) { + pending_notifications = g_hash_table_new_full(NULL, NULL, NULL, unref_nullsafe); + send_notifcation_timer = g_source_new(&send_notifcation_funcs, sizeof(GSource)); + g_source_attach(send_notifcation_timer, NULL); + GApplication *app = g_application_get_default(); + gchar *desktop_id = g_strdup_printf("%s.desktop", g_application_get_application_id(app)); + GDesktopAppInfo *info = g_desktop_app_info_new(desktop_id); + if (!info) // some desktop environments don't allow XDG-portal notifications without a desktop file + setenv("GNOTIFICATION_BACKEND", "freedesktop", 0); + else + g_object_unref(info); + g_free(desktop_id); + 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); } + 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); - GVariant *buttons = g_variant_builder_end(builder); +JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder(JNIEnv *env, jobject this) +{ + 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) { 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); } if (text_jstr) { 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); } 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); GBytes *icon_bytes = g_mapped_file_get_bytes(icon_file); GIcon *icon = g_bytes_icon_new(icon_bytes); - GVariant *icon_serialized = g_icon_serialize(icon); - g_variant_builder_add(builder, "{sv}", "icon", icon_serialized); - g_variant_unref(icon_serialized); + g_notification_set_icon(notification, icon); g_object_unref(icon); g_bytes_unref(icon_bytes); g_mapped_file_unref(icon_file); g_free(icon_path_full); (*env)->ReleaseStringUTFChars(env, icon_jstr, icon_path); } - g_variant_builder_add(builder, "{sv}", "default-action", g_variant_new_string("default-action")); - g_variant_builder_add(builder, "{sv}", "default-action-target", serialize_intent(env, type, action, className)); - 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); + g_notification_set_default_action_and_target_value(notification, "app.notificationaction", serialize_intent(env, type, action, className, data)); + queue_notification(id, notification); if (ongoing) g_hash_table_add(ongoing_notifications, GINT_TO_POINTER(id)); } JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeCancel(JNIEnv *env, jobject this, jint id) { - char *id_string = g_strdup_printf("%d", id); - if (portal) - xdp_portal_remove_notification(portal, id_string); - g_free(id_string); + queue_notification(id, NULL); } static void remove_ongoing_notification(gpointer key, gpointer value, gpointer user_data) { 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); } diff --git a/src/api-impl-jni/generated_headers/android_app_NotificationManager.h b/src/api-impl-jni/generated_headers/android_app_NotificationManager.h index 8e69870c..9cf344a8 100644 --- a/src/api-impl-jni/generated_headers/android_app_NotificationManager.h +++ b/src/api-impl-jni/generated_headers/android_app_NotificationManager.h @@ -18,18 +18,18 @@ JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder /* * Class: android_app_NotificationManager * 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 - (JNIEnv *, jobject, jlong, jstring, jint, jstring, jstring); + (JNIEnv *, jobject, jlong, jstring, jint, jstring, jstring, jstring); /* * Class: android_app_NotificationManager * 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 - (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 diff --git a/src/api-impl/android/app/NotificationManager.java b/src/api-impl/android/app/NotificationManager.java index a8bb77d2..0e1849bc 100644 --- a/src/api-impl/android/app/NotificationManager.java +++ b/src/api-impl/android/app/NotificationManager.java @@ -1,15 +1,13 @@ package android.app; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; import android.app.Notification.MediaStyle; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -17,9 +15,6 @@ public class NotificationManager { private static int mpris_notification_id = -1; - // store Intents in map, as long as Parcelable serialization is not yet implemented - private static Map intents = new HashMap(); - public void cancelAll() {} public void notify(String tag, int id, Notification notification) { @@ -37,23 +32,26 @@ public class NotificationManager { int intentType = -1; String actionName = null; String className = null; + String data = null; if (action.intent != null) { intentType = action.intent.type; actionName = action.intent.intent.getAction(); 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; String actionName = null; String className = null; + String data = null; if (notification.intent != null) { intentType = notification.intent.type; actionName = notification.intent.intent.getAction(); 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) { @@ -80,16 +78,14 @@ public class NotificationManager { 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; action = "".equals(action) ? null : action; className = "".equals(className) ? null : className; - Intent intent = intents.remove(id); - if (intent == null || !Objects.equals(action, intent.getAction()) || !Objects.equals(className, intent.getComponent() == null ? null : intent.getComponent().getClassName())) { - intent = new Intent(action); - if (className != null) { - intent.setComponent(new ComponentName(context, className)); - } + data = "".equals(data) ? null : data; + Intent intent = new Intent(action, data != null ? Uri.parse(data) : null); + if (className != null) { + intent.setComponent(new ComponentName(context, className)); } if (intentType == 0) { // type Activity context.startActivity(intent); @@ -103,8 +99,8 @@ public class NotificationManager { public void createNotificationChannel(NotificationChannel channel) {} protected native long nativeInitBuilder(); - protected native void nativeAddAction(long builder, String title, 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); + 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, String data); protected native void nativeShowMPRIS(String packageName, String identiy); protected native void nativeCancel(int id); protected native void nativeCancelMPRIS(); diff --git a/src/main-executable/main.c b/src/main-executable/main.c index 04b84657..320ac33d 100644 --- a/src/main-executable/main.c +++ b/src/main-executable/main.c @@ -780,7 +780,12 @@ int main(int argc, char **argv) callback_data->extra_jvm_options = 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 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, "open", G_CALLBACK(open), callback_data); status = g_application_run(G_APPLICATION(app), argc, argv); - g_object_unref(app); remove_ongoing_notifications(); + g_object_unref(app); return status; }