Implement notification callbacks on Android

* doc/lispref/os.texi (Desktop Notifications): Document that
:on-cancel, :on-action and :actions are now supported on
Android.

* java/org/gnu/emacs/EmacsActivity.java (onNewIntent): New
function.

* java/org/gnu/emacs/EmacsDesktopNotification.java
(NOTIFICATION_ACTION, NOTIFICATION_TAG, NOTIFICATION_DISMISSED):
New constants.  <actions, titles>: New fields.
(insertActions): New function.
(display1, display): Insert actions on Jelly Bean and up, and
arrange to be notified when the notification is dismissed.
(CancellationReceiver): New class.

* java/org/gnu/emacs/EmacsNative.java (sendNotificationDeleted)
(sendNotificationAction): New functions.

* src/android.c (sendDndDrag, sendDndUri, sendDndText): Correct
return types.
(sendNotificationDeleted, sendNotificationAction)
(android_exception_check_5, android_exception_check_6): New
functions.

* src/android.h:

* src/androidgui.h (struct android_notification_event): New
structure.
(union android_event): New member for notification events.

* src/androidselect.c (android_init_emacs_desktop_notification):
Update JNI signatures.
(android_notifications_notify_1, Fandroid_notifications_notify):
New arguments ACTIONS, ACTION_CB and CANCEL_CB.  Convert and
record them as appropriate.
(android_notification_deleted, android_notification_action): New
functions.
(syms_of_androidselect): Prepare a hash table of outstanding
notifications.
<QCactions, QCon_action, QCon_cancel> New defsyms.

* src/androidterm.c (handle_one_android_event)
<ANDROID_NOTIFICATION_DELETED>
<ANDROID_NOTIFICATION_ACTION>: Dispatch event contents to
androidselect.c for processing.

* src/androidterm.h:

* src/androidvfs.c (java_string_class): Export.

* src/keyboard.c (kbd_buffer_get_event) <NOTIFICATION_EVENT>:
Call callback specified by the event.

* src/termhooks.h (enum event_kind) [HAVE_ANDROID]: New
enum NOTIFICATION_EVENT.
This commit is contained in:
Po Lu 2024-03-11 21:40:47 +08:00
parent 75cfc6c73f
commit a7a37341ca
13 changed files with 608 additions and 35 deletions

View file

@ -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}.

View file

@ -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

View file

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

View file

@ -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);

View file

@ -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

View file

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

View file

@ -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

View file

@ -30,6 +30,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#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, "<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 (&notification_table);
}

View file

@ -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)

View file

@ -25,6 +25,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#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);

View file

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

View file

@ -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:
{

View file

@ -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. */