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.
This commit is contained in:
Po Lu 2023-08-20 20:23:56 +08:00
parent 321f6bd572
commit 4f714dc081
8 changed files with 484 additions and 6 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@
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>

View file

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

View file

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