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. <actions, titles>: 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. <QCactions, QCon_action, QCon_cancel> New defsyms. * src/androidterm.c (handle_one_android_event) <ANDROID_NOTIFICATION_DELETED> <ANDROID_NOTIFICATION_ACTION>: 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) <NOTIFICATION_EVENT>: Call callback specified by the event. * src/termhooks.h (enum event_kind) [HAVE_ANDROID]: New enum NOTIFICATION_EVENT.
This commit is contained in:
parent
75cfc6c73f
commit
a7a37341ca
13 changed files with 608 additions and 35 deletions
|
@ -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}.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
161
src/android.c
161
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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -30,6 +30,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
|
|||
#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, "<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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -25,6 +25,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
|
|||
#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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
{
|
||||
|
|
|
@ -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. */
|
||||
|
|
Loading…
Add table
Reference in a new issue