From 4f714dc08137d36d0d1e886814008f2abe5712c0 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 20 Aug 2023 20:23:56 +0800 Subject: [PATCH] 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 + .../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(-) create mode 100644 java/org/gnu/emacs/EmacsDesktopNotification.java create mode 100644 java/res/layout/sdk8_notifications_view.xml 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 . --> + + + + 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 . */ + +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 @@ + + + + + + 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 @@ You should have received a copy of the GNU General Public License along with GNU Emacs. If not, see . --> - + Restart Emacs with -Q 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 . */ #include #include +#include +#include + #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, "", "(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); }