summaryrefslogtreecommitdiff
path: root/src/androidselect.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/androidselect.c')
-rw-r--r--src/androidselect.c1056
1 files changed, 1056 insertions, 0 deletions
diff --git a/src/androidselect.c b/src/androidselect.c
new file mode 100644
index 00000000000..2f6114d0fcb
--- /dev/null
+++ b/src/androidselect.c
@@ -0,0 +1,1056 @@
+/* Communication module for Android terminals.
+
+Copyright (C) 2023-2024 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/>. */
+
+#include <config.h>
+#include <assert.h>
+#include <minmax.h>
+#include <unistd.h>
+
+#include <boot-time.h>
+#include <sys/types.h>
+
+#include "lisp.h"
+#include "blockinput.h"
+#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
+ selection.
+
+ While newer versions of Android are supposed to have the necessary
+ interfaces for transferring other kinds of selection data, doing so
+ is too complicated, and involves registering ``content providers''
+ and all kinds of other stuff; for this reason, Emacs does not
+ support setting the clipboard contents to anything other than plain
+ text. */
+
+
+
+/* Structure describing the EmacsClipboard class. */
+
+struct android_emacs_clipboard
+{
+ jclass class;
+ jmethodID set_clipboard;
+ jmethodID owns_clipboard;
+ jmethodID clipboard_exists;
+ jmethodID get_clipboard;
+ jmethodID make_clipboard;
+ jmethodID get_clipboard_targets;
+ jmethodID get_clipboard_data;
+};
+
+/* Methods associated with the EmacsClipboard class. */
+static struct android_emacs_clipboard clipboard_class;
+
+/* Reference to the EmacsClipboard object. */
+static jobject clipboard;
+
+
+
+static void
+android_init_emacs_clipboard (void)
+{
+ jclass old;
+
+ clipboard_class.class
+ = (*android_java_env)->FindClass (android_java_env,
+ "org/gnu/emacs/EmacsClipboard");
+ eassert (clipboard_class.class);
+
+ old = clipboard_class.class;
+ clipboard_class.class
+ = (jclass) (*android_java_env)->NewGlobalRef (android_java_env,
+ old);
+ ANDROID_DELETE_LOCAL_REF (old);
+
+ if (!clipboard_class.class)
+ emacs_abort ();
+
+#define FIND_METHOD(c_name, name, signature) \
+ clipboard_class.c_name \
+ = (*android_java_env)->GetMethodID (android_java_env, \
+ clipboard_class.class, \
+ name, signature); \
+ eassert (clipboard_class.c_name);
+
+ FIND_METHOD (set_clipboard, "setClipboard", "([B)V");
+ FIND_METHOD (owns_clipboard, "ownsClipboard", "()I");
+ FIND_METHOD (clipboard_exists, "clipboardExists", "()Z");
+ FIND_METHOD (get_clipboard, "getClipboard", "()[B");
+ FIND_METHOD (get_clipboard_targets, "getClipboardTargets",
+ "()[[B");
+ FIND_METHOD (get_clipboard_data, "getClipboardData",
+ "([B)[J");
+
+ clipboard_class.make_clipboard
+ = (*android_java_env)->GetStaticMethodID (android_java_env,
+ clipboard_class.class,
+ "makeClipboard",
+ "()Lorg/gnu/emacs/"
+ "EmacsClipboard;");
+ eassert (clipboard_class.make_clipboard);
+
+#undef FIND_METHOD
+}
+
+
+
+
+DEFUN ("android-clipboard-owner-p", Fandroid_clipboard_owner_p,
+ Sandroid_clipboard_owner_p, 0, 0, 0,
+ doc: /* Return whether or not Emacs owns the clipboard.
+Alternatively, return the symbol `lambda' if that could not be
+determined. */)
+ (void)
+{
+ jint rc;
+
+ if (!android_init_gui)
+ error ("Accessing clipboard without display connection");
+
+ block_input ();
+ rc = (*android_java_env)->CallIntMethod (android_java_env,
+ clipboard,
+ clipboard_class.owns_clipboard);
+ android_exception_check ();
+ unblock_input ();
+
+ /* If rc is 0 or 1, then Emacs knows whether or not it owns the
+ clipboard. If rc is -1, then Emacs does not. */
+
+ if (rc < 0)
+ return Qlambda;
+
+ return rc ? Qt : Qnil;
+}
+
+DEFUN ("android-set-clipboard", Fandroid_set_clipboard,
+ Sandroid_set_clipboard, 1, 1, 0,
+ doc: /* Set the clipboard text to STRING. */)
+ (Lisp_Object string)
+{
+ jarray bytes;
+
+ if (!android_init_gui)
+ error ("Accessing clipboard without display connection");
+
+ CHECK_STRING (string);
+ string = ENCODE_UTF_8 (string);
+
+ bytes = (*android_java_env)->NewByteArray (android_java_env,
+ SBYTES (string));
+ android_exception_check ();
+
+ (*android_java_env)->SetByteArrayRegion (android_java_env, bytes,
+ 0, SBYTES (string),
+ (jbyte *) SDATA (string));
+ (*android_java_env)->CallVoidMethod (android_java_env,
+ clipboard,
+ clipboard_class.set_clipboard,
+ bytes);
+ android_exception_check_1 (bytes);
+
+ ANDROID_DELETE_LOCAL_REF (bytes);
+ return Qnil;
+}
+
+DEFUN ("android-get-clipboard", Fandroid_get_clipboard,
+ Sandroid_get_clipboard, 0, 0, 0,
+ doc: /* Return the current contents of the clipboard.
+Value is a multibyte string containing decoded clipboard
+text.
+Alternatively, return nil if the clipboard is empty. */)
+ (void)
+{
+ Lisp_Object string;
+ jarray bytes;
+ jmethodID method;
+ size_t length;
+ jbyte *data;
+
+ if (!android_init_gui)
+ error ("No Android display connection!");
+
+ method = clipboard_class.get_clipboard;
+ bytes
+ = (*android_java_env)->CallObjectMethod (android_java_env,
+ clipboard,
+ method);
+ android_exception_check ();
+
+ if (!bytes)
+ return Qnil;
+
+ length = (*android_java_env)->GetArrayLength (android_java_env,
+ bytes);
+ data = (*android_java_env)->GetByteArrayElements (android_java_env,
+ bytes, NULL);
+ android_exception_check_nonnull (data, bytes);
+
+ string = make_unibyte_string ((char *) data, length);
+
+ (*android_java_env)->ReleaseByteArrayElements (android_java_env,
+ bytes, data,
+ JNI_ABORT);
+ ANDROID_DELETE_LOCAL_REF (bytes);
+
+ /* Now decode the resulting string. */
+ return code_convert_string_norecord (string, Qutf_8, false);
+}
+
+DEFUN ("android-clipboard-exists-p", Fandroid_clipboard_exists_p,
+ Sandroid_clipboard_exists_p, 0, 0, 0,
+ doc: /* Return whether or not clipboard contents exist. */)
+ (void)
+{
+ jboolean rc;
+ jmethodID method;
+
+ if (!android_init_gui)
+ error ("No Android display connection");
+
+ method = clipboard_class.clipboard_exists;
+ rc = (*android_java_env)->CallBooleanMethod (android_java_env,
+ clipboard,
+ method);
+ android_exception_check ();
+
+ return rc ? Qt : Qnil;
+}
+
+DEFUN ("android-browse-url-internal", Fandroid_browse_url_internal,
+ Sandroid_browse_url_internal, 1, 2, 0,
+ doc: /* Open URL in an external application.
+
+URL should be a URL-encoded URL with a scheme specified unless SEND is
+non-nil. Signal an error upon failure.
+
+If SEND is nil, start a program that is able to display the URL, such
+as a web browser. Otherwise, try to share URL using programs such as
+email clients.
+
+If URL is a file URI, convert it into a `content' address accessible to
+other programs. Files inside the /content or /assets directories cannot
+be opened through such addresses, which this function does not provide
+for. Use `android-browse-url' instead. */)
+ (Lisp_Object url, Lisp_Object send)
+{
+ Lisp_Object value;
+
+ if (!android_init_gui)
+ error ("No Android display connection!");
+
+ CHECK_STRING (url);
+ value = android_browse_url (url, send);
+
+ /* Signal an error upon failure. */
+ if (!NILP (value))
+ signal_error ("Error browsing URL", value);
+
+ return Qnil;
+}
+
+
+
+/* MIME clipboard support. This provides support for reading MIME
+ data (but not text) from the clipboard. */
+
+DEFUN ("android-get-clipboard-targets", Fandroid_get_clipboard_targets,
+ Sandroid_get_clipboard_targets, 0, 0, 0,
+ doc: /* Return a list of data types in the clipboard.
+Value is a list of MIME types as strings, each defining a single extra
+data type available from the clipboard. */)
+ (void)
+{
+ jarray bytes_array;
+ jbyteArray bytes;
+ jmethodID method;
+ size_t length, length1, i;
+ jbyte *data;
+ Lisp_Object targets, tem;
+
+ if (!android_init_gui)
+ error ("No Android display connection!");
+
+ targets = Qnil;
+ block_input ();
+ method = clipboard_class.get_clipboard_targets;
+ bytes_array = (*android_java_env)->CallObjectMethod (android_java_env,
+ clipboard, method);
+ android_exception_check ();
+
+ if (!bytes_array)
+ goto fail;
+
+ length = (*android_java_env)->GetArrayLength (android_java_env,
+ bytes_array);
+ for (i = 0; i < length; ++i)
+ {
+ /* Retrieve the MIME type. */
+ bytes
+ = (*android_java_env)->GetObjectArrayElement (android_java_env,
+ bytes_array, i);
+ android_exception_check_nonnull (bytes, bytes_array);
+
+ /* Cons it onto the list of targets. */
+ length1 = (*android_java_env)->GetArrayLength (android_java_env,
+ bytes);
+ data = (*android_java_env)->GetByteArrayElements (android_java_env,
+ bytes, NULL);
+ android_exception_check_nonnull_1 (data, bytes, bytes_array);
+
+ /* Decode the string. */
+ tem = make_unibyte_string ((char *) data, length1);
+ tem = code_convert_string_norecord (tem, Qutf_8, false);
+ targets = Fcons (tem, targets);
+
+ /* Delete the retrieved data. */
+ (*android_java_env)->ReleaseByteArrayElements (android_java_env,
+ bytes, data,
+ JNI_ABORT);
+ ANDROID_DELETE_LOCAL_REF (bytes);
+ }
+ unblock_input ();
+
+ ANDROID_DELETE_LOCAL_REF (bytes_array);
+ return Fnreverse (targets);
+
+ fail:
+ unblock_input ();
+ return Qnil;
+}
+
+/* Free the memory inside PTR, a pointer to a char pointer. */
+
+static void
+android_xfree_inside (void *ptr)
+{
+ xfree (*(char **) ptr);
+}
+
+DEFUN ("android-get-clipboard-data", Fandroid_get_clipboard_data,
+ Sandroid_get_clipboard_data, 1, 1, 0,
+ doc: /* Return the clipboard data of the given MIME TYPE.
+Value is a unibyte string containing the entire contents of the
+clipboard, after its owner has converted the data to the given
+MIME type. Value is nil if the conversion fails, or if the data
+is not present.
+
+Value is also nil if the clipboard data consists of a single URL which
+does not have any corresponding data. In that case, use
+`android-get-clipboard' instead. */)
+ (Lisp_Object type)
+{
+ jlongArray array;
+ jbyteArray bytes;
+ jmethodID method;
+ int fd;
+ ptrdiff_t rc;
+ jlong offset, length, *longs;
+ specpdl_ref ref;
+ char *buffer, *start;
+
+ if (!android_init_gui)
+ error ("No Android display connection!");
+
+ /* Encode the string as UTF-8. */
+ CHECK_STRING (type);
+ type = ENCODE_UTF_8 (type);
+
+ /* Then give it to the selection code. */
+ block_input ();
+ bytes = (*android_java_env)->NewByteArray (android_java_env,
+ SBYTES (type));
+ (*android_java_env)->SetByteArrayRegion (android_java_env, bytes,
+ 0, SBYTES (type),
+ (jbyte *) SDATA (type));
+ android_exception_check ();
+
+ method = clipboard_class.get_clipboard_data;
+ array = (*android_java_env)->CallObjectMethod (android_java_env,
+ clipboard, method,
+ bytes);
+ android_exception_check_1 (bytes);
+ ANDROID_DELETE_LOCAL_REF (bytes);
+
+ if (!array)
+ goto fail;
+
+ longs = (*android_java_env)->GetLongArrayElements (android_java_env,
+ array, NULL);
+ android_exception_check_nonnull (longs, array);
+
+ /* longs[0] is the file descriptor.
+ longs[1] is an offset to apply to the file.
+ longs[2] is either -1, or the number of bytes to read from the
+ file. */
+ fd = longs[0];
+ offset = longs[1];
+ length = longs[2];
+
+ (*android_java_env)->ReleaseLongArrayElements (android_java_env,
+ array, longs,
+ JNI_ABORT);
+ ANDROID_DELETE_LOCAL_REF (array);
+ unblock_input ();
+
+ /* Now begin reading from longs[0]. */
+ ref = SPECPDL_INDEX ();
+ record_unwind_protect_int (close_file_unwind, fd);
+
+ if (length != -1)
+ {
+ buffer = xmalloc (MIN (length, PTRDIFF_MAX));
+ record_unwind_protect_ptr (xfree, buffer);
+
+ rc = emacs_read_quit (fd, buffer,
+ MIN (length, PTRDIFF_MAX));
+
+ /* Return nil upon an IO problem. */
+ if (rc < 0)
+ return unbind_to (ref, Qnil);
+
+ /* Return the data as a unibyte string. */
+ return unbind_to (ref, make_unibyte_string (buffer, rc));
+ }
+
+ /* Otherwise, read BUFSIZ bytes at a time. */
+ buffer = xmalloc (BUFSIZ);
+ length = 0;
+ start = buffer;
+
+ record_unwind_protect_ptr (android_xfree_inside, &buffer);
+
+ /* Seek to the start of the data. */
+
+ if (offset)
+ {
+ if (lseek (fd, offset, SEEK_SET) < 0)
+ return unbind_to (ref, Qnil);
+ }
+
+ while (true)
+ {
+ rc = emacs_read_quit (fd, start, BUFSIZ);
+
+ if (ckd_add (&length, length, rc)
+ || PTRDIFF_MAX - length < BUFSIZ)
+ memory_full (PTRDIFF_MAX);
+
+ if (rc < 0)
+ return unbind_to (ref, Qnil);
+
+ if (rc < BUFSIZ)
+ break;
+
+ buffer = xrealloc (buffer, length + BUFSIZ);
+ start = buffer + length;
+ }
+
+ return unbind_to (ref, make_unibyte_string (buffer, rc));
+
+ fail:
+ unblock_input ();
+ return Qnil;
+}
+
+
+
+/* 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;
+
+/* Hash table pairing notification identifiers with callbacks. */
+static Lisp_Object notification_table;
+
+/* 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); \
+ eassert (notification_class.c_name);
+
+ FIND_METHOD (init, "<init>", "(Ljava/lang/String;"
+ "Ljava/lang/String;Ljava/lang/String;"
+ "Ljava/lang/String;II[Ljava/lang/String;"
+ "[Ljava/lang/String;J)V");
+ FIND_METHOD (display, "display", "()V");
+#undef FIND_METHOD
+}
+
+/* Return the numeric resource ID designating the icon within the
+ ``android.R.drawable'' package by the supplied NAME.
+
+ If no icon is found, return that of
+ ``android.R.drawable.ic_dialog_alert''. */
+
+static jint
+android_locate_icon (const char *name)
+{
+ jclass drawable;
+ jfieldID field;
+ jint rc;
+
+ if (android_verify_jni_string (name))
+ /* If NAME isn't valid, return the default value. */
+ return 17301543; /* android.R.drawable.ic_dialog_alert. */
+
+ drawable = (*android_java_env)->FindClass (android_java_env,
+ "android/R$drawable");
+ android_exception_check ();
+
+ field = (*android_java_env)->GetStaticFieldID (android_java_env,
+ drawable, name, "I");
+ (*android_java_env)->ExceptionClear (android_java_env);
+
+ if (!field)
+ rc = 17301543; /* android.R.drawable.ic_dialog_alert. */
+ else
+ rc = (*android_java_env)->GetStaticIntField (android_java_env,
+ drawable, field);
+
+ ANDROID_DELETE_LOCAL_REF (drawable);
+ return rc;
+}
+
+/* Display a desktop notification with the provided TITLE, BODY,
+ REPLACES_ID, GROUP, ICON, URGENCY, ACTIONS, TIMEOUT, RESIDENT,
+ ACTION_CB and CLOSE_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 actions,
+ Lisp_Object timeout, Lisp_Object resident,
+ Lisp_Object action_cb, Lisp_Object close_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;
+ jlong timeout_val;
+
+ 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);
+
+ /* Decode the timeout. */
+
+ timeout_val = 0;
+
+ if (!NILP (timeout))
+ {
+ CHECK_INTEGER (timeout);
+
+ if (!integer_to_intmax (timeout, &id)
+ || id > TYPE_MAXIMUM (jlong)
+ || id < TYPE_MINIMUM (jlong))
+ signal_error ("Invalid timeout", timeout);
+
+ if (id > 0)
+ timeout_val = id;
+ }
+
+ 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. */
+ ckd_add (&counter, counter, 1);
+ id = counter;
+ }
+ else
+ {
+ CHECK_INTEGER (replaces_id);
+ if (!integer_to_intmax (replaces_id, &id))
+ id = -1; /* Overflow. */
+ }
+
+ /* Locate the integer ID linked to ICON. */
+ icon1 = android_locate_icon (SSDATA (icon));
+
+ /* 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, NULL);
+ body1 = android_build_string (body, title1, NULL);
+ group1 = android_build_string (group, body1, title1, NULL);
+ identifier1
+ = (*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,
+ action_keys, action_titles,
+ timeout_val);
+ android_exception_check_6 (title1, body1, group1, identifier1,
+ action_titles, action_keys);
+
+ /* Delete unused local references. */
+ ANDROID_DELETE_LOCAL_REF (title1);
+ ANDROID_DELETE_LOCAL_REF (body1);
+ ANDROID_DELETE_LOCAL_REF (group1);
+ ANDROID_DELETE_LOCAL_REF (identifier1);
+ ANDROID_DELETE_LOCAL_REF (action_keys);
+ ANDROID_DELETE_LOCAL_REF (action_titles);
+
+ /* 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);
+
+ /* If callbacks are provided, save them into notification_table. */
+
+ if (!NILP (action_cb) || !NILP (close_cb) || !NILP (resident))
+ Fputhash (build_string (identifier), list3 (action_cb, close_cb,
+ resident),
+ notification_table);
+
+ /* 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.
+ :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 defined here will be
+ displayed, not counting any with "default" as its
+ key.
+ :timeout Number of miliseconds from the display of the
+ notification at which it will be automatically
+ dismissed, or a value of zero or smaller if it
+ is to remain until user action is taken to dismiss
+ it.
+ :resident When set the notification will not be automatically
+ dismissed when it or an action is selected.
+ :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-close Function to call if the notification is dismissed,
+ with the notification id and the symbol `undefined'
+ for arguments.
+
+The notification group and timeout are ignored on Android 7.1 and
+earlier versions of Android. On more recent versions, the group
+identifies a category that will be displayed in the system Settings
+menu, and the urgency provided always extends to affect all
+notifications displayed within that category, though it may be ignored
+if higher than any previously-specified urgency or if the user have
+already configured a different urgency for this category from Settings.
+If the group is not provided, it defaults to the string "Desktop
+Notifications" with the urgency suffixed.
+
+Each caller should strive to provide one unchanging combination of
+notification group and urgency for each kind of notification it sends,
+inasmuch as the system may, subject to user configuration, disregard
+the urgency specified within a notification, should it not be the
+first notification sent to its notification group.
+
+The provided icon should be the name of a "drawable resource" present
+within the "android.R.drawable" class designating an icon with a
+transparent background. Should no icon be 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
+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, timeout, resident;
+ Lisp_Object icon;
+ Lisp_Object key, value, actions, action_cb, close_cb;
+ ptrdiff_t i;
+ AUTO_STRING (default_icon, "ic_dialog_alert");
+
+ if (!android_init_gui)
+ error ("No Android display connection!");
+
+ /* Clear each variable above. */
+ title = body = replaces_id = group = icon = urgency = actions = Qnil;
+ timeout = resident = action_cb = close_cb = 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;
+ else if (EQ (key, QCicon))
+ icon = value;
+ else if (EQ (key, QCactions))
+ actions = value;
+ else if (EQ (key, QCtimeout))
+ timeout = value;
+ else if (EQ (key, QCresident))
+ resident = value;
+ else if (EQ (key, QCon_action))
+ action_cb = value;
+ else if (EQ (key, QCon_close))
+ close_cb = 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))
+ {
+ AUTO_STRING (format, "Desktop Notifications (%s importance)");
+ group = CALLN (Fformat, format, urgency);
+ }
+
+ if (NILP (icon))
+ icon = default_icon;
+ else
+ CHECK_STRING (icon);
+
+ return make_int (android_notifications_notify_1 (title, body, replaces_id,
+ group, icon, urgency,
+ actions, timeout, resident,
+ action_cb, close_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 (XCAR (XCDR (item)))
+ && sscanf (event->tag, "%*d.%*ld.%jd", &id) > 0)
+ {
+ ie->kind = NOTIFICATION_EVENT;
+ ie->arg = list3 (XCAR (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;
+ jstring tag_object;
+ jmethodID method;
+
+ 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);
+ }
+
+ /* Test whether ITEM is resident. Non-resident notifications must be
+ removed when activated. */
+
+ if (!CONSP (item) || NILP (XCAR (XCDR (XCDR (item)))))
+ {
+ method = service_class.cancel_notification;
+ tag_object
+ = (*android_java_env)->NewStringUTF (android_java_env,
+ event->tag);
+ android_exception_check ();
+
+ (*android_java_env)->CallNonvirtualVoidMethod (android_java_env,
+ emacs_service,
+ service_class.class,
+ method, tag_object);
+ android_exception_check_1 (tag_object);
+ ANDROID_DELETE_LOCAL_REF (tag_object);
+
+ /* Remove the notification from the callback table. */
+ if (!NILP (item))
+ Fremhash (tag, notification_table);
+ }
+}
+
+
+
+void
+init_androidselect (void)
+{
+ jobject tem;
+ jmethodID make_clipboard;
+
+ if (!android_init_gui)
+ return;
+
+ android_init_emacs_clipboard ();
+ android_init_emacs_desktop_notification ();
+
+ make_clipboard = clipboard_class.make_clipboard;
+ tem
+ = (*android_java_env)->CallStaticObjectMethod (android_java_env,
+ clipboard_class.class,
+ make_clipboard);
+ if (!tem)
+ emacs_abort ();
+
+ clipboard = (*android_java_env)->NewGlobalRef (android_java_env, tem);
+
+ if (!clipboard)
+ emacs_abort ();
+
+ ANDROID_DELETE_LOCAL_REF (tem);
+}
+
+void
+syms_of_androidselect (void)
+{
+ DEFSYM (QCtitle, ":title");
+ DEFSYM (QCbody, ":body");
+ DEFSYM (QCreplaces_id, ":replaces-id");
+ DEFSYM (QCgroup, ":group");
+ DEFSYM (QCurgency, ":urgency");
+ DEFSYM (QCicon, ":icon");
+ DEFSYM (QCactions, ":actions");
+ DEFSYM (QCtimeout, ":timeout");
+ DEFSYM (QCresident, ":resident");
+ DEFSYM (QCon_action, ":on-action");
+ DEFSYM (QCon_close, ":on-close");
+
+ DEFSYM (Qlow, "low");
+ DEFSYM (Qnormal, "normal");
+ DEFSYM (Qcritical, "critical");
+
+ defsubr (&Sandroid_clipboard_owner_p);
+ defsubr (&Sandroid_set_clipboard);
+ defsubr (&Sandroid_get_clipboard);
+ defsubr (&Sandroid_clipboard_exists_p);
+ defsubr (&Sandroid_browse_url_internal);
+ defsubr (&Sandroid_get_clipboard_targets);
+ defsubr (&Sandroid_get_clipboard_data);
+
+ defsubr (&Sandroid_notifications_notify);
+
+ notification_table = CALLN (Fmake_hash_table, QCtest, Qequal);
+ staticpro (&notification_table);
+}