diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi index 60ae57d4c1d..ecd88a39489 100644 --- a/doc/lispref/os.texi +++ b/doc/lispref/os.texi @@ -3241,6 +3241,9 @@ of parameters analogous to its namesake in @item :title @var{title} @item :body @var{body} @item :replaces-id @var{replaces-id} +@item :on-action @var{on-action} +@item :on-cancel @var{on-cancel} +@item :actions @var{actions} These have the same meaning as they do when used in calls to @code{notifications-notify}. diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 66a1e41d84c..06b9c0f005d 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -453,6 +453,27 @@ public class EmacsActivity extends Activity syncFullscreenWith (window); } + @Override + public final void + onNewIntent (Intent intent) + { + String tag, action; + + /* This function is called when EmacsActivity is relaunched from a + notification. */ + + if (intent == null || EmacsService.SERVICE == null) + return; + + tag = intent.getStringExtra (EmacsDesktopNotification.NOTIFICATION_TAG); + action + = intent.getStringExtra (EmacsDesktopNotification.NOTIFICATION_ACTION); + + if (tag == null || action == null) + return; + + EmacsNative.sendNotificationAction (tag, action); + } @Override diff --git a/java/org/gnu/emacs/EmacsDesktopNotification.java b/java/org/gnu/emacs/EmacsDesktopNotification.java index fb35e3fea1f..f52c3d9d4fb 100644 --- a/java/org/gnu/emacs/EmacsDesktopNotification.java +++ b/java/org/gnu/emacs/EmacsDesktopNotification.java @@ -24,9 +24,12 @@ import android.app.NotificationChannel; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.net.Uri; + import android.os.Build; import android.widget.RemoteViews; @@ -44,6 +47,16 @@ public final class EmacsDesktopNotification { + /* Intent tag for notification action data. */ + public static final String NOTIFICATION_ACTION = "emacs:notification_action"; + + /* Intent tag for notification IDs. */ + public static final String NOTIFICATION_TAG = "emacs:notification_tag"; + + /* Action ID assigned to the broadcast receiver which should be + notified of any notification's being dismissed. */ + public static final String NOTIFICATION_DISMISSED = "org.gnu.emacs.DISMISSED"; + /* The content of this desktop notification. */ public final String content; @@ -66,10 +79,15 @@ public final class EmacsDesktopNotification /* The importance of this notification's group. */ public final int importance; + /* Array of actions and their user-facing text to be offered by this + notification. */ + public final String[] actions, titles; + public EmacsDesktopNotification (String title, String content, String group, String tag, int icon, - int importance) + int importance, + String[] actions, String[] titles) { this.content = content; this.title = title; @@ -77,12 +95,68 @@ public final class EmacsDesktopNotification this.tag = tag; this.icon = icon; this.importance = importance; + this.actions = actions; + this.titles = titles; } /* Functions for displaying desktop notifications. */ + /* Insert each action in actions and titles into the notification + builder BUILDER, with pending intents created with CONTEXT holding + suitable metadata. */ + + @SuppressWarnings ("deprecation") + private void + insertActions (Context context, Notification.Builder builder) + { + int i; + PendingIntent pending; + Intent intent; + Notification.Action.Builder action; + + if (actions == null) + return; + + for (i = 0; i < actions.length; ++i) + { + /* Actions named default should not be displayed. */ + if (actions[i].equals ("default")) + continue; + + intent = new Intent (context, EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + + /* Pending intents are specific to combinations of class, action + and data, but not information provided as extras. In order + that its target may be invoked with the action and tag set + below, generate a URL from those two elements and specify it + as the intent data, which ensures that the intent allocated + fully reflects the duo. */ + + intent.setData (new Uri.Builder ().scheme ("action") + .appendPath (tag).appendPath (actions[i]) + .build ()); + intent.putExtra (NOTIFICATION_ACTION, actions[i]); + intent.putExtra (NOTIFICATION_TAG, tag); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + pending = PendingIntent.getActivity (context, 0, intent, + PendingIntent.FLAG_IMMUTABLE); + else + pending = PendingIntent.getActivity (context, 0, intent, 0); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + { + action = new Notification.Action.Builder (0, titles[i], pending); + builder.addAction (action.build ()); + } + else + builder.addAction (0, titles[i], pending); + } + } + /* Internal helper for `display' executed on the main thread. */ @SuppressWarnings ("deprecation") /* Notification.Builder (Context). */ @@ -97,6 +171,7 @@ public final class EmacsDesktopNotification Intent intent; PendingIntent pending; int priority; + Notification.Builder builder; tem = context.getSystemService (Context.NOTIFICATION_SERVICE); manager = (NotificationManager) tem; @@ -108,13 +183,16 @@ public final class EmacsDesktopNotification (such as its importance) will be overridden. */ channel = new NotificationChannel (group, group, importance); manager.createNotificationChannel (channel); + builder = new Notification.Builder (context, group); - /* Create a notification object and display it. */ - notification = (new Notification.Builder (context, group) - .setContentTitle (title) - .setContentText (content) - .setSmallIcon (icon) - .build ()); + /* Create and configure a notification object and display + it. */ + + builder.setContentTitle (title); + builder.setContentText (content); + builder.setSmallIcon (icon); + insertActions (context, builder); + notification = builder.build (); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { @@ -138,12 +216,16 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) break; } - notification = (new Notification.Builder (context) - .setContentTitle (title) - .setContentText (content) - .setSmallIcon (icon) - .setPriority (priority) - .build ()); + builder = new Notification.Builder (context); + builder.setContentTitle (title); + builder.setContentText (content); + builder.setSmallIcon (icon); + builder.setPriority (priority); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + insertActions (context, builder); + + notification = builder.build (); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) notification.priority = priority; @@ -170,6 +252,12 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) intent = new Intent (context, EmacsActivity.class); intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData (new Uri.Builder () + .scheme ("action") + .appendPath (tag) + .build ()); + intent.putExtra (NOTIFICATION_ACTION, "default"); + intent.putExtra (NOTIFICATION_TAG, tag); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) pending = PendingIntent.getActivity (context, 0, intent, @@ -179,6 +267,27 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) notification.contentIntent = pending; + /* Provide a cancellation intent to respond to notification + dismissals. */ + + intent = new Intent (context, CancellationReceiver.class); + intent.setAction (NOTIFICATION_DISMISSED); + intent.setPackage ("org.gnu.emacs"); + intent.setData (new Uri.Builder () + .scheme ("action") + .appendPath (tag) + .build ()); + intent.putExtra (NOTIFICATION_TAG, tag); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + pending = PendingIntent.getBroadcast (context, 0, intent, + (PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_ONE_SHOT)); + else + pending = PendingIntent.getBroadcast (context, 0, intent, + PendingIntent.FLAG_ONE_SHOT); + + notification.deleteIntent = pending; manager.notify (tag, 2, notification); } @@ -199,4 +308,31 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) } }); } + + + + /* Broadcast receiver. This is something of a system-wide callback + arranged to be invoked whenever a notification posted by Emacs is + dismissed, in order to relay news of its dismissal to + androidselect.c and run or remove callbacks as appropriate. */ + + public static class CancellationReceiver extends BroadcastReceiver + { + @Override + public void + onReceive (Context context, Intent intent) + { + String tag, action; + + if (intent == null || EmacsService.SERVICE == null) + return; + + tag = intent.getStringExtra (NOTIFICATION_TAG); + + if (tag == null) + return; + + EmacsNative.sendNotificationDeleted (tag); + } + }; }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cd0e70923d1..6845f833908 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -196,6 +196,12 @@ public static native long sendDndUri (short window, int x, int y, public static native long sendDndText (short window, int x, int y, String text); + /* Send an ANDROID_NOTIFICATION_CANCELED event. */ + public static native void sendNotificationDeleted (String tag); + + /* Send an ANDROID_NOTIFICATION_ACTION event. */ + public static native void sendNotificationAction (String tag, String action); + /* Return the file name associated with the specified file descriptor, or NULL if there is none. */ public static native byte[] getProcName (int fd); diff --git a/src/android.c b/src/android.c index d7bd06f1f34..125bb5209c3 100644 --- a/src/android.c +++ b/src/android.c @@ -2457,7 +2457,7 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, return event_serial; } -JNIEXPORT jboolean JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDndDrag) (JNIEnv *env, jobject object, jshort window, jint x, jint y) { @@ -2477,7 +2477,7 @@ NATIVE_NAME (sendDndDrag) (JNIEnv *env, jobject object, return event_serial; } -JNIEXPORT jboolean JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDndUri) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jstring string) @@ -2514,7 +2514,7 @@ NATIVE_NAME (sendDndUri) (JNIEnv *env, jobject object, return event_serial; } -JNIEXPORT jboolean JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDndText) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jstring string) @@ -2551,6 +2551,85 @@ NATIVE_NAME (sendDndText) (JNIEnv *env, jobject object, return event_serial; } +JNIEXPORT jlong JNICALL +NATIVE_NAME (sendNotificationDeleted) (JNIEnv *env, jobject object, + jstring tag) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + const char *characters; + + event.notification.type = ANDROID_NOTIFICATION_DELETED; + event.notification.serial = ++event_serial; + event.notification.window = ANDROID_NONE; + + /* TAG is guaranteed to be an ASCII string, of which the JNI character + encoding is a superset. */ + characters = (*env)->GetStringUTFChars (env, tag, NULL); + if (!characters) + return 0; + + event.notification.tag = strdup (characters); + (*env)->ReleaseStringUTFChars (env, tag, characters); + if (!event.notification.tag) + return 0; + + event.notification.action = NULL; + event.notification.length = 0; + + android_write_event (&event); + return event_serial; +} + +JNIEXPORT jlong JNICALL +NATIVE_NAME (sendNotificationAction) (JNIEnv *env, jobject object, + jstring tag, jstring action) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + const void *characters; + jsize length; + uint16_t *buffer; + + event.notification.type = ANDROID_NOTIFICATION_ACTION; + event.notification.serial = ++event_serial; + event.notification.window = ANDROID_NONE; + + /* TAG is guaranteed to be an ASCII string, of which the JNI character + encoding is a superset. */ + characters = (*env)->GetStringUTFChars (env, tag, NULL); + if (!characters) + return 0; + + event.notification.tag = strdup (characters); + (*env)->ReleaseStringUTFChars (env, tag, characters); + if (!event.notification.tag) + return 0; + + length = (*env)->GetStringLength (env, action); + buffer = malloc (length * sizeof *buffer); + characters = (*env)->GetStringChars (env, action, NULL); + + if (!characters) + { + /* The JVM has run out of memory; return and let the out of memory + error take its course. */ + xfree (event.notification.tag); + return 0; + } + + memcpy (buffer, characters, length * sizeof *buffer); + (*env)->ReleaseStringChars (env, action, characters); + + event.notification.action = buffer; + event.notification.length = length; + + android_write_event (&event); + return event_serial; +} + JNIEXPORT jboolean JNICALL NATIVE_NAME (shouldForwardMultimediaButtons) (JNIEnv *env, jobject object) @@ -6310,6 +6389,82 @@ android_exception_check_4 (jobject object, jobject object1, memory_full (0); } +/* Like android_exception_check_4, except it takes more than four local + reference arguments. */ + +void +android_exception_check_5 (jobject object, jobject object1, + jobject object2, jobject object3, + jobject object4) +{ + if (likely (!(*android_java_env)->ExceptionCheck (android_java_env))) + return; + + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error. " + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + + if (object) + ANDROID_DELETE_LOCAL_REF (object); + + if (object1) + ANDROID_DELETE_LOCAL_REF (object1); + + if (object2) + ANDROID_DELETE_LOCAL_REF (object2); + + if (object3) + ANDROID_DELETE_LOCAL_REF (object3); + + if (object4) + ANDROID_DELETE_LOCAL_REF (object4); + + memory_full (0); +} + + +/* Like android_exception_check_5, except it takes more than five local + reference arguments. */ + +void +android_exception_check_6 (jobject object, jobject object1, + jobject object2, jobject object3, + jobject object4, jobject object5) +{ + if (likely (!(*android_java_env)->ExceptionCheck (android_java_env))) + return; + + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error. " + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + + if (object) + ANDROID_DELETE_LOCAL_REF (object); + + if (object1) + ANDROID_DELETE_LOCAL_REF (object1); + + if (object2) + ANDROID_DELETE_LOCAL_REF (object2); + + if (object3) + ANDROID_DELETE_LOCAL_REF (object3); + + if (object4) + ANDROID_DELETE_LOCAL_REF (object4); + + if (object5) + ANDROID_DELETE_LOCAL_REF (object5); + + memory_full (0); +} + /* Check for JNI problems based on the value of OBJECT. Signal out of memory if OBJECT is NULL. OBJECT1 means the diff --git a/src/android.h b/src/android.h index e1834cebf68..ee634a3e76c 100644 --- a/src/android.h +++ b/src/android.h @@ -118,6 +118,10 @@ extern void android_exception_check_1 (jobject); extern void android_exception_check_2 (jobject, jobject); extern void android_exception_check_3 (jobject, jobject, jobject); extern void android_exception_check_4 (jobject, jobject, jobject, jobject); +extern void android_exception_check_5 (jobject, jobject, jobject, jobject, + jobject); +extern void android_exception_check_6 (jobject, jobject, jobject, jobject, + jobject, jobject); extern void android_exception_check_nonnull (void *, jobject); extern void android_exception_check_nonnull_1 (void *, jobject, jobject); @@ -306,6 +310,9 @@ extern JNIEnv *android_java_env; extern JavaVM *android_jvm; #endif /* THREADS_ENABLED */ +/* The Java String class. */ +extern jclass java_string_class; + /* The EmacsService object. */ extern jobject emacs_service; diff --git a/src/androidgui.h b/src/androidgui.h index 73b60c483d3..d89aee51055 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -251,6 +251,8 @@ enum android_event_type ANDROID_DND_DRAG_EVENT, ANDROID_DND_URI_EVENT, ANDROID_DND_TEXT_EVENT, + ANDROID_NOTIFICATION_DELETED, + ANDROID_NOTIFICATION_ACTION, }; struct android_any_event @@ -535,6 +537,29 @@ struct android_dnd_event size_t length; }; +struct android_notification_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* The event serial. */ + unsigned long serial; + + /* The window that gave rise to the event (None). */ + android_window window; + + /* The identifier of the notification whose status changed. + Must be deallocated with `free'. */ + char *tag; + + /* The action that was activated, if any. Must be deallocated with + `free'. */ + unsigned short *action; + + /* Length of that data. */ + size_t length; +}; + union android_event { enum android_event_type type; @@ -571,6 +596,10 @@ union android_event protocol, whereas there exist several competing X protocols implemented in terms of X client messages. */ struct android_dnd_event dnd; + + /* X provides no equivalent interface for displaying + notifications. */ + struct android_notification_event notification; }; enum diff --git a/src/androidselect.c b/src/androidselect.c index 61f1c6045db..04f4cf1573f 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -30,6 +30,7 @@ along with GNU Emacs. If not, see . */ #include "coding.h" #include "android.h" #include "androidterm.h" +#include "termhooks.h" /* Selection support on Android is confined to copying and pasting of plain text and MIME data from the clipboard. There is no primary @@ -490,6 +491,9 @@ struct android_emacs_desktop_notification /* Methods provided by the EmacsDesktopNotification class. */ static struct android_emacs_desktop_notification notification_class; +/* Hash table pairing notification identifiers with callbacks. */ +static Lisp_Object notification_table; + /* Initialize virtual function IDs and class pointers tied to the EmacsDesktopNotification class. */ @@ -521,7 +525,8 @@ android_init_emacs_desktop_notification (void) FIND_METHOD (init, "", "(Ljava/lang/String;" "Ljava/lang/String;Ljava/lang/String;" - "Ljava/lang/String;II)V"); + "Ljava/lang/String;II[Ljava/lang/String;" + "[Ljava/lang/String;)V"); FIND_METHOD (display, "display", "()V"); #undef FIND_METHOD } @@ -562,25 +567,32 @@ android_locate_icon (const char *name) } /* Display a desktop notification with the provided TITLE, BODY, - REPLACES_ID, GROUP, ICON, and URGENCY. Return an identifier for - the resulting notification. */ + REPLACES_ID, GROUP, ICON, URGENCY, ACTIONS, ACTION_CB and CANCEL_CB. + Return an identifier for the resulting notification. */ static intmax_t android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, Lisp_Object replaces_id, Lisp_Object group, Lisp_Object icon, - Lisp_Object urgency) + Lisp_Object urgency, Lisp_Object actions, + Lisp_Object action_cb, + Lisp_Object cancel_cb) { static intmax_t counter; intmax_t id; jstring title1, body1, group1, identifier1; jint type, icon1; jobject notification; + jobjectArray action_keys, action_titles; char identifier[INT_STRLEN_BOUND (int) + INT_STRLEN_BOUND (long int) + INT_STRLEN_BOUND (intmax_t) + sizeof "..."]; struct timespec boot_time; + Lisp_Object key, value, tem; + jint nitems, i; + jstring item; + Lisp_Object length; if (EQ (urgency, Qlow)) type = 2; /* IMPORTANCE_LOW */ @@ -591,6 +603,29 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, else signal_error ("Invalid notification importance given", urgency); + nitems = 0; + + /* If ACTIONS is provided, split it into two arrays of Java strings + holding keys and titles. */ + + if (!NILP (actions)) + { + /* Count the number of items to be inserted. */ + + length = Flength (actions); + if (!TYPE_RANGED_FIXNUMP (jint, length)) + error ("Action list too long"); + nitems = XFIXNAT (length); + if (nitems & 1) + error ("Length of action list is invalid"); + nitems /= 2; + + /* Verify that the list consists exclusively of strings. */ + tem = actions; + FOR_EACH_TAIL (tem) + CHECK_STRING (XCAR (tem)); + } + if (NILP (replaces_id)) { /* Generate a new identifier. */ @@ -626,14 +661,62 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, = (*android_java_env)->NewStringUTF (android_java_env, identifier); android_exception_check_3 (title1, body1, group1); + /* Create the arrays for action identifiers and titles if + provided. */ + + if (nitems) + { + action_keys = (*android_java_env)->NewObjectArray (android_java_env, + nitems, + java_string_class, + NULL); + android_exception_check_4 (title, body1, group1, identifier1); + action_titles = (*android_java_env)->NewObjectArray (android_java_env, + nitems, + java_string_class, + NULL); + android_exception_check_5 (title, body1, group1, identifier1, + action_keys); + + for (i = 0; i < nitems; ++i) + { + key = XCAR (actions); + value = XCAR (XCDR (actions)); + actions = XCDR (XCDR (actions)); + + /* Create a string for this action. */ + item = android_build_string (key, body1, group1, identifier1, + action_keys, action_titles, NULL); + (*android_java_env)->SetObjectArrayElement (android_java_env, + action_keys, i, + item); + ANDROID_DELETE_LOCAL_REF (item); + + /* Create a string for this title. */ + item = android_build_string (value, body1, group1, identifier1, + action_keys, action_titles, NULL); + (*android_java_env)->SetObjectArrayElement (android_java_env, + action_titles, i, + item); + ANDROID_DELETE_LOCAL_REF (item); + } + } + else + { + action_keys = NULL; + action_titles = NULL; + } + /* Create the notification. */ notification = (*android_java_env)->NewObject (android_java_env, notification_class.class, notification_class.init, title1, body1, group1, - identifier1, icon1, type); - android_exception_check_4 (title1, body1, group1, identifier1); + identifier1, icon1, type, + action_keys, action_titles); + android_exception_check_6 (title1, body1, group1, identifier1, + action_titles, action_keys); /* Delete unused local references. */ ANDROID_DELETE_LOCAL_REF (title1); @@ -641,6 +724,12 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, ANDROID_DELETE_LOCAL_REF (group1); ANDROID_DELETE_LOCAL_REF (identifier1); + if (action_keys) + ANDROID_DELETE_LOCAL_REF (action_keys); + + if (action_titles) + ANDROID_DELETE_LOCAL_REF (action_titles); + /* Display the notification. */ (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, notification, @@ -649,6 +738,12 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, android_exception_check_1 (notification); ANDROID_DELETE_LOCAL_REF (notification); + /* If callbacks are provided, save them into notification_table. */ + + if (!NILP (action_cb) || !NILP (cancel_cb)) + Fputhash (build_string (identifier), Fcons (action_cb, cancel_cb), + notification_table); + /* Return the ID. */ return id; } @@ -659,14 +754,28 @@ DEFUN ("android-notifications-notify", Fandroid_notifications_notify, ARGS must contain keywords followed by values. Each of the following keywords is understood: - :title The notification title. - :body The notification body. - :replaces-id The ID of a previous notification to supersede. - :group The notification group, or nil. - :urgency One of the symbols `low', `normal' or `critical', - defining the importance of the notification group. - :icon The name of a drawable resource to display as the - notification's icon. + :title The notification title. + :body The notification body. + :replaces-id The ID of a previous notification to supersede. + :group The notification group, or nil. + :urgency One of the symbols `low', `normal' or `critical', + defining the importance of the notification group. + :icon The name of a drawable resource to display as the + notification's icon. + :actions A list of actions of the form: + (KEY TITLE KEY TITLE ...) + where KEY and TITLE are both strings. + The action for which CALLBACK is called when the + notification itself is selected is named "default", + its existence is implied, and its TITLE is ignored. + No more than three actions can be defined, not + counting any action with "default" as its key. + :on-action Function to call when an action is invoked. + The notification id and the key of the action are + provided as arguments to the function. + :on-cancel Function to call if the notification is dismissed, + with the notification id and the symbol `undefined' + for arguments. The notification group is ignored on Android 7.1 and earlier versions of Android. Outside such older systems, it identifies a category that @@ -686,6 +795,9 @@ within the "android.R.drawable" class designating an icon with a transparent background. If no icon is provided (or the icon is absent from this system), it defaults to "ic_dialog_alert". +Actions specified with :actions cannot be displayed on Android 4.0 and +earlier versions of the system. + When the system is running Android 13 or later, notifications sent will be silently disregarded unless permission to display notifications is expressly granted from the "App Info" settings panel @@ -701,14 +813,15 @@ usage: (android-notifications-notify &rest ARGS) */) { Lisp_Object title, body, replaces_id, group, urgency; Lisp_Object icon; - Lisp_Object key, value; + Lisp_Object key, value, actions, action_cb, cancel_cb; ptrdiff_t i; if (!android_init_gui) error ("No Android display connection!"); /* Clear each variable above. */ - title = body = replaces_id = group = icon = urgency = Qnil; + title = body = replaces_id = group = icon = urgency = actions = Qnil; + action_cb = cancel_cb = Qnil; /* If NARGS is odd, error. */ @@ -734,6 +847,12 @@ usage: (android-notifications-notify &rest ARGS) */) urgency = value; else if (EQ (key, QCicon)) icon = value; + else if (EQ (key, QCactions)) + actions = value; + else if (EQ (key, QCon_action)) + action_cb = value; + else if (EQ (key, QCon_cancel)) + cancel_cb = value; } /* Demand at least TITLE and BODY be present. */ @@ -758,7 +877,58 @@ usage: (android-notifications-notify &rest ARGS) */) CHECK_STRING (icon); return make_int (android_notifications_notify_1 (title, body, replaces_id, - group, icon, urgency)); + group, icon, urgency, + actions, action_cb, + cancel_cb)); +} + +/* Run callbacks in response to a notification being deleted. + Save any input generated for the keyboard within *IE. + EVENT should be the notification deletion event. */ + +void +android_notification_deleted (struct android_notification_event *event, + struct input_event *ie) +{ + Lisp_Object item, tag; + intmax_t id; + + tag = build_string (event->tag); + item = Fgethash (tag, notification_table, Qnil); + + if (!NILP (item)) + Fremhash (tag, notification_table); + + if (CONSP (item) && FUNCTIONP (XCDR (item)) + && sscanf (event->tag, "%*d.%*ld.%jd", &id) > 0) + { + ie->kind = NOTIFICATION_EVENT; + ie->arg = list3 (XCDR (item), make_int (id), + Qundefined); + } +} + +/* Run callbacks in response to one of a notification's actions being + invoked, saving any input generated for the keyboard within *IE. + EVENT should be the notification deletion event, and ACTION the + action key. */ + +void +android_notification_action (struct android_notification_event *event, + struct input_event *ie, Lisp_Object action) +{ + Lisp_Object item, tag; + intmax_t id; + + tag = build_string (event->tag); + item = Fgethash (tag, notification_table, Qnil); + + if (CONSP (item) && FUNCTIONP (XCAR (item)) + && sscanf (event->tag, "%*d.%*ld.%jd", &id) > 0) + { + ie->kind = NOTIFICATION_EVENT; + ie->arg = list3 (XCAR (item), make_int (id), action); + } } @@ -800,6 +970,9 @@ syms_of_androidselect (void) DEFSYM (QCgroup, ":group"); DEFSYM (QCurgency, ":urgency"); DEFSYM (QCicon, ":icon"); + DEFSYM (QCactions, ":actions"); + DEFSYM (QCon_action, ":on-action"); + DEFSYM (QCon_cancel, ":on-cancel"); DEFSYM (Qlow, "low"); DEFSYM (Qnormal, "normal"); @@ -814,4 +987,7 @@ syms_of_androidselect (void) defsubr (&Sandroid_get_clipboard_data); defsubr (&Sandroid_notifications_notify); + + notification_table = CALLN (Fmake_hash_table, QCtest, Qequal); + staticpro (¬ification_table); } diff --git a/src/androidterm.c b/src/androidterm.c index baf26abe322..f68f8a9ef62 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -1761,6 +1761,26 @@ handle_one_android_event (struct android_display_info *dpyinfo, free (event->dnd.uri_or_string); goto OTHER; + case ANDROID_NOTIFICATION_DELETED: + case ANDROID_NOTIFICATION_ACTION: + + if (event->notification.type == ANDROID_NOTIFICATION_DELETED) + android_notification_deleted (&event->notification, &inev.ie); + else + { + Lisp_Object action; + + action = android_decode_utf16 (event->notification.action, + event->notification.length); + android_notification_action (&event->notification, &inev.ie, + action); + } + + /* Free dynamically allocated data. */ + free (event->notification.tag); + free (event->notification.action); + goto OTHER; + default: goto OTHER; } @@ -4740,7 +4760,7 @@ android_sync_edit (void) /* Return a copy of the specified Java string and its length in *LENGTH. Use the JNI environment ENV. Value is NULL if copying - *the string fails. */ + the string fails. */ static unsigned short * android_copy_java_string (JNIEnv *env, jstring string, size_t *length) diff --git a/src/androidterm.h b/src/androidterm.h index 41c93067e82..ca6929bef0e 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -25,6 +25,7 @@ along with GNU Emacs. If not, see . */ #include "character.h" #include "dispextern.h" #include "font.h" +#include "termhooks.h" struct android_bitmap_record { @@ -464,6 +465,11 @@ extern void syms_of_sfntfont_android (void); #ifndef ANDROID_STUBIFY +extern void android_notification_deleted (struct android_notification_event *, + struct input_event *); +extern void android_notification_action (struct android_notification_event *, + struct input_event *, Lisp_Object); + extern void init_androidselect (void); extern void syms_of_androidselect (void); diff --git a/src/androidvfs.c b/src/androidvfs.c index d618e351204..4bb652f3eb7 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -292,7 +292,7 @@ struct android_parcel_file_descriptor_class }; /* The java.lang.String class. */ -static jclass java_string_class; +jclass java_string_class; /* Fields and methods associated with the Cursor class. */ static struct android_cursor_class cursor_class; diff --git a/src/keyboard.c b/src/keyboard.c index 1ba74a59537..91faf4582fa 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4187,6 +4187,16 @@ kbd_buffer_get_event (KBOARD **kbp, break; } +#ifdef HAVE_ANDROID + case NOTIFICATION_EVENT: + { + kbd_fetch_ptr = next_kbd_event (event); + input_pending = readable_events (0); + CALLN (Fapply, XCAR (event->ie.arg), XCDR (event->ie.arg)); + break; + } +#endif /* HAVE_ANDROID */ + #ifdef HAVE_EXT_MENU_BAR case MENU_BAR_ACTIVATE_EVENT: { diff --git a/src/termhooks.h b/src/termhooks.h index 8defebb20bd..d828c62ce33 100644 --- a/src/termhooks.h +++ b/src/termhooks.h @@ -343,6 +343,10 @@ enum event_kind the notification that was clicked. */ , NOTIFICATION_CLICKED_EVENT #endif /* HAVE_HAIKU */ +#ifdef HAVE_ANDROID + /* In a NOTIFICATION_EVENT, .arg is a lambda to evaluate. */ + , NOTIFICATION_EVENT +#endif /* HAVE_ANDROID */ }; /* Bit width of an enum event_kind tag at the start of structs and unions. */