@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
@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
@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
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
<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"/>
--- /dev/null
+/* 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;
+
+\f
+
+/* 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. */
+
+\f
+
+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;
+ }
+
+\f
+
+ /* 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);
+ }
+ });
+ }
+};
--- /dev/null
+<!-- 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>
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>
(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)
(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"
#include <minmax.h>
#include <unistd.h>
+#include <boot-time.h>
+#include <sys/types.h>
+
#include "lisp.h"
#include "blockinput.h"
#include "coding.h"
\f
+/* 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));
+}
+
+\f
+
void
init_androidselect (void)
{
return;
android_init_emacs_clipboard ();
+ android_init_emacs_desktop_notification ();
make_clipboard = clipboard_class.make_clipboard;
tem
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);
defsubr (&Sandroid_browse_url);
defsubr (&Sandroid_get_clipboard_targets);
defsubr (&Sandroid_get_clipboard_data);
+
+ defsubr (&Sandroid_notifications_notify);
}