emacs-diffs
[Top][All Lists]
Advanced

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

master 4f714dc0813: Support desktop notifications on Android


From: Po Lu
Subject: master 4f714dc0813: Support desktop notifications on Android
Date: Sun, 20 Aug 2023 08:26:30 -0400 (EDT)

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

    Support desktop notifications on Android
    
    * doc/emacs/android.texi (Android Environment): Correct list of
    permissions granted by default.
    
    * doc/lispref/os.texi (Desktop Notifications): Document the new
    function `android-notifications-notify' and its limitations.
    
    * java/AndroidManifest.xml.in: Request notification permissions.
    
    * java/org/gnu/emacs/EmacsDesktopNotification.java: New file.
    
    * java/res/layout/sdk8_notifications_view.xml: New file holding
    substitute notification widget definitions for Android versions
    prior to 3.0.
    
    * java/res/values/strings.xml: Remove inadvertently introduced
    tag attribute.
    
    * lisp/org/org-clock.el (haiku-notifications-notify): Correct
    file name in function declaration.
    (android-notifications-notify): New declaration.
    (org-show-notification): Use `android-notifications-notify'.
    
    * src/androidselect.c (android_init_emacs_desktop_notification)
    (android_notifications_notify_1, Fandroid_notifications_notify):
    New functions.
    (init_androidselect, syms_of_androidselect): Initialize new
    class and define new subr.
---
 doc/emacs/android.texi                           |   4 +-
 doc/lispref/os.texi                              |  44 ++++-
 java/AndroidManifest.xml.in                      |   5 +
 java/org/gnu/emacs/EmacsDesktopNotification.java | 152 ++++++++++++++
 java/res/layout/sdk8_notifications_view.xml      |  31 +++
 java/res/values/strings.xml                      |   2 +-
 lisp/org/org-clock.el                            |  10 +-
 src/androidselect.c                              | 242 +++++++++++++++++++++++
 8 files changed, 484 insertions(+), 6 deletions(-)

diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi
index 4dfe9098612..a85589f864c 100644
--- a/doc/emacs/android.texi
+++ b/doc/emacs/android.texi
@@ -491,6 +491,8 @@ permissions it has requested upon being installed:
 @code{android.permission.RECORD_AUDIO}
 @item
 @code{android.permission.CAMERA}
+@item
+@code{android.permission.POST_NOTIFICATIONS}
 @end itemize
 
 While most of these permissions are left unused by Emacs itself, they
@@ -516,8 +518,6 @@ permissions upon installation:
 @code{android.permission.TRANSMIT_IR}
 @item
 @code{android.permission.WAKE_LOCK}
-@item
-@code{android.permission.POST_NOTIFICATIONS}
 @end itemize
 
 Other permissions must be granted by the user through the system
diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi
index b6d34ae0a3d..cf65380a3ac 100644
--- a/doc/lispref/os.texi
+++ b/doc/lispref/os.texi
@@ -2850,8 +2850,8 @@ Emacs is restarted by the session manager.
 @cindex notifications, on desktop
 
 Emacs is able to send @dfn{notifications} on systems that support the
-freedesktop.org Desktop Notifications Specification, MS-Windows, and
-Haiku.
+freedesktop.org Desktop Notifications Specification, MS-Windows,
+Haiku, and Android.
 
 In order to use this functionality on POSIX hosts, Emacs must have
 been compiled with D-Bus support, and the @code{notifications} library
@@ -3200,6 +3200,46 @@ be exploited as the @code{:replaces-id} parameter to a 
subsequent call
 to this function.
 @end defun
 
+@cindex desktop notifications, Android
+When Emacs is built as an Android application package, displaying
+notifications is facilitated by the function
+@code{android-notifications-notify}.  This function does not feature
+call-backs, and has several idiosyncrasies, when compared to
+@code{notifications-notify}.
+
+@defun android-notifications-notify &rest params
+This function displays a desktop notification.  @var{params} is a list
+of parameters analogous to its namesake in
+@code{notifications-notify}.  The parameters are:
+
+@table @code
+@item :title @var{title}
+@item :body @var{body}
+@item :replaces-id @var{replaces-id}
+These have the same meaning as they do when used in calls to
+@code{notifications-notify}.
+
+@item :urgency @var{urgency}
+@item :group @var{group}
+These two parameters are ignored under Android 7.1 and earlier
+versions of the system.  The set of values for @var{urgency} is the
+same as with @code{notifications-notify}, but the urgency applies to
+all notifications displayed with the defined @var{group}.
+
+If @var{group} is nil or not present within @var{params}, it is
+replaced by the string @samp{"Desktop Notifications"}.
+@end table
+
+It returns a number identifying the notification, which may be
+supplied as the @code{:replaces-id} parameter to a later call to this
+function.
+
+If Emacs is not afforded the permission to display notifications
+(@pxref{Android Environment,,, emacs, The GNU Emacs Manual}) under
+Android 13 and later, any notifications sent will be silently
+disregarded.
+@end defun
+
 @node File Notifications
 @section Notifications on File Changes
 @cindex file notifications
diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in
index 895e7f88c57..b9cda401c9d 100644
--- a/java/AndroidManifest.xml.in
+++ b/java/AndroidManifest.xml.in
@@ -66,6 +66,11 @@ along with GNU Emacs.  If not, see 
<https://www.gnu.org/licenses/>. -->
 
   <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
 
+  <!-- And under Android 13 or later to post desktop
+       notifications.  -->
+
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+
   <uses-sdk android:minSdkVersion="@ANDROID_MIN_SDK@"
            android:targetSdkVersion="33"/>
 
diff --git a/java/org/gnu/emacs/EmacsDesktopNotification.java 
b/java/org/gnu/emacs/EmacsDesktopNotification.java
new file mode 100644
index 00000000000..8f55ffe8145
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDesktopNotification.java
@@ -0,0 +1,152 @@
+/* Communication module for Android terminals.  -*- c-file-style: "GNU" -*-
+
+Copyright (C) 2023 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or (at
+your option) any later version.
+
+GNU Emacs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.  */
+
+package org.gnu.emacs;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.NotificationChannel;
+
+import android.content.Context;
+
+import android.os.Build;
+
+import android.widget.RemoteViews;
+
+
+
+/* Structure designating a single desktop notification.
+
+   New versions of Android also organize notifications into individual
+   ``channels'', which are used to implement groups.  Unlike on other
+   systems, notification importance is set for each group, not for
+   each individual notification.  */
+
+
+
+public final class EmacsDesktopNotification
+{
+  /* The content of this desktop notification.  */
+  public final String content;
+
+  /* The title of this desktop notification.  */
+  public final String title;
+
+  /* The notification group.  */
+  public final String group;
+
+  /* String identifying this notification for future replacement.
+     Typically a string resembling ``XXXX.NNNN.YYYY'', where XXXX is
+     the system boot time, NNNN is the PID of this Emacs instance, and
+     YYYY is the counter value returned by the notifications display
+     function.  */
+  public final String tag;
+
+  /* The importance of this notification's group.  */
+  public final int importance;
+
+  public
+  EmacsDesktopNotification (String title, String content,
+                           String group, String tag, int importance)
+  {
+    this.content    = content;
+    this.title     = title;
+    this.group     = group;
+    this.tag        = tag;
+    this.importance = importance;
+  }
+
+
+
+  /* Functions for displaying desktop notifications.  */
+
+  /* Internal helper for `display' executed on the main thread.  */
+
+  @SuppressWarnings ("deprecation") /* Notification.Builder (Context).  */
+  private void
+  display1 (Context context)
+  {
+    NotificationManager manager;
+    NotificationChannel channel;
+    Notification notification;
+    Object tem;
+    RemoteViews contentView;
+
+    tem = context.getSystemService (Context.NOTIFICATION_SERVICE);
+    manager = (NotificationManager) tem;
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+      {
+       /* Create the notification channel for this group.  If a group
+          already exists with the same name, its linked attributes
+          (such as its importance) will be overridden.  */
+        channel = new NotificationChannel (group, group, importance);
+       manager.createNotificationChannel (channel);
+
+       /* Create a notification object and display it.  */
+       notification = (new Notification.Builder (context, group)
+                       .setContentTitle (title)
+                       .setContentText (content)
+                       .setSmallIcon (R.drawable.emacs)
+                       .build ());
+      }
+    else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+      notification = (new Notification.Builder (context)
+                     .setContentTitle (title)
+                     .setContentText (content)
+                     .setSmallIcon (R.drawable.emacs)
+                     .build ());
+    else
+      {
+       notification = new Notification ();
+       notification.icon = R.drawable.emacs;
+
+       /* This remote widget tree is defined in
+          java/res/layout/sdk8_notifications_view.xml.  */
+       notification.contentView
+         = contentView
+         = new RemoteViews ("org.gnu.emacs",
+                            R.layout.sdk8_notifications_view);
+       contentView.setTextViewText (R.id.sdk8_notifications_title,
+                                    title);
+       contentView.setTextViewText (R.id.sdk8_notifications_content,
+                                    content);
+      }
+
+    manager.notify (tag, 2, notification);
+  }
+
+  /* Display this desktop notification.
+
+     Create a notification channel named GROUP or update its
+     importance if such a channel is already defined.  */
+
+  public void
+  display ()
+  {
+    EmacsService.SERVICE.runOnUiThread (new Runnable () {
+       @Override
+       public void
+       run ()
+       {
+         display1 (EmacsService.SERVICE);
+       }
+      });
+  }
+};
diff --git a/java/res/layout/sdk8_notifications_view.xml 
b/java/res/layout/sdk8_notifications_view.xml
new file mode 100644
index 00000000000..2daa5beea86
--- /dev/null
+++ b/java/res/layout/sdk8_notifications_view.xml
@@ -0,0 +1,31 @@
+<!-- Notification content widget tree for GNU Emacs on Android 2.3.
+
+Copyright (C) 2023 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+GNU Emacs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+             android:orientation="vertical"
+             android:layout_width="match_parent"
+             android:layout_height="wrap_content"
+             android:padding="8dp">
+  <TextView android:id="@+id/sdk8_notifications_title"
+           android:layout_width="wrap_content"
+           android:layout_height="wrap_content"/>
+  <TextView android:id="@+id/sdk8_notifications_content"
+           android:layout_width="wrap_content"
+           android:layout_height="wrap_content"/>
+</LinearLayout>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 8a11cb007ee..0bf1ef0ac9b 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -17,7 +17,7 @@ GNU General Public License for more details.
 You should have received a copy of the GNU General Public License
 along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. -->
 
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+<resources>
   <string name="start_quick_title">
     Restart Emacs with -Q
   </string>
diff --git a/lisp/org/org-clock.el b/lisp/org/org-clock.el
index 32ef0eb4291..6ab313e1218 100644
--- a/lisp/org/org-clock.el
+++ b/lisp/org/org-clock.el
@@ -51,7 +51,8 @@
 (declare-function org-dynamic-block-define "org" (type func))
 (declare-function w32-notification-notify "w32fns.c" (&rest params))
 (declare-function w32-notification-close "w32fns.c" (&rest params))
-(declare-function haiku-notifications-notify "haikufns.c")
+(declare-function haiku-notifications-notify "haikuselect.c")
+(declare-function android-notifications-notify "androidselect.c")
 
 (defvar org-frame-title-format-backup nil)
 (defvar org-state)
@@ -861,6 +862,13 @@ use libnotify if available, or fall back on a message."
          (haiku-notifications-notify :title "Org mode message"
                                      :body notification
                                      :urgency 'low))
+        ((fboundp 'android-notifications-notify)
+         ;; N.B. timeouts are not available under Haiku or Android.
+         (android-notifications-notify :title "Org mode message"
+                                       :body notification
+                                       ;; Low urgency notifications
+                                       ;; are by default hidden.
+                                       :urgency 'normal))
        ((fboundp 'w32-notification-notify)
         (let ((id (w32-notification-notify
                    :title "Org mode message"
diff --git a/src/androidselect.c b/src/androidselect.c
index 9910e7921de..5551598032d 100644
--- a/src/androidselect.c
+++ b/src/androidselect.c
@@ -22,6 +22,9 @@ along with GNU Emacs.  If not, see 
<https://www.gnu.org/licenses/>.  */
 #include <minmax.h>
 #include <unistd.h>
 
+#include <boot-time.h>
+#include <sys/types.h>
+
 #include "lisp.h"
 #include "blockinput.h"
 #include "coding.h"
@@ -466,6 +469,232 @@ does not have any corresponding data.  In that case, use
 
 
 
+/* Desktop notifications.  `android-desktop-notify' implements a
+   facsimile of `notifications-notify'.  */
+
+/* Structure describing the EmacsDesktopNotification class.  */
+
+struct android_emacs_desktop_notification
+{
+  jclass class;
+  jmethodID init;
+  jmethodID display;
+};
+
+/* Methods provided by the EmacsDesktopNotification class.  */
+static struct android_emacs_desktop_notification notification_class;
+
+/* Initialize virtual function IDs and class pointers tied to the
+   EmacsDesktopNotification class.  */
+
+static void
+android_init_emacs_desktop_notification (void)
+{
+  jclass old;
+
+  notification_class.class
+    = (*android_java_env)->FindClass (android_java_env,
+                                     "org/gnu/emacs/EmacsDesktopNotification");
+  eassert (notification_class.class);
+
+  old = notification_class.class;
+  notification_class.class
+    = (jclass) (*android_java_env)->NewGlobalRef (android_java_env,
+                                                 old);
+  ANDROID_DELETE_LOCAL_REF (old);
+
+  if (!notification_class.class)
+    emacs_abort ();
+
+#define FIND_METHOD(c_name, name, signature)                           \
+  notification_class.c_name                                            \
+    = (*android_java_env)->GetMethodID (android_java_env,              \
+                                       notification_class.class,       \
+                                       name, signature);               \
+  assert (notification_class.c_name);
+
+  FIND_METHOD (init, "<init>", "(Ljava/lang/String;"
+              "Ljava/lang/String;Ljava/lang/String;"
+              "Ljava/lang/String;I)V");
+  FIND_METHOD (display, "display", "()V");
+#undef FIND_METHOD
+}
+
+/* Display a desktop notification with the provided TITLE, BODY,
+   REPLACES_ID, GROUP and URGENCY.  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 urgency)
+{
+  static intmax_t counter;
+  intmax_t id;
+  jstring title1, body1, group1, identifier1;
+  jint type;
+  jobject notification;
+  char identifier[INT_STRLEN_BOUND (int)
+                 + INT_STRLEN_BOUND (long int)
+                 + INT_STRLEN_BOUND (intmax_t)
+                 + sizeof "..."];
+  struct timespec boot_time;
+
+  if (EQ (urgency, Qlow))
+    type = 2; /* IMPORTANCE_LOW */
+  else if (EQ (urgency, Qnormal))
+    type = 3; /* IMPORTANCE_DEFAULT */
+  else if (EQ (urgency, Qcritical))
+    type = 4; /* IMPORTANCE_HIGH */
+  else
+    signal_error ("Invalid notification importance given", urgency);
+
+  if (NILP (replaces_id))
+    {
+      /* Generate a new identifier.  */
+      INT_ADD_WRAPV (counter, 1, &counter);
+      id = counter;
+    }
+  else
+    {
+      CHECK_INTEGER (replaces_id);
+      if (!integer_to_intmax (replaces_id, &id))
+       id = -1; /* Overflow.  */
+    }
+
+  /* Generate a unique identifier for this notification.  Because
+     Android persists notifications past system shutdown, also include
+     the boot time within IDENTIFIER.  Scale it down to avoid being
+     perturbed by minor instabilities in the returned boot time,
+     however.  */
+
+  boot_time.tv_sec = 0;
+  get_boot_time (&boot_time);
+  sprintf (identifier, "%d.%ld.%jd", (int) getpid (),
+          (long int) (boot_time.tv_sec / 2), id);
+
+  /* Encode all strings into their Java counterparts.  */
+  title1 = android_build_string (title);
+  body1  = android_build_string (body);
+  group1 = android_build_string (group);
+  identifier1 = android_build_jstring (identifier);
+
+  /* Create the notification.  */
+  notification
+    = (*android_java_env)->NewObject (android_java_env,
+                                     notification_class.class,
+                                     notification_class.init,
+                                     title1, body1, group1,
+                                     identifier1, type);
+  android_exception_check_4 (title1, body1, group1, identifier1);
+
+  /* Delete unused local references.  */
+  ANDROID_DELETE_LOCAL_REF (title1);
+  ANDROID_DELETE_LOCAL_REF (body1);
+  ANDROID_DELETE_LOCAL_REF (group1);
+  ANDROID_DELETE_LOCAL_REF (identifier1);
+
+  /* Display the notification.  */
+  (*android_java_env)->CallNonvirtualVoidMethod (android_java_env,
+                                                notification,
+                                                notification_class.class,
+                                                notification_class.display);
+  android_exception_check_1 (notification);
+  ANDROID_DELETE_LOCAL_REF (notification);
+
+  /* Return the ID.  */
+  return id;
+}
+
+DEFUN ("android-notifications-notify", Fandroid_notifications_notify,
+       Sandroid_notifications_notify, 0, MANY, 0, doc:
+       /* Display a desktop notification.
+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.
+
+The notification group and urgency are ignored on Android 7.1 and
+earlier versions of Android.  Outside such older systems, it
+identifies a category that will be displayed in the system Settings
+menu.  The urgency provided always extends to affect all notifications
+displayed within that category.  If the group is not provided, it
+defaults to the string "Desktop Notifications".
+
+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
+corresponding to Emacs.
+
+A title and body must be supplied.  Value is an integer (fixnum or
+bignum) uniquely designating the notification displayed, which may
+subsequently be specified as the `:replaces-id' of another call to
+this function.
+
+usage: (android-notifications-notify &rest ARGS) */)
+  (ptrdiff_t nargs, Lisp_Object *args)
+{
+  Lisp_Object title, body, replaces_id, group, urgency;
+  Lisp_Object key, value;
+  ptrdiff_t i;
+
+  if (!android_init_gui)
+    error ("No Android display connection!");
+
+  /* Clear each variable above.  */
+  title = body = replaces_id = group = urgency = Qnil;
+
+  /* If NARGS is odd, error.  */
+
+  if (nargs & 1)
+    error ("Odd number of arguments in call to 
`android-notifications-notify'");
+
+  /* Next, iterate through ARGS, searching for arguments.  */
+
+  for (i = 0; i < nargs; i += 2)
+    {
+      key = args[i];
+      value = args[i + 1];
+
+      if (EQ (key, QCtitle))
+       title = value;
+      else if (EQ (key, QCbody))
+       body = value;
+      else if (EQ (key, QCreplaces_id))
+       replaces_id = value;
+      else if (EQ (key, QCgroup))
+       group = value;
+      else if (EQ (key, QCurgency))
+       urgency = value;
+    }
+
+  /* Demand at least TITLE and BODY be present.  */
+
+  if (NILP (title) || NILP (body))
+    error ("Title or body not provided");
+
+  /* Now check the type and possibly expand each non-nil argument.  */
+
+  CHECK_STRING (title);
+  CHECK_STRING (body);
+
+  if (NILP (urgency))
+    urgency = Qlow;
+
+  if (NILP (group))
+    group   = build_string ("Desktop Notifications");
+
+  return make_int (android_notifications_notify_1 (title, body, replaces_id,
+                                                  group, urgency));
+}
+
+
+
 void
 init_androidselect (void)
 {
@@ -476,6 +705,7 @@ init_androidselect (void)
     return;
 
   android_init_emacs_clipboard ();
+  android_init_emacs_desktop_notification ();
 
   make_clipboard = clipboard_class.make_clipboard;
   tem
@@ -496,6 +726,16 @@ init_androidselect (void)
 void
 syms_of_androidselect (void)
 {
+  DEFSYM (QCtitle, ":title");
+  DEFSYM (QCbody, ":body");
+  DEFSYM (QCreplaces_id, ":replaces-id");
+  DEFSYM (QCgroup, ":group");
+  DEFSYM (QCurgency, ":urgency");
+
+  DEFSYM (Qlow, "low");
+  DEFSYM (Qnormal, "normal");
+  DEFSYM (Qcritical, "critical");
+
   defsubr (&Sandroid_clipboard_owner_p);
   defsubr (&Sandroid_set_clipboard);
   defsubr (&Sandroid_get_clipboard);
@@ -503,4 +743,6 @@ syms_of_androidselect (void)
   defsubr (&Sandroid_browse_url);
   defsubr (&Sandroid_get_clipboard_targets);
   defsubr (&Sandroid_get_clipboard_data);
+
+  defsubr (&Sandroid_notifications_notify);
 }



reply via email to

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