commit 3ed5777fa732123e8bf630293020f9d1766694b7 (HEAD, refs/remotes/origin/master) Author: Po Lu Date: Tue Mar 12 12:24:59 2024 +0800 Fix CheckJNI crash on Android 2.2 * src/androidfns.c (syms_of_androidfns_for_pdumper): Don't free local reference to script name if nonexistent. diff --git a/src/androidfns.c b/src/androidfns.c index 0675a0a3c98..83cf81c1f07 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -3398,9 +3398,9 @@ syms_of_androidfns_for_pdumper (void) string, data); } } - } - ANDROID_DELETE_LOCAL_REF (string); + ANDROID_DELETE_LOCAL_REF (string); + } /* And variant. */ commit fd293c7c67d03204356be3cd6a0cb565dec9ecbf Author: Po Lu Date: Tue Mar 12 10:46:00 2024 +0800 ; Check in missing change to AndroidManifest.xml.in * java/AndroidManifest.xml.in: Register CancellationReceiver. diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index 27af9c912fe..4d23c752747 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -316,6 +316,13 @@ along with GNU Emacs. If not, see . --> + + + + + + Date: Mon Mar 11 21:51:29 2024 -0400 (gnus-convert-old-newsrc): Remove ancient converters * lisp/gnus/gnus-start.el (gnus-convert-old-newsrc): Remove converters from 2004 and before. * lisp/gnus/legacy-gnus-agent.el: Delete file. diff --git a/lisp/gnus/gnus-start.el b/lisp/gnus/gnus-start.el index f337278994c..05ad4303b5c 100644 --- a/lisp/gnus/gnus-start.el +++ b/lisp/gnus/gnus-start.el @@ -2285,14 +2285,16 @@ If FORCE is non-nil, the .newsrc file is read." ;; doesn't change with each release) and the ;; function that must be applied to convert the ;; previous version into the current version. - '(("September Gnus v0.1" nil - gnus-convert-old-ticks) - ("Oort Gnus v0.08" "legacy-gnus-agent" - gnus-agent-convert-to-compressed-agentview) - ("Gnus v5.10.7" "legacy-gnus-agent" - gnus-agent-unlist-expire-days) - ("Gnus v5.10.7" "legacy-gnus-agent" - gnus-agent-unhook-expire-days))) + '(;;These all date back to 2004 or earlier! + ;; ("September Gnus v0.1" nil + ;; gnus-convert-old-ticks) + ;; ("Oort Gnus v0.08" "legacy-gnus-agent" + ;; gnus-agent-convert-to-compressed-agentview) + ;; ("Gnus v5.10.7" "legacy-gnus-agent" + ;; gnus-agent-unlist-expire-days) + ;; ("Gnus v5.10.7" "legacy-gnus-agent" + ;; gnus-agent-unhook-expire-days) + )) #'car-less-than-car))) ;; Skip converters older than the file version (while (and converters (>= fcv (caar converters))) diff --git a/lisp/gnus/legacy-gnus-agent.el b/lisp/gnus/legacy-gnus-agent.el deleted file mode 100644 index d4f08c72de8..00000000000 --- a/lisp/gnus/legacy-gnus-agent.el +++ /dev/null @@ -1,260 +0,0 @@ -;;; legacy-gnus-agent.el --- Legacy unplugged support for Gnus -*- lexical-binding: t; -*- - -;; Copyright (C) 2004-2024 Free Software Foundation, Inc. - -;; Author: Kevin Greiner -;; Keywords: news - -;; 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 . - -;;; Commentary: - -;; Conversion functions for the Agent. - -;;; Code: -(require 'gnus-start) -(require 'gnus-util) -(require 'gnus-range) -(require 'gnus-agent) - -;; Oort Gnus v0.08 - This release updated agent to no longer use -;; history file and to support a compressed alist. - -(defvar gnus-agent-compressed-agentview-search-only nil) - -(defun gnus-agent-convert-to-compressed-agentview (converting-to) - "Iterates over all agentview files to ensure that they have been -converted to the compressed format." - - (let ((search-in (list gnus-agent-directory)) - here - members - member - converted-something) - (while (setq here (pop search-in)) - (setq members (directory-files here t)) - (while (setq member (pop members)) - (cond ((string-match "/\\.\\.?$" member) - nil) - ((file-directory-p member) - (push member search-in)) - ((equal (file-name-nondirectory member) ".agentview") - (setq converted-something - (or (gnus-agent-convert-agentview member) - converted-something)))))) - - (if converted-something - (gnus-message 4 "Successfully converted Gnus %s offline (agent) files to %s" gnus-newsrc-file-version converting-to)))) - -(defun gnus-agent-convert-to-compressed-agentview-prompt () - (catch 'found-file-to-convert - (let ((gnus-agent-compressed-agentview-search-only t)) - (gnus-agent-convert-to-compressed-agentview nil)))) - -(gnus-convert-mark-converter-prompt 'gnus-agent-convert-to-compressed-agentview 'gnus-agent-convert-to-compressed-agentview-prompt) - -(defun gnus-agent-convert-agentview (file) - "Load FILE and do a `read' there." - (with-temp-buffer - (nnheader-insert-file-contents file) - (goto-char (point-min)) - (let ((inhibit-quit t) - (alist (read (current-buffer))) - (version (condition-case nil (read (current-buffer)) - (end-of-file 0))) - changed-version - history-file) - - (cond - ((= version 0) - (let (entry - (gnus-command-method nil)) - (mm-disable-multibyte) ;; everything is binary - (erase-buffer) - (insert "\n") - (let ((file (concat (file-name-directory file) "/history"))) - (when (file-exists-p file) - (nnheader-insert-file-contents file) - (setq history-file file))) - - (goto-char (point-min)) - (while (not (eobp)) - (if (and (looking-at - "[^\t\n]+\t\\([0-9]+\\)\t\\([^ \n]+\\) \\([0-9]+\\)") - (string= (gnus-agent-article-name ".agentview" (match-string 2)) - file) - (setq entry (assoc (string-to-number (match-string 3)) alist))) - (setcdr entry (string-to-number (match-string 1)))) - (forward-line 1)) - (setq changed-version t))) - ((= version 1) - (setq changed-version t))) - - (when changed-version - (when gnus-agent-compressed-agentview-search-only - (throw 'found-file-to-convert t)) - - (erase-buffer) - (let (article-id day-of-download comp-list compressed) - (while alist - (setq article-id (caar alist) - day-of-download (cdar alist) - comp-list (assq day-of-download compressed) - alist (cdr alist)) - (if comp-list - (setcdr comp-list (cons article-id (cdr comp-list))) - (push (list day-of-download article-id) compressed))) - (setq alist compressed) - (while alist - (setq comp-list (pop alist)) - (setcdr comp-list - (gnus-compress-sequence (nreverse (cdr comp-list))))) - (princ compressed (current-buffer))) - (insert "\n2\n") - (write-file file) - (when history-file - (delete-file history-file)) - t)))) - -;; End of Oort Gnus v0.08 updates - -;; No Gnus v0.3 - This release provides a mechanism for upgrading gnus -;; from previous versions. Therefore, the previous -;; hacks to handle a gnus-agent-expire-days that -;; specifies a list of values can be removed. - -(defun gnus-agent-unlist-expire-days (converting-to) - (when (listp gnus-agent-expire-days) - (let (buffer) - (unwind-protect - (save-window-excursion - (setq buffer (gnus-get-buffer-create " *Gnus agent upgrade*")) - (set-buffer buffer) - (erase-buffer) - (insert "The definition of gnus-agent-expire-days has been changed.\nYou currently have it set to the list:\n ") - (gnus-pp gnus-agent-expire-days) - - (insert - (format-message - "\nIn order to use version `%s' of gnus, you will need to set\n" - converting-to)) - (insert "gnus-agent-expire-days to an integer. If you still wish to set different\n") - (insert "expiration days to individual groups, you must instead set the\n") - (insert (format-message - "`agent-days-until-old' group and/or topic parameter.\n")) - (insert "\n") - (insert "If you would like, gnus can iterate over every group comparing its name to the\n") - (insert "regular expressions that you currently have in gnus-agent-expire-days. When\n") - (insert (format-message - "gnus finds a match, it will update that group's `agent-days-until-old' group\n")) - (insert "parameter to the value associated with the regular expression.\n") - (insert "\n") - (insert "Whether gnus assigns group parameters, or not, gnus will terminate with an\n") - (insert "ERROR as soon as this function completes. The reason is that you must\n") - (insert "manually edit your configuration to either not set gnus-agent-expire-days or\n") - (insert "to set it to an integer before gnus can be used.\n") - (insert "\n") - (insert "Once you have successfully edited gnus-agent-expire-days, gnus will be able to\n") - (insert "execute past this function.\n") - (insert "\n") - (insert "Should gnus use gnus-agent-expire-days to assign\n") - (insert "agent-days-until-old parameters to individual groups? (Y/N)") - - (switch-to-buffer buffer) - (beep) - (beep) - - (let ((echo-keystrokes 0) - c) - (while (progn (setq c (read-char-exclusive)) - (cond ((or (eq c ?y) (eq c ?Y)) - (save-excursion - (let ((groups (gnus-group-listed-groups))) - (while groups - (let* ((group (pop groups)) - (days gnus-agent-expire-days) - (day (catch 'found - (while days - (when (eq 0 (string-match - (caar days) - group)) - (throw 'found (cadr (car days)))) - (setq days (cdr days))) - nil))) - (when day - (gnus-group-set-parameter group 'agent-days-until-old - day)))))) - nil - ) - ((or (eq c ?n) (eq c ?N)) - nil) - (t - t)))))) - (kill-buffer buffer)) - (error "Change gnus-agent-expire-days to an integer for gnus to start")))) - -;; The gnus-agent-unlist-expire-days has its own conversion prompt. -;; Therefore, hide the default prompt. -(gnus-convert-mark-converter-prompt 'gnus-agent-unlist-expire-days t) - -(defun gnus-agent-unhook-expire-days (_converting-to) - "Remove every lambda from `gnus-group-prepare-hook' that mention the -symbol `gnus-agent-do-once' in their definition. This should NOT be -necessary as gnus-agent.el no longer adds them. However, it is -possible that the hook was persistently saved." - (let ((h t)) ; Iterate from bgn of hook. - (while h - (let ((func (progn (when (eq h t) - ;; Init h to list of functions. - (setq h (cond ((listp gnus-group-prepare-hook) - gnus-group-prepare-hook) - ((boundp 'gnus-group-prepare-hook) - (list gnus-group-prepare-hook))))) - (pop h)))) - - (when (cond ((byte-code-function-p func) - ;; Search def. of compiled function for - ;; gnus-agent-do-once string. - (let* (definition - print-level - print-length - (standard-output - (lambda (char) - (setq definition (cons char definition))))) - (princ func) ; Populates definition with reversed list - ; of characters. - (let* ((i (length definition)) - (s (make-string i 0))) - (while definition - (aset s (setq i (1- i)) (pop definition))) - - (string-match "\\bgnus-agent-do-once\\b" s)))) - ((listp func) - (eq (cadr (nth 2 func)) 'gnus-agent-do-once) ; Handles eval'd lambda. - )) - - (remove-hook 'gnus-group-prepare-hook func) - ;; I don't what remove-hook is going to actually do to the - ;; hook list so start over from the beginning. - (setq h t)))))) - -;; gnus-agent-unhook-expire-days is safe in that it does not modify -;; the .newsrc.eld file. -(gnus-convert-mark-converter-prompt 'gnus-agent-unhook-expire-days t) - -(provide 'legacy-gnus-agent) - -;;; legacy-gnus-agent.el ends here commit 7ea3a464036f123f70d89b4571afcdeb3e650688 Author: Po Lu Date: Tue Mar 12 09:58:51 2024 +0800 Resolve inconsistency between Android and XDG notifications * doc/lispref/os.texi (Desktop Notifications): * src/androidselect.c (android_notifications_notify_1) (Fandroid_notifications_notify, android_notification_action) (syms_of_androidselect): Rename `:on-cancel' to `:on-close'. diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi index 65c5ed2b4cc..435886320fd 100644 --- a/doc/lispref/os.texi +++ b/doc/lispref/os.texi @@ -3242,7 +3242,7 @@ of parameters analogous to its namesake in @item :body @var{body} @item :replaces-id @var{replaces-id} @item :on-action @var{on-action} -@item :on-cancel @var{on-cancel} +@item :on-cancel @var{on-close} @item :actions @var{actions} @item :resident @var{resident} These have the same meaning as they do when used in calls to diff --git a/src/androidselect.c b/src/androidselect.c index a5a4c4c2e59..521133976a7 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -568,7 +568,7 @@ android_locate_icon (const char *name) /* Display a desktop notification with the provided TITLE, BODY, REPLACES_ID, GROUP, ICON, URGENCY, ACTIONS, RESIDENT, ACTION_CB and - CANCEL_CB. Return an identifier for the resulting notification. */ + CLOSE_CB. Return an identifier for the resulting notification. */ static intmax_t android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, @@ -576,7 +576,7 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, Lisp_Object group, Lisp_Object icon, Lisp_Object urgency, Lisp_Object actions, Lisp_Object resident, Lisp_Object action_cb, - Lisp_Object cancel_cb) + Lisp_Object close_cb) { static intmax_t counter; intmax_t id; @@ -740,8 +740,8 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, /* If callbacks are provided, save them into notification_table. */ - if (!NILP (action_cb) || !NILP (cancel_cb) || !NILP (resident)) - Fputhash (build_string (identifier), list3 (action_cb, cancel_cb, + if (!NILP (action_cb) || !NILP (close_cb) || !NILP (resident)) + Fputhash (build_string (identifier), list3 (action_cb, close_cb, resident), notification_table); @@ -776,7 +776,7 @@ keywords is understood: :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-cancel Function to call if the notification is dismissed, + :on-close Function to call if the notification is dismissed, with the notification id and the symbol `undefined' for arguments. @@ -816,7 +816,7 @@ usage: (android-notifications-notify &rest ARGS) */) { Lisp_Object title, body, replaces_id, group, urgency, resident; Lisp_Object icon; - Lisp_Object key, value, actions, action_cb, cancel_cb; + Lisp_Object key, value, actions, action_cb, close_cb; ptrdiff_t i; if (!android_init_gui) @@ -824,7 +824,7 @@ usage: (android-notifications-notify &rest ARGS) */) /* Clear each variable above. */ title = body = replaces_id = group = icon = urgency = actions = Qnil; - resident = action_cb = cancel_cb = Qnil; + resident = action_cb = close_cb = Qnil; /* If NARGS is odd, error. */ @@ -856,8 +856,8 @@ usage: (android-notifications-notify &rest ARGS) */) resident = value; else if (EQ (key, QCon_action)) action_cb = value; - else if (EQ (key, QCon_cancel)) - cancel_cb = value; + else if (EQ (key, QCon_close)) + close_cb = value; } /* Demand at least TITLE and BODY be present. */ @@ -884,7 +884,7 @@ usage: (android-notifications-notify &rest ARGS) */) return make_int (android_notifications_notify_1 (title, body, replaces_id, group, icon, urgency, actions, resident, - action_cb, cancel_cb)); + action_cb, close_cb)); } /* Run callbacks in response to a notification being deleted. @@ -1003,7 +1003,7 @@ syms_of_androidselect (void) DEFSYM (QCactions, ":actions"); DEFSYM (QCresident, ":resident"); DEFSYM (QCon_action, ":on-action"); - DEFSYM (QCon_cancel, ":on-cancel"); + DEFSYM (QCon_close, ":on-close"); DEFSYM (Qlow, "low"); DEFSYM (Qnormal, "normal"); commit fd33b637e986e7ec1c34a1358b5c71e31db95c11 Author: Po Lu Date: Tue Mar 12 09:54:54 2024 +0800 ; Fix omission from last change * src/androidselect.c (android_notification_deleted): Adjust for changed notification list format. diff --git a/src/androidselect.c b/src/androidselect.c index bcb7bcd2c3b..a5a4c4c2e59 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -908,7 +908,7 @@ android_notification_deleted (struct android_notification_event *event, && sscanf (event->tag, "%*d.%*ld.%jd", &id) > 0) { ie->kind = NOTIFICATION_EVENT; - ie->arg = list3 (XCDR (item), make_int (id), + ie->arg = list3 (XCAR (XCDR (item)), make_int (id), Qundefined); } } commit d7ded996082503ca00546c220c7ce8d96e16b76a Author: Po Lu Date: Tue Mar 12 09:48:53 2024 +0800 Implement notification residency on Android * doc/lispref/os.texi (Desktop Notifications): Document support for `:resident'. * java/org/gnu/emacs/EmacsService.java (cancelNotification): * src/android.c (android_init_emacs_service): * src/android.h (struct android_emacs_service): New function. * src/androidselect.c (android_notifications_notify_1) (Fandroid_notifications_notify): New parameter QCresident; save it within notification lists. (android_notification_deleted, android_notification_action): Adjust for changes to the format of notification lists and cancel non-resident notifications when an action is selected. (syms_of_androidselect): : New symbol. diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi index ecd88a39489..65c5ed2b4cc 100644 --- a/doc/lispref/os.texi +++ b/doc/lispref/os.texi @@ -3244,11 +3244,13 @@ of parameters analogous to its namesake in @item :on-action @var{on-action} @item :on-cancel @var{on-cancel} @item :actions @var{actions} +@item :resident @var{resident} These have the same meaning as they do when used in calls to -@code{notifications-notify}. +@code{notifications-notify}, except that no more than three non-default +actions will be displayed. @item :urgency @var{urgency} -The set of values for @var{urgency} is the same as with +The set of accepted 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}, except under Android 7.1 and earlier. diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d17ba597d8e..9bc40d63311 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1967,4 +1967,29 @@ In addition, arbitrary runtime exceptions (such as else requestStorageAccess30 (); } + + + + /* Notification miscellany. */ + + /* Cancel any notification displayed with the tag TAG. */ + + public void + cancelNotification (final String string) + { + Object tem; + final NotificationManager manager; + + tem = getSystemService (Context.NOTIFICATION_SERVICE); + manager = (NotificationManager) tem; + + runOnUiThread (new Runnable () { + @Override + public void + run () + { + manager.cancel (string, 2); + } + }); + } }; diff --git a/src/android.c b/src/android.c index 125bb5209c3..dcd5c6d99c7 100644 --- a/src/android.c +++ b/src/android.c @@ -1688,6 +1688,8 @@ android_init_emacs_service (void) "externalStorageAvailable", "()Z"); FIND_METHOD (request_storage_access, "requestStorageAccess", "()V"); + FIND_METHOD (cancel_notification, + "cancelNotification", "(Ljava/lang/String;)V"); #undef FIND_METHOD } diff --git a/src/android.h b/src/android.h index ee634a3e76c..2ca3d7e1446 100644 --- a/src/android.h +++ b/src/android.h @@ -302,6 +302,7 @@ struct android_emacs_service jmethodID valid_authority; jmethodID external_storage_available; jmethodID request_storage_access; + jmethodID cancel_notification; }; extern JNIEnv *android_java_env; diff --git a/src/androidselect.c b/src/androidselect.c index 04f4cf1573f..bcb7bcd2c3b 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -567,15 +567,15 @@ android_locate_icon (const char *name) } /* Display a desktop notification with the provided TITLE, BODY, - REPLACES_ID, GROUP, ICON, URGENCY, ACTIONS, ACTION_CB and CANCEL_CB. - Return an identifier for the resulting notification. */ + REPLACES_ID, GROUP, ICON, URGENCY, ACTIONS, RESIDENT, ACTION_CB and + CANCEL_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 action_cb, + Lisp_Object resident, Lisp_Object action_cb, Lisp_Object cancel_cb) { static intmax_t counter; @@ -740,8 +740,9 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, /* If callbacks are provided, save them into notification_table. */ - if (!NILP (action_cb) || !NILP (cancel_cb)) - Fputhash (build_string (identifier), Fcons (action_cb, cancel_cb), + if (!NILP (action_cb) || !NILP (cancel_cb) || !NILP (resident)) + Fputhash (build_string (identifier), list3 (action_cb, cancel_cb, + resident), notification_table); /* Return the ID. */ @@ -755,12 +756,12 @@ ARGS must contain keywords followed by values. Each of the following keywords is understood: :title The notification title. - :body The notification body. + :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 + :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 ...) @@ -770,6 +771,8 @@ keywords is understood: its existence is implied, and its TITLE is ignored. No more than three actions can be defined, not counting any action with "default" as its key. + :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. @@ -811,7 +814,7 @@ this function. usage: (android-notifications-notify &rest ARGS) */) (ptrdiff_t nargs, Lisp_Object *args) { - Lisp_Object title, body, replaces_id, group, urgency; + Lisp_Object title, body, replaces_id, group, urgency, resident; Lisp_Object icon; Lisp_Object key, value, actions, action_cb, cancel_cb; ptrdiff_t i; @@ -821,7 +824,7 @@ usage: (android-notifications-notify &rest ARGS) */) /* Clear each variable above. */ title = body = replaces_id = group = icon = urgency = actions = Qnil; - action_cb = cancel_cb = Qnil; + resident = action_cb = cancel_cb = Qnil; /* If NARGS is odd, error. */ @@ -849,6 +852,8 @@ usage: (android-notifications-notify &rest ARGS) */) icon = value; else if (EQ (key, QCactions)) actions = value; + else if (EQ (key, QCresident)) + resident = value; else if (EQ (key, QCon_action)) action_cb = value; else if (EQ (key, QCon_cancel)) @@ -878,8 +883,8 @@ usage: (android-notifications-notify &rest ARGS) */) return make_int (android_notifications_notify_1 (title, body, replaces_id, group, icon, urgency, - actions, action_cb, - cancel_cb)); + actions, resident, + action_cb, cancel_cb)); } /* Run callbacks in response to a notification being deleted. @@ -899,7 +904,7 @@ android_notification_deleted (struct android_notification_event *event, if (!NILP (item)) Fremhash (tag, notification_table); - if (CONSP (item) && FUNCTIONP (XCDR (item)) + if (CONSP (item) && FUNCTIONP (XCAR (XCDR (item))) && sscanf (event->tag, "%*d.%*ld.%jd", &id) > 0) { ie->kind = NOTIFICATION_EVENT; @@ -919,6 +924,8 @@ android_notification_action (struct android_notification_event *event, { Lisp_Object item, tag; intmax_t id; + jstring tag_object; + jmethodID method; tag = build_string (event->tag); item = Fgethash (tag, notification_table, Qnil); @@ -929,6 +936,29 @@ android_notification_action (struct android_notification_event *event, 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); + } } @@ -971,6 +1001,7 @@ syms_of_androidselect (void) DEFSYM (QCurgency, ":urgency"); DEFSYM (QCicon, ":icon"); DEFSYM (QCactions, ":actions"); + DEFSYM (QCresident, ":resident"); DEFSYM (QCon_action, ":on-action"); DEFSYM (QCon_cancel, ":on-cancel"); commit bf38783c32e794e46fd03210242f265f34257940 Author: Po Lu Date: Tue Mar 12 08:51:52 2024 +0800 Fix notification cancellation detection on Android * java/org/gnu/emacs/EmacsDesktopNotification.java (display1): Don't specify FLAG_ONE_SHOT in cancel intents. diff --git a/java/org/gnu/emacs/EmacsDesktopNotification.java b/java/org/gnu/emacs/EmacsDesktopNotification.java index f52c3d9d4fb..d05ed2e6203 100644 --- a/java/org/gnu/emacs/EmacsDesktopNotification.java +++ b/java/org/gnu/emacs/EmacsDesktopNotification.java @@ -281,11 +281,9 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) pending = PendingIntent.getBroadcast (context, 0, intent, - (PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_ONE_SHOT)); + PendingIntent.FLAG_IMMUTABLE); else - pending = PendingIntent.getBroadcast (context, 0, intent, - PendingIntent.FLAG_ONE_SHOT); + pending = PendingIntent.getBroadcast (context, 0, intent, 0); notification.deleteIntent = pending; manager.notify (tag, 2, notification); commit a7a37341cad230448e487d0ffa343eeeb8a66a65 Author: Po Lu Date: Mon Mar 11 21:40:47 2024 +0800 Implement notification callbacks on Android * doc/lispref/os.texi (Desktop Notifications): Document that :on-cancel, :on-action and :actions are now supported on Android. * java/org/gnu/emacs/EmacsActivity.java (onNewIntent): New function. * java/org/gnu/emacs/EmacsDesktopNotification.java (NOTIFICATION_ACTION, NOTIFICATION_TAG, NOTIFICATION_DISMISSED): New constants. : New fields. (insertActions): New function. (display1, display): Insert actions on Jelly Bean and up, and arrange to be notified when the notification is dismissed. (CancellationReceiver): New class. * java/org/gnu/emacs/EmacsNative.java (sendNotificationDeleted) (sendNotificationAction): New functions. * src/android.c (sendDndDrag, sendDndUri, sendDndText): Correct return types. (sendNotificationDeleted, sendNotificationAction) (android_exception_check_5, android_exception_check_6): New functions. * src/android.h: * src/androidgui.h (struct android_notification_event): New structure. (union android_event): New member for notification events. * src/androidselect.c (android_init_emacs_desktop_notification): Update JNI signatures. (android_notifications_notify_1, Fandroid_notifications_notify): New arguments ACTIONS, ACTION_CB and CANCEL_CB. Convert and record them as appropriate. (android_notification_deleted, android_notification_action): New functions. (syms_of_androidselect): Prepare a hash table of outstanding notifications. New defsyms. * src/androidterm.c (handle_one_android_event) : Dispatch event contents to androidselect.c for processing. * src/androidterm.h: * src/androidvfs.c (java_string_class): Export. * src/keyboard.c (kbd_buffer_get_event) : Call callback specified by the event. * src/termhooks.h (enum event_kind) [HAVE_ANDROID]: New enum NOTIFICATION_EVENT. diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi index 60ae57d4c1d..ecd88a39489 100644 --- a/doc/lispref/os.texi +++ b/doc/lispref/os.texi @@ -3241,6 +3241,9 @@ of parameters analogous to its namesake in @item :title @var{title} @item :body @var{body} @item :replaces-id @var{replaces-id} +@item :on-action @var{on-action} +@item :on-cancel @var{on-cancel} +@item :actions @var{actions} These have the same meaning as they do when used in calls to @code{notifications-notify}. diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 66a1e41d84c..06b9c0f005d 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -453,6 +453,27 @@ public class EmacsActivity extends Activity syncFullscreenWith (window); } + @Override + public final void + onNewIntent (Intent intent) + { + String tag, action; + + /* This function is called when EmacsActivity is relaunched from a + notification. */ + + if (intent == null || EmacsService.SERVICE == null) + return; + + tag = intent.getStringExtra (EmacsDesktopNotification.NOTIFICATION_TAG); + action + = intent.getStringExtra (EmacsDesktopNotification.NOTIFICATION_ACTION); + + if (tag == null || action == null) + return; + + EmacsNative.sendNotificationAction (tag, action); + } @Override diff --git a/java/org/gnu/emacs/EmacsDesktopNotification.java b/java/org/gnu/emacs/EmacsDesktopNotification.java index fb35e3fea1f..f52c3d9d4fb 100644 --- a/java/org/gnu/emacs/EmacsDesktopNotification.java +++ b/java/org/gnu/emacs/EmacsDesktopNotification.java @@ -24,9 +24,12 @@ import android.app.NotificationChannel; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.net.Uri; + import android.os.Build; import android.widget.RemoteViews; @@ -44,6 +47,16 @@ public final class EmacsDesktopNotification { + /* Intent tag for notification action data. */ + public static final String NOTIFICATION_ACTION = "emacs:notification_action"; + + /* Intent tag for notification IDs. */ + public static final String NOTIFICATION_TAG = "emacs:notification_tag"; + + /* Action ID assigned to the broadcast receiver which should be + notified of any notification's being dismissed. */ + public static final String NOTIFICATION_DISMISSED = "org.gnu.emacs.DISMISSED"; + /* The content of this desktop notification. */ public final String content; @@ -66,10 +79,15 @@ public final class EmacsDesktopNotification /* The importance of this notification's group. */ public final int importance; + /* Array of actions and their user-facing text to be offered by this + notification. */ + public final String[] actions, titles; + public EmacsDesktopNotification (String title, String content, String group, String tag, int icon, - int importance) + int importance, + String[] actions, String[] titles) { this.content = content; this.title = title; @@ -77,12 +95,68 @@ public final class EmacsDesktopNotification this.tag = tag; this.icon = icon; this.importance = importance; + this.actions = actions; + this.titles = titles; } /* Functions for displaying desktop notifications. */ + /* Insert each action in actions and titles into the notification + builder BUILDER, with pending intents created with CONTEXT holding + suitable metadata. */ + + @SuppressWarnings ("deprecation") + private void + insertActions (Context context, Notification.Builder builder) + { + int i; + PendingIntent pending; + Intent intent; + Notification.Action.Builder action; + + if (actions == null) + return; + + for (i = 0; i < actions.length; ++i) + { + /* Actions named default should not be displayed. */ + if (actions[i].equals ("default")) + continue; + + intent = new Intent (context, EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + + /* Pending intents are specific to combinations of class, action + and data, but not information provided as extras. In order + that its target may be invoked with the action and tag set + below, generate a URL from those two elements and specify it + as the intent data, which ensures that the intent allocated + fully reflects the duo. */ + + intent.setData (new Uri.Builder ().scheme ("action") + .appendPath (tag).appendPath (actions[i]) + .build ()); + intent.putExtra (NOTIFICATION_ACTION, actions[i]); + intent.putExtra (NOTIFICATION_TAG, tag); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + pending = PendingIntent.getActivity (context, 0, intent, + PendingIntent.FLAG_IMMUTABLE); + else + pending = PendingIntent.getActivity (context, 0, intent, 0); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + { + action = new Notification.Action.Builder (0, titles[i], pending); + builder.addAction (action.build ()); + } + else + builder.addAction (0, titles[i], pending); + } + } + /* Internal helper for `display' executed on the main thread. */ @SuppressWarnings ("deprecation") /* Notification.Builder (Context). */ @@ -97,6 +171,7 @@ public final class EmacsDesktopNotification Intent intent; PendingIntent pending; int priority; + Notification.Builder builder; tem = context.getSystemService (Context.NOTIFICATION_SERVICE); manager = (NotificationManager) tem; @@ -108,13 +183,16 @@ public final class EmacsDesktopNotification (such as its importance) will be overridden. */ channel = new NotificationChannel (group, group, importance); manager.createNotificationChannel (channel); + builder = new Notification.Builder (context, group); - /* Create a notification object and display it. */ - notification = (new Notification.Builder (context, group) - .setContentTitle (title) - .setContentText (content) - .setSmallIcon (icon) - .build ()); + /* Create and configure a notification object and display + it. */ + + builder.setContentTitle (title); + builder.setContentText (content); + builder.setSmallIcon (icon); + insertActions (context, builder); + notification = builder.build (); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { @@ -138,12 +216,16 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) break; } - notification = (new Notification.Builder (context) - .setContentTitle (title) - .setContentText (content) - .setSmallIcon (icon) - .setPriority (priority) - .build ()); + builder = new Notification.Builder (context); + builder.setContentTitle (title); + builder.setContentText (content); + builder.setSmallIcon (icon); + builder.setPriority (priority); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + insertActions (context, builder); + + notification = builder.build (); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) notification.priority = priority; @@ -170,6 +252,12 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) intent = new Intent (context, EmacsActivity.class); intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData (new Uri.Builder () + .scheme ("action") + .appendPath (tag) + .build ()); + intent.putExtra (NOTIFICATION_ACTION, "default"); + intent.putExtra (NOTIFICATION_TAG, tag); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) pending = PendingIntent.getActivity (context, 0, intent, @@ -179,6 +267,27 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) notification.contentIntent = pending; + /* Provide a cancellation intent to respond to notification + dismissals. */ + + intent = new Intent (context, CancellationReceiver.class); + intent.setAction (NOTIFICATION_DISMISSED); + intent.setPackage ("org.gnu.emacs"); + intent.setData (new Uri.Builder () + .scheme ("action") + .appendPath (tag) + .build ()); + intent.putExtra (NOTIFICATION_TAG, tag); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + pending = PendingIntent.getBroadcast (context, 0, intent, + (PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_ONE_SHOT)); + else + pending = PendingIntent.getBroadcast (context, 0, intent, + PendingIntent.FLAG_ONE_SHOT); + + notification.deleteIntent = pending; manager.notify (tag, 2, notification); } @@ -199,4 +308,31 @@ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) } }); } + + + + /* Broadcast receiver. This is something of a system-wide callback + arranged to be invoked whenever a notification posted by Emacs is + dismissed, in order to relay news of its dismissal to + androidselect.c and run or remove callbacks as appropriate. */ + + public static class CancellationReceiver extends BroadcastReceiver + { + @Override + public void + onReceive (Context context, Intent intent) + { + String tag, action; + + if (intent == null || EmacsService.SERVICE == null) + return; + + tag = intent.getStringExtra (NOTIFICATION_TAG); + + if (tag == null) + return; + + EmacsNative.sendNotificationDeleted (tag); + } + }; }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cd0e70923d1..6845f833908 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -196,6 +196,12 @@ public static native long sendDndUri (short window, int x, int y, public static native long sendDndText (short window, int x, int y, String text); + /* Send an ANDROID_NOTIFICATION_CANCELED event. */ + public static native void sendNotificationDeleted (String tag); + + /* Send an ANDROID_NOTIFICATION_ACTION event. */ + public static native void sendNotificationAction (String tag, String action); + /* Return the file name associated with the specified file descriptor, or NULL if there is none. */ public static native byte[] getProcName (int fd); diff --git a/src/android.c b/src/android.c index d7bd06f1f34..125bb5209c3 100644 --- a/src/android.c +++ b/src/android.c @@ -2457,7 +2457,7 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, return event_serial; } -JNIEXPORT jboolean JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDndDrag) (JNIEnv *env, jobject object, jshort window, jint x, jint y) { @@ -2477,7 +2477,7 @@ NATIVE_NAME (sendDndDrag) (JNIEnv *env, jobject object, return event_serial; } -JNIEXPORT jboolean JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDndUri) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jstring string) @@ -2514,7 +2514,7 @@ NATIVE_NAME (sendDndUri) (JNIEnv *env, jobject object, return event_serial; } -JNIEXPORT jboolean JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDndText) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jstring string) @@ -2551,6 +2551,85 @@ NATIVE_NAME (sendDndText) (JNIEnv *env, jobject object, return event_serial; } +JNIEXPORT jlong JNICALL +NATIVE_NAME (sendNotificationDeleted) (JNIEnv *env, jobject object, + jstring tag) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + const char *characters; + + event.notification.type = ANDROID_NOTIFICATION_DELETED; + event.notification.serial = ++event_serial; + event.notification.window = ANDROID_NONE; + + /* TAG is guaranteed to be an ASCII string, of which the JNI character + encoding is a superset. */ + characters = (*env)->GetStringUTFChars (env, tag, NULL); + if (!characters) + return 0; + + event.notification.tag = strdup (characters); + (*env)->ReleaseStringUTFChars (env, tag, characters); + if (!event.notification.tag) + return 0; + + event.notification.action = NULL; + event.notification.length = 0; + + android_write_event (&event); + return event_serial; +} + +JNIEXPORT jlong JNICALL +NATIVE_NAME (sendNotificationAction) (JNIEnv *env, jobject object, + jstring tag, jstring action) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + const void *characters; + jsize length; + uint16_t *buffer; + + event.notification.type = ANDROID_NOTIFICATION_ACTION; + event.notification.serial = ++event_serial; + event.notification.window = ANDROID_NONE; + + /* TAG is guaranteed to be an ASCII string, of which the JNI character + encoding is a superset. */ + characters = (*env)->GetStringUTFChars (env, tag, NULL); + if (!characters) + return 0; + + event.notification.tag = strdup (characters); + (*env)->ReleaseStringUTFChars (env, tag, characters); + if (!event.notification.tag) + return 0; + + length = (*env)->GetStringLength (env, action); + buffer = malloc (length * sizeof *buffer); + characters = (*env)->GetStringChars (env, action, NULL); + + if (!characters) + { + /* The JVM has run out of memory; return and let the out of memory + error take its course. */ + xfree (event.notification.tag); + return 0; + } + + memcpy (buffer, characters, length * sizeof *buffer); + (*env)->ReleaseStringChars (env, action, characters); + + event.notification.action = buffer; + event.notification.length = length; + + android_write_event (&event); + return event_serial; +} + JNIEXPORT jboolean JNICALL NATIVE_NAME (shouldForwardMultimediaButtons) (JNIEnv *env, jobject object) @@ -6310,6 +6389,82 @@ android_exception_check_4 (jobject object, jobject object1, memory_full (0); } +/* Like android_exception_check_4, except it takes more than four local + reference arguments. */ + +void +android_exception_check_5 (jobject object, jobject object1, + jobject object2, jobject object3, + jobject object4) +{ + if (likely (!(*android_java_env)->ExceptionCheck (android_java_env))) + return; + + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error. " + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + + if (object) + ANDROID_DELETE_LOCAL_REF (object); + + if (object1) + ANDROID_DELETE_LOCAL_REF (object1); + + if (object2) + ANDROID_DELETE_LOCAL_REF (object2); + + if (object3) + ANDROID_DELETE_LOCAL_REF (object3); + + if (object4) + ANDROID_DELETE_LOCAL_REF (object4); + + memory_full (0); +} + + +/* Like android_exception_check_5, except it takes more than five local + reference arguments. */ + +void +android_exception_check_6 (jobject object, jobject object1, + jobject object2, jobject object3, + jobject object4, jobject object5) +{ + if (likely (!(*android_java_env)->ExceptionCheck (android_java_env))) + return; + + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error. " + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + + if (object) + ANDROID_DELETE_LOCAL_REF (object); + + if (object1) + ANDROID_DELETE_LOCAL_REF (object1); + + if (object2) + ANDROID_DELETE_LOCAL_REF (object2); + + if (object3) + ANDROID_DELETE_LOCAL_REF (object3); + + if (object4) + ANDROID_DELETE_LOCAL_REF (object4); + + if (object5) + ANDROID_DELETE_LOCAL_REF (object5); + + memory_full (0); +} + /* Check for JNI problems based on the value of OBJECT. Signal out of memory if OBJECT is NULL. OBJECT1 means the diff --git a/src/android.h b/src/android.h index e1834cebf68..ee634a3e76c 100644 --- a/src/android.h +++ b/src/android.h @@ -118,6 +118,10 @@ extern void android_exception_check_1 (jobject); extern void android_exception_check_2 (jobject, jobject); extern void android_exception_check_3 (jobject, jobject, jobject); extern void android_exception_check_4 (jobject, jobject, jobject, jobject); +extern void android_exception_check_5 (jobject, jobject, jobject, jobject, + jobject); +extern void android_exception_check_6 (jobject, jobject, jobject, jobject, + jobject, jobject); extern void android_exception_check_nonnull (void *, jobject); extern void android_exception_check_nonnull_1 (void *, jobject, jobject); @@ -306,6 +310,9 @@ extern JNIEnv *android_java_env; extern JavaVM *android_jvm; #endif /* THREADS_ENABLED */ +/* The Java String class. */ +extern jclass java_string_class; + /* The EmacsService object. */ extern jobject emacs_service; diff --git a/src/androidgui.h b/src/androidgui.h index 73b60c483d3..d89aee51055 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -251,6 +251,8 @@ enum android_event_type ANDROID_DND_DRAG_EVENT, ANDROID_DND_URI_EVENT, ANDROID_DND_TEXT_EVENT, + ANDROID_NOTIFICATION_DELETED, + ANDROID_NOTIFICATION_ACTION, }; struct android_any_event @@ -535,6 +537,29 @@ struct android_dnd_event size_t length; }; +struct android_notification_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* The event serial. */ + unsigned long serial; + + /* The window that gave rise to the event (None). */ + android_window window; + + /* The identifier of the notification whose status changed. + Must be deallocated with `free'. */ + char *tag; + + /* The action that was activated, if any. Must be deallocated with + `free'. */ + unsigned short *action; + + /* Length of that data. */ + size_t length; +}; + union android_event { enum android_event_type type; @@ -571,6 +596,10 @@ union android_event protocol, whereas there exist several competing X protocols implemented in terms of X client messages. */ struct android_dnd_event dnd; + + /* X provides no equivalent interface for displaying + notifications. */ + struct android_notification_event notification; }; enum diff --git a/src/androidselect.c b/src/androidselect.c index 61f1c6045db..04f4cf1573f 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -30,6 +30,7 @@ along with GNU Emacs. If not, see . */ #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 @@ -490,6 +491,9 @@ struct android_emacs_desktop_notification /* 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. */ @@ -521,7 +525,8 @@ android_init_emacs_desktop_notification (void) FIND_METHOD (init, "", "(Ljava/lang/String;" "Ljava/lang/String;Ljava/lang/String;" - "Ljava/lang/String;II)V"); + "Ljava/lang/String;II[Ljava/lang/String;" + "[Ljava/lang/String;)V"); FIND_METHOD (display, "display", "()V"); #undef FIND_METHOD } @@ -562,25 +567,32 @@ android_locate_icon (const char *name) } /* Display a desktop notification with the provided TITLE, BODY, - REPLACES_ID, GROUP, ICON, and URGENCY. Return an identifier for - the resulting notification. */ + REPLACES_ID, GROUP, ICON, URGENCY, ACTIONS, ACTION_CB and CANCEL_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 urgency, Lisp_Object actions, + Lisp_Object action_cb, + Lisp_Object cancel_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; if (EQ (urgency, Qlow)) type = 2; /* IMPORTANCE_LOW */ @@ -591,6 +603,29 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, else signal_error ("Invalid notification importance given", urgency); + 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. */ @@ -626,14 +661,62 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, = (*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); - android_exception_check_4 (title1, body1, group1, identifier1); + identifier1, icon1, type, + action_keys, action_titles); + android_exception_check_6 (title1, body1, group1, identifier1, + action_titles, action_keys); /* Delete unused local references. */ ANDROID_DELETE_LOCAL_REF (title1); @@ -641,6 +724,12 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, ANDROID_DELETE_LOCAL_REF (group1); ANDROID_DELETE_LOCAL_REF (identifier1); + if (action_keys) + ANDROID_DELETE_LOCAL_REF (action_keys); + + if (action_titles) + ANDROID_DELETE_LOCAL_REF (action_titles); + /* Display the notification. */ (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, notification, @@ -649,6 +738,12 @@ android_notifications_notify_1 (Lisp_Object title, Lisp_Object body, android_exception_check_1 (notification); ANDROID_DELETE_LOCAL_REF (notification); + /* If callbacks are provided, save them into notification_table. */ + + if (!NILP (action_cb) || !NILP (cancel_cb)) + Fputhash (build_string (identifier), Fcons (action_cb, cancel_cb), + notification_table); + /* Return the ID. */ return id; } @@ -659,14 +754,28 @@ DEFUN ("android-notifications-notify", Fandroid_notifications_notify, 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. + :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 can be defined, not + counting any action with "default" as its key. + :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-cancel Function to call if the notification is dismissed, + with the notification id and the symbol `undefined' + for arguments. The notification group is ignored on Android 7.1 and earlier versions of Android. Outside such older systems, it identifies a category that @@ -686,6 +795,9 @@ within the "android.R.drawable" class designating an icon with a transparent background. If no icon is 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 @@ -701,14 +813,15 @@ usage: (android-notifications-notify &rest ARGS) */) { Lisp_Object title, body, replaces_id, group, urgency; Lisp_Object icon; - Lisp_Object key, value; + Lisp_Object key, value, actions, action_cb, cancel_cb; ptrdiff_t i; if (!android_init_gui) error ("No Android display connection!"); /* Clear each variable above. */ - title = body = replaces_id = group = icon = urgency = Qnil; + title = body = replaces_id = group = icon = urgency = actions = Qnil; + action_cb = cancel_cb = Qnil; /* If NARGS is odd, error. */ @@ -734,6 +847,12 @@ usage: (android-notifications-notify &rest ARGS) */) urgency = value; else if (EQ (key, QCicon)) icon = value; + else if (EQ (key, QCactions)) + actions = value; + else if (EQ (key, QCon_action)) + action_cb = value; + else if (EQ (key, QCon_cancel)) + cancel_cb = value; } /* Demand at least TITLE and BODY be present. */ @@ -758,7 +877,58 @@ usage: (android-notifications-notify &rest ARGS) */) CHECK_STRING (icon); return make_int (android_notifications_notify_1 (title, body, replaces_id, - group, icon, urgency)); + group, icon, urgency, + actions, action_cb, + cancel_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 (XCDR (item)) + && sscanf (event->tag, "%*d.%*ld.%jd", &id) > 0) + { + ie->kind = NOTIFICATION_EVENT; + ie->arg = list3 (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; + + 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); + } } @@ -800,6 +970,9 @@ syms_of_androidselect (void) DEFSYM (QCgroup, ":group"); DEFSYM (QCurgency, ":urgency"); DEFSYM (QCicon, ":icon"); + DEFSYM (QCactions, ":actions"); + DEFSYM (QCon_action, ":on-action"); + DEFSYM (QCon_cancel, ":on-cancel"); DEFSYM (Qlow, "low"); DEFSYM (Qnormal, "normal"); @@ -814,4 +987,7 @@ syms_of_androidselect (void) defsubr (&Sandroid_get_clipboard_data); defsubr (&Sandroid_notifications_notify); + + notification_table = CALLN (Fmake_hash_table, QCtest, Qequal); + staticpro (¬ification_table); } diff --git a/src/androidterm.c b/src/androidterm.c index baf26abe322..f68f8a9ef62 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -1761,6 +1761,26 @@ handle_one_android_event (struct android_display_info *dpyinfo, free (event->dnd.uri_or_string); goto OTHER; + case ANDROID_NOTIFICATION_DELETED: + case ANDROID_NOTIFICATION_ACTION: + + if (event->notification.type == ANDROID_NOTIFICATION_DELETED) + android_notification_deleted (&event->notification, &inev.ie); + else + { + Lisp_Object action; + + action = android_decode_utf16 (event->notification.action, + event->notification.length); + android_notification_action (&event->notification, &inev.ie, + action); + } + + /* Free dynamically allocated data. */ + free (event->notification.tag); + free (event->notification.action); + goto OTHER; + default: goto OTHER; } @@ -4740,7 +4760,7 @@ android_sync_edit (void) /* Return a copy of the specified Java string and its length in *LENGTH. Use the JNI environment ENV. Value is NULL if copying - *the string fails. */ + the string fails. */ static unsigned short * android_copy_java_string (JNIEnv *env, jstring string, size_t *length) diff --git a/src/androidterm.h b/src/androidterm.h index 41c93067e82..ca6929bef0e 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -25,6 +25,7 @@ along with GNU Emacs. If not, see . */ #include "character.h" #include "dispextern.h" #include "font.h" +#include "termhooks.h" struct android_bitmap_record { @@ -464,6 +465,11 @@ extern void syms_of_sfntfont_android (void); #ifndef ANDROID_STUBIFY +extern void android_notification_deleted (struct android_notification_event *, + struct input_event *); +extern void android_notification_action (struct android_notification_event *, + struct input_event *, Lisp_Object); + extern void init_androidselect (void); extern void syms_of_androidselect (void); diff --git a/src/androidvfs.c b/src/androidvfs.c index d618e351204..4bb652f3eb7 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -292,7 +292,7 @@ struct android_parcel_file_descriptor_class }; /* The java.lang.String class. */ -static jclass java_string_class; +jclass java_string_class; /* Fields and methods associated with the Cursor class. */ static struct android_cursor_class cursor_class; diff --git a/src/keyboard.c b/src/keyboard.c index 1ba74a59537..91faf4582fa 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4187,6 +4187,16 @@ kbd_buffer_get_event (KBOARD **kbp, break; } +#ifdef HAVE_ANDROID + case NOTIFICATION_EVENT: + { + kbd_fetch_ptr = next_kbd_event (event); + input_pending = readable_events (0); + CALLN (Fapply, XCAR (event->ie.arg), XCDR (event->ie.arg)); + break; + } +#endif /* HAVE_ANDROID */ + #ifdef HAVE_EXT_MENU_BAR case MENU_BAR_ACTIVATE_EVENT: { diff --git a/src/termhooks.h b/src/termhooks.h index 8defebb20bd..d828c62ce33 100644 --- a/src/termhooks.h +++ b/src/termhooks.h @@ -343,6 +343,10 @@ enum event_kind the notification that was clicked. */ , NOTIFICATION_CLICKED_EVENT #endif /* HAVE_HAIKU */ +#ifdef HAVE_ANDROID + /* In a NOTIFICATION_EVENT, .arg is a lambda to evaluate. */ + , NOTIFICATION_EVENT +#endif /* HAVE_ANDROID */ }; /* Bit width of an enum event_kind tag at the start of structs and unions. */ commit 75cfc6c73faa1561018b1212156964a7919c69fe Author: Basil L. Contovounesios Date: Mon Mar 11 11:16:20 2024 +0100 ; Fix error message in last change to bindat.el Remove trailing period as per "(elisp) Error Symbols". Relates to the following discussion: https://lists.gnu.org/r/emacs-devel/2023-10/msg00473.html https://lists.gnu.org/r/emacs-devel/2024-03/msg00340.html diff --git a/lisp/emacs-lisp/bindat.el b/lisp/emacs-lisp/bindat.el index a2161022a89..ef0ec688dbd 100644 --- a/lisp/emacs-lisp/bindat.el +++ b/lisp/emacs-lisp/bindat.el @@ -205,7 +205,7 @@ ('strz (bindat--unpack-strz len)) ('vec (when (> len (length bindat-raw)) - (error "Vector length %d is greater than raw data length %d." + (error "Vector length %d is greater than raw data length %d" len (length bindat-raw))) (let ((v (make-vector len 0)) (vlen 1)) (if (consp vectype) commit 2d61ebb505977af4f9fd90f92a776599a73f8501 Author: Paul Eggert Date: Mon Mar 11 00:03:39 2024 -0700 Change bare-symbol back to match intent Also, attempt to document the intent better. Problem reported by Alan Mackenzie (Bug#69684). * src/data.c (Fbare_symbol): Do not signal if the SYM is a symbol with position and symbols-with-pos-enabled is nil. Instead, ignore symbols-with-pos-enabled, as that was the intent. * test/src/data-tests.el (data-tests-bare-symbol): New test, to help prevent this bug from reoccurring. diff --git a/doc/lispref/objects.texi b/doc/lispref/objects.texi index 41171bcaafc..279f449a994 100644 --- a/doc/lispref/objects.texi +++ b/doc/lispref/objects.texi @@ -2399,10 +2399,10 @@ The @code{equal} function recursively compares the contents of objects if they are integers, strings, markers, vectors, bool-vectors, byte-code function objects, char-tables, records, or font objects. -If @var{object1} or @var{object2} is a symbol with position, -@code{equal} regards it as its bare symbol when +If @var{object1} or @var{object2} contains symbols with position, +@code{equal} treats them as if they were their bare symbols when @code{symbols-with-pos-enabled} is non-@code{nil}. Otherwise -@code{equal} compares two symbols with position by recursively +@code{equal} compares two symbols with position by comparing their components. @xref{Symbols with Position}. Other objects are considered @code{equal} only if they are @code{eq}. diff --git a/doc/lispref/symbols.texi b/doc/lispref/symbols.texi index 6f9b1ef0ec7..c76bf3d3820 100644 --- a/doc/lispref/symbols.texi +++ b/doc/lispref/symbols.texi @@ -780,13 +780,16 @@ Symbol forms whose names start with @samp{#_} are not transformed. @cindex symbol with position @cindex bare symbol -A @dfn{symbol with position} is a symbol, the @dfn{bare symbol}, -together with an unsigned integer called the @dfn{position}. Symbols -with position don't themselves have entries in the obarray (though -their bare symbols do; @pxref{Creating Symbols}). - -Symbols with position are for the use of the byte compiler, which -records in them the position of each symbol occurrence and uses those +A @dfn{symbol with position} is a symbol, called the @dfn{bare symbol}, +together with a nonnegative fixnum called the @dfn{position}. +Even though a symbol with position often acts like its bare symbol, +it is not a symbol: instead, it is an object that has both a bare symbol +and a position. Because symbols with position are not symbols, +they don't have entries in the obarray, though their bare symbols +typically do (@pxref{Creating Symbols}). + +The byte compiler uses symbols with position, +records in them the position of each symbol occurrence, and uses those positions in warning and error messages. They shouldn't normally be used otherwise. Doing so can cause unexpected results with basic Emacs functions such as @code{eq} and @code{equal}. @@ -799,22 +802,19 @@ just the bare symbol to be printed by binding the variable operation. The byte compiler does this before writing its output to the compiled Lisp file. -For most purposes, when the flag variable -@code{symbols-with-pos-enabled} is non-@code{nil}, symbols with -positions behave just as their bare symbols would. For example, -@samp{(eq # foo)} has a value @code{t} when the -variable is set; likewise, @code{equal} will treat a symbol with -position argument as its bare symbol. +When the flag variable @code{symbols-with-pos-enabled} is non-@code{nil}, +a symbol with position ordinarily behaves like its bare symbol. +For example, @samp{(eq (position-symbol 'foo 12345) 'foo)} yields @code{t}, +and @code{equal} likewise treats a symbol with position as its bare symbol. -When @code{symbols-with-pos-enabled} is @code{nil}, any symbols with -position continue to exist, but do not behave as symbols, or have the -other useful properties outlined in the previous paragraph. @code{eq} -returns @code{t} when given identical arguments, and @code{equal} -returns @code{t} when given arguments with @code{equal} components. +When @code{symbols-with-pos-enabled} is @code{nil}, symbols with +position behave as themselves, not as symbols. For example, @samp{(eq +(position-symbol 'foo 12345) 'foo)} yields @code{nil}, and @code{equal} +likewise treats a symbol with position as not equal to its bare symbol. Most of the time in Emacs @code{symbols-with-pos-enabled} is @code{nil}, but the byte compiler and the native compiler bind it to -@code{t} when they run. +@code{t} when they run and Emacs runs a little more slowly in this case. Typically, symbols with position are created by the byte compiler calling the reader function @code{read-positioning-symbols} @@ -822,36 +822,44 @@ calling the reader function @code{read-positioning-symbols} @code{position-symbol}. @defvar symbols-with-pos-enabled -When this variable is non-@code{nil}, a symbol with position behaves -like the contained bare symbol. Emacs runs a little more slowly in -this case. +This variable affects the behavior of symbols with position when they +are not being printed and are not arguments to one of the functions +defined later in this section. When this variable is non-@code{nil}, +such a symbol with position behaves like its bare symbol; otherwise it +behaves as itself, not as a symbol. @end defvar @defvar print-symbols-bare When bound to non-@code{nil}, the Lisp printer prints only the bare symbol of a symbol with position, ignoring the position. +Otherwise a symbol with position prints as itself, not as a symbol. @end defvar -@defun symbol-with-pos-p symbol -This function returns @code{t} if @var{symbol} is a symbol with +@defun symbol-with-pos-p object +This function returns @code{t} if @var{object} is a symbol with position, @code{nil} otherwise. +Unlike @code{symbolp}, this function ignores @code{symbols-with-pos-enabled}. @end defun -@defun bare-symbol symbol -This function returns the bare symbol contained in @var{symbol}, or -@var{symbol} itself if it is already a bare symbol. For any other -type of object, it signals an error. +@defun bare-symbol sym +This function returns the bare symbol of the symbol with +position @var{sym}, or @var{sym} itself if it is already a symbol. +For any other type of object, it signals an error. +This function ignores @code{symbols-with-pos-enabled}. @end defun -@defun symbol-with-pos-pos symbol -This function returns the position, a number, from a symbol with -position. For any other type of object, it signals an error. +@defun symbol-with-pos-pos sympos +This function returns the position, a nonnegative fixnum, from the symbol with +position @var{sympos}. For any other type of object, it signals an error. +This function ignores @code{symbols-with-pos-enabled}. @end defun @defun position-symbol sym pos -Make a new symbol with position. @var{sym} is either a bare symbol or -a symbol with position, and supplies the symbol part of the new -object. @var{pos} is either an integer which becomes the number part -of the new object, or a symbol with position whose position is used. +Make a new symbol with position. The new object's bare symbol is taken +from @var{sym}, which is either a symbol, or a symbol with position +whose bare symbol is used. The new object's position is taken from +@var{pos}, which is either a nonnegative fixnum, or a symbol with +position whose position is used. Emacs signals an error if either argument is invalid. +This function ignores @code{symbols-with-pos-enabled}. @end defun diff --git a/src/data.c b/src/data.c index df08eaf8102..35f4c82c68f 100644 --- a/src/data.c +++ b/src/data.c @@ -339,7 +339,8 @@ DEFUN ("bare-symbol-p", Fbare_symbol_p, Sbare_symbol_p, 1, 1, 0, } DEFUN ("symbol-with-pos-p", Fsymbol_with_pos_p, Ssymbol_with_pos_p, 1, 1, 0, - doc: /* Return t if OBJECT is a symbol together with position. */ + doc: /* Return t if OBJECT is a symbol together with position. +Ignore `symbols-with-pos-enabled'. */ attributes: const) (Lisp_Object object) { @@ -789,25 +790,32 @@ Doing that might make Emacs dysfunctional, and might even crash Emacs. */) } DEFUN ("bare-symbol", Fbare_symbol, Sbare_symbol, 1, 1, 0, - doc: /* Extract, if need be, the bare symbol from SYM, a symbol. */) + doc: /* Extract, if need be, the bare symbol from SYM. +SYM is either a symbol or a symbol with position. +Ignore `symbols-with-pos-enabled'. */) (register Lisp_Object sym) { - CHECK_SYMBOL (sym); - return BARE_SYMBOL_P (sym) ? sym : XSYMBOL_WITH_POS_SYM (sym); + if (BARE_SYMBOL_P (sym)) + return sym; + if (SYMBOL_WITH_POS_P (sym)) + return XSYMBOL_WITH_POS_SYM (sym); + xsignal2 (Qwrong_type_argument, list2 (Qsymbolp, Qsymbol_with_pos_p), sym); } DEFUN ("symbol-with-pos-pos", Fsymbol_with_pos_pos, Ssymbol_with_pos_pos, 1, 1, 0, - doc: /* Extract the position from a symbol with position. */) - (register Lisp_Object ls) + doc: /* Extract the position from the symbol with position SYMPOS. +Ignore `symbols-with-pos-enabled'. */) + (register Lisp_Object sympos) { - CHECK_TYPE (SYMBOL_WITH_POS_P (ls), Qsymbol_with_pos_p, ls); - return XSYMBOL_WITH_POS_POS (ls); + CHECK_TYPE (SYMBOL_WITH_POS_P (sympos), Qsymbol_with_pos_p, sympos); + return XSYMBOL_WITH_POS_POS (sympos); } DEFUN ("remove-pos-from-symbol", Fremove_pos_from_symbol, Sremove_pos_from_symbol, 1, 1, 0, doc: /* If ARG is a symbol with position, return it without the position. -Otherwise, return ARG unchanged. Compare with `bare-symbol'. */) +Otherwise, return ARG unchanged. Ignore `symbols-with-pos-enabled'. +Compare with `bare-symbol'. */) (register Lisp_Object arg) { if (SYMBOL_WITH_POS_P (arg)) @@ -816,10 +824,11 @@ Otherwise, return ARG unchanged. Compare with `bare-symbol'. */) } DEFUN ("position-symbol", Fposition_symbol, Sposition_symbol, 2, 2, 0, - doc: /* Create a new symbol with position. + doc: /* Make a new symbol with position. SYM is a symbol, with or without position, the symbol to position. -POS, the position, is either a fixnum or a symbol with position from which -the position will be taken. */) +POS, the position, is either a nonnegative fixnum, +or a symbol with position from which the position will be taken. +Ignore `symbols-with-pos-enabled'. */) (register Lisp_Object sym, register Lisp_Object pos) { Lisp_Object bare = Fbare_symbol (sym); @@ -4374,7 +4383,7 @@ This variable cannot be set; trying to do so will signal an error. */); DEFSYM (Qsymbols_with_pos_enabled, "symbols-with-pos-enabled"); DEFVAR_BOOL ("symbols-with-pos-enabled", symbols_with_pos_enabled, - doc: /* Non-nil when "symbols with position" can be used as symbols. + doc: /* If non-nil, a symbol with position ordinarily behaves as its bare symbol. Bind this to non-nil in applications such as the byte compiler. */); symbols_with_pos_enabled = false; diff --git a/test/src/data-tests.el b/test/src/data-tests.el index 8af7e902109..ad3b2071254 100644 --- a/test/src/data-tests.el +++ b/test/src/data-tests.el @@ -833,4 +833,9 @@ comparing the subr with a much slower Lisp implementation." (should-error (defalias 'data-tests--da-c 'data-tests--da-d) :type 'cyclic-function-indirection)) +(ert-deftest data-tests-bare-symbol () + (dolist (symbols-with-pos-enabled '(nil t)) + (dolist (sym (list nil t 'xyzzy (make-symbol ""))) + (should (eq sym (bare-symbol (position-symbol sym 0))))))) + ;;; data-tests.el ends here