separate Intent serialization and action handling out of the NotificationManager

This way, the mechanism can in the future also be used to send Intents
from one app to another.
This commit is contained in:
Julian Winkler
2025-07-07 16:41:10 +02:00
parent 3708cc990e
commit 4ec150c802
9 changed files with 171 additions and 90 deletions

View File

@@ -16,14 +16,15 @@ static GHashTable *ongoing_notifications = NULL;
/* 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 */
order at the desktop environment.
Normally 20ms should be enough to prevent notification update order issues, but we use a 10x larger value
to be safe and 200ms should be more than enough as notification update interval. */
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)
{
printf("Sending notifications\n");
GApplication *app = g_application_get_default();
GHashTableIter iter;
gpointer key, value;
@@ -58,20 +59,6 @@ static void unref_nullsafe(void *data) {
g_object_unref(data);
}
static void notification_action(GSimpleAction *action, GVariant* parameter, gpointer user_data)
{
printf("notification_action\n");
int type;
const char *actionName;
const char *className;
const char *data;
JNIEnv *env = get_jni_env();
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));
}
static void queue_notification(int id, GNotification *notification) {
g_mutex_lock(&pending_notifications_mutex);
if (!pending_notifications) {
@@ -81,22 +68,21 @@ static void queue_notification(int id, GNotification *notification) {
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
/* Some desktop environments don't allow XDG-portal notifications without a desktop file.
There is no public API to force a specific backend, so we have to set the environment variable.
The GNOTIFICATION_BACKEND variable will be read by GIO the first time the notification backend is used.
This method should be future proof unless the freedesktop backend is removed. */
if (!info)
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
}
if (g_source_get_ready_time(send_notifcation_timer) == -1)
g_source_set_ready_time(send_notifcation_timer, 0); // immediately
}
JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder(JNIEnv *env, jobject this)
@@ -104,32 +90,18 @@ JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder(J
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)
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction(JNIEnv *env, jobject this, jlong builder_ptr, jstring name_jstr, jint type, jobject intent)
{
GNotification *notification = _PTR(builder_ptr);
const char *name = "";
if (name_jstr) {
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) {
g_notification_add_button_with_target_value(notification, name, intent_actionname_from_type(type), intent_serialize(env, intent));
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)
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, jobject intent)
{
GNotification *notification = _PTR(builder_ptr);
@@ -157,7 +129,7 @@ JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotificati
g_free(icon_path_full);
(*env)->ReleaseStringUTFChars(env, icon_jstr, icon_path);
}
g_notification_set_default_action_and_target_value(notification, "app.notificationaction", serialize_intent(env, type, action, className, data));
g_notification_set_default_action_and_target_value(notification, intent_actionname_from_type(type), intent_serialize(env, intent));
queue_notification(id, notification);
if (ongoing)
g_hash_table_add(ongoing_notifications, GINT_TO_POINTER(id));

View File

@@ -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;Ljava/lang/String;)V
* Signature: (JLjava/lang/String;ILandroid/content/Intent;)V
*/
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction
(JNIEnv *, jobject, jlong, jstring, jint, jstring, jstring, jstring);
(JNIEnv *, jobject, jlong, jstring, jint, jobject);
/*
* Class: android_app_NotificationManager
* Method: nativeShowNotification
* Signature: (JILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
* Signature: (JILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILandroid/content/Intent;)V
*/
JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotification
(JNIEnv *, jobject, jlong, jint, jstring, jstring, jstring, jboolean, jint, jstring, jstring, jstring);
(JNIEnv *, jobject, jlong, jint, jstring, jstring, jstring, jboolean, jint, jobject);
/*
* Class: android_app_NotificationManager

View File

@@ -159,6 +159,8 @@ void set_up_handle_cache(JNIEnv *env)
if((*env)->ExceptionCheck(env))
(*env)->ExceptionDescribe(env);
handle_cache.context.sendBroadcast = _METHOD(handle_cache.context.class, "sendBroadcast", "(Landroid/content/Intent;)V");
handle_cache.context.startActivity = _METHOD(handle_cache.context.class, "startActivity", "(Landroid/content/Intent;)V");
handle_cache.context.startService = _METHOD(handle_cache.context.class, "startService", "(Landroid/content/Intent;)Landroid/content/ComponentName;");
handle_cache.application.class = _REF((*env)->FindClass(env, "android/app/Application"));
handle_cache.application.get_app_icon_path = _METHOD(handle_cache.application.class, "get_app_icon_path", "()Ljava/lang/String;");
@@ -179,6 +181,8 @@ void set_up_handle_cache(JNIEnv *env)
handle_cache.intent.constructor = _METHOD(handle_cache.intent.class, "<init>", "()V");
handle_cache.intent.putExtraCharSequence = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;Ljava/lang/CharSequence;)Landroid/content/Intent;");
handle_cache.intent.putExtraByteArray = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;[B)Landroid/content/Intent;");
handle_cache.intent.getDataString = _METHOD(handle_cache.intent.class, "getDataString", "()Ljava/lang/String;");
handle_cache.intent.setClassName = _METHOD(handle_cache.intent.class, "setClassName", "(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;");
handle_cache.instrumentation.class = _REF((*env)->FindClass(env, "android/app/Instrumentation"));
@@ -188,6 +192,9 @@ void set_up_handle_cache(JNIEnv *env)
handle_cache.canvas.class = _REF((*env)->FindClass(env, "android/graphics/Canvas"));
handle_cache.canvas.drawText = _METHOD(handle_cache.canvas.class, "drawText", "(Ljava/lang/CharSequence;IIFFLandroid/graphics/Paint;)V");
handle_cache.uri.class = _REF((*env)->FindClass(env, "android/net/Uri"));
handle_cache.uri.parse = _STATIC_METHOD(handle_cache.uri.class, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
}
void extract_from_apk(const char *path, const char *target) {
@@ -371,3 +378,56 @@ void atl_safe_gtk_widget_queue_resize(GtkWidget *widget)
gtk_widget_add_tick_callback(widget, queue_queue_resize, NULL, NULL);
}
}
GVariant *intent_serialize(JNIEnv *env, jobject intent) {
jstring action_jstr = _GET_OBJ_FIELD(intent, "action", "Ljava/lang/String;");
jobject component = _GET_OBJ_FIELD(intent, "component", "Landroid/content/ComponentName;");
jstring className_jstr = component ? _GET_OBJ_FIELD(component, "mClass", "Ljava/lang/String;") : NULL;
jstring data_jstr = (*env)->CallObjectMethod(env, intent, handle_cache.intent.getDataString);
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 *variant = g_variant_new(INTENT_G_VARIANT_TYPE_STRING, 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 variant;
}
jobject intent_deserialize(JNIEnv *env, GVariant *variant) {
const char *action;
const char *className;
const char *data;
g_variant_get(variant, INTENT_G_VARIANT_TYPE_STRING, &action, &className, &data);
if (action && action[0] == '\0')
action = NULL;
if (className && className[0] == '\0')
className = NULL;
if (data && data[0] == '\0')
data = NULL;
jobject intent = (*env)->NewObject(env, handle_cache.intent.class, handle_cache.intent.constructor);
_SET_OBJ_FIELD(intent, "action", "Ljava/lang/String;", _JSTRING(action));
if (className)
(*env)->CallObjectMethod(env, intent, handle_cache.intent.setClassName, _GET_STATIC_OBJ_FIELD(handle_cache.context.class, "this_application", "Landroid/app/Application;"), _JSTRING(className));
if (data)
_SET_OBJ_FIELD(intent, "data", "Landroid/net/Uri;", (*env)->CallStaticObjectMethod(env, handle_cache.uri.class, handle_cache.uri.parse, _JSTRING(data)));
return intent;
}
const char *intent_actionname_from_type(int type) {
switch (type) {
case 0:
return "app.startActivity";
case 1:
return "app.startService";
case 2:
return "app.sendBroadcast";
default:
return NULL;
}
}

View File

@@ -103,6 +103,8 @@ struct handle_cache {
jclass class;
jmethodID get_package_name;
jmethodID sendBroadcast;
jmethodID startActivity;
jmethodID startService;
} context;
struct {
jclass class;
@@ -128,6 +130,8 @@ struct handle_cache {
jmethodID constructor;
jmethodID putExtraCharSequence;
jmethodID putExtraByteArray;
jmethodID getDataString;
jmethodID setClassName;
} intent;
struct {
jclass class;
@@ -143,6 +147,10 @@ struct handle_cache {
jclass class;
jmethodID drawText;
} canvas;
struct {
jclass class;
jmethodID parse;
} uri;
};
extern struct handle_cache handle_cache;
@@ -195,4 +203,9 @@ void atl_safe_gtk_widget_set_visible(GtkWidget *widget, gboolean visible);
void atl_safe_gtk_widget_queue_allocate(GtkWidget *widget);
void atl_safe_gtk_widget_queue_resize(GtkWidget *widget);
#define INTENT_G_VARIANT_TYPE_STRING "(sss)" // (action, className, data)
GVariant *intent_serialize(JNIEnv *env, jobject intent);
jobject intent_deserialize(JNIEnv *env, GVariant *variant);
const char *intent_actionname_from_type(int type);
#endif