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; }