summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPo Lu <luangruo@yahoo.com>2024-03-11 21:40:47 +0800
committerPo Lu <luangruo@yahoo.com>2024-03-11 21:41:14 +0800
commita7a37341cad230448e487d0ffa343eeeb8a66a65 (patch)
treeab06b5826c5ae7bdd5b3dcc85b3ecba8dbee8c84
parent75cfc6c73faa1561018b1212156964a7919c69fe (diff)
downloademacs-a7a37341cad230448e487d0ffa343eeeb8a66a65.tar.gz
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.
-rw-r--r--doc/lispref/os.texi3
-rw-r--r--java/org/gnu/emacs/EmacsActivity.java21
-rw-r--r--java/org/gnu/emacs/EmacsDesktopNotification.java162
-rw-r--r--java/org/gnu/emacs/EmacsNative.java6
-rw-r--r--src/android.c161
-rw-r--r--src/android.h7
-rw-r--r--src/androidgui.h29
-rw-r--r--src/androidselect.c210
-rw-r--r--src/androidterm.c22
-rw-r--r--src/androidterm.h6
-rw-r--r--src/androidvfs.c2
-rw-r--r--src/keyboard.c10
-rw-r--r--src/termhooks.h4
13 files changed, 608 insertions, 35 deletions
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.NotificationManager;
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 @@ import android.widget.RemoteViews;
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 @@ public final class EmacsDesktopNotification
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 @@ public final class EmacsDesktopNotification
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 @@ public final class EmacsDesktopNotification
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 @@ public final class EmacsDesktopNotification
}
});
}
+
+
+
+ /* 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 final class EmacsNative
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 <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);
}
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 <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);
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. */