emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

master a7a37341cad: Implement notification callbacks on Android


From: Po Lu
Subject: master a7a37341cad: Implement notification callbacks on Android
Date: Mon, 11 Mar 2024 09:41:31 -0400 (EDT)

branch: master
commit a7a37341cad230448e487d0ffa343eeeb8a66a65
Author: Po Lu <luangruo@yahoo.com>
Commit: Po Lu <luangruo@yahoo.com>

    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.
---
 doc/lispref/os.texi                              |   3 +
 java/org/gnu/emacs/EmacsActivity.java            |  21 +++
 java/org/gnu/emacs/EmacsDesktopNotification.java | 162 +++++++++++++++--
 java/org/gnu/emacs/EmacsNative.java              |   6 +
 src/android.c                                    | 161 ++++++++++++++++-
 src/android.h                                    |   7 +
 src/androidgui.h                                 |  29 ++++
 src/androidselect.c                              | 210 +++++++++++++++++++++--
 src/androidterm.c                                |  22 ++-
 src/androidterm.h                                |   6 +
 src/androidvfs.c                                 |   2 +-
 src/keyboard.c                                   |  10 ++
 src/termhooks.h                                  |   4 +
 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.  */



reply via email to

[Prev in Thread] Current Thread [Next in Thread]