diff --git a/meson.build b/meson.build index ccb36b55..b35d4fec 100644 --- a/meson.build +++ b/meson.build @@ -85,6 +85,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/app/android_app_Activity.c', 'src/api-impl-jni/app/android_app_AlertDialog.c', 'src/api-impl-jni/app/android_app_Dialog.c', + 'src/api-impl-jni/app/android_app_NotificationManager.c', 'src/api-impl-jni/audio/android_media_AudioTrack.c', 'src/api-impl-jni/audio/android_media_SoundPool.c', 'src/api-impl-jni/content/android_content_ClipboardManager.c', diff --git a/src/api-impl-jni/app/android_app_NotificationManager.c b/src/api-impl-jni/app/android_app_NotificationManager.c new file mode 100644 index 00000000..62f4ac47 --- /dev/null +++ b/src/api-impl-jni/app/android_app_NotificationManager.c @@ -0,0 +1,98 @@ +#include + +#include "../defines.h" +#include "../util.h" + +#include "../generated_headers/android_app_NotificationManager.h" + +static XdpPortal *portal = NULL; + +JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder(JNIEnv *env, jobject this) +{ + return _INTPTR(g_variant_builder_new(G_VARIANT_TYPE("aa{sv}"))); +} + +static GVariant *serialize_intent(JNIEnv *env, jint type, jstring action_jstr, jstring className_jstr) +{ + const char *action = action_jstr ? (*env)->GetStringUTFChars(env, action_jstr, NULL) : NULL; + const char *className = className_jstr ? (*env)->GetStringUTFChars(env, className_jstr, NULL) : NULL; + GVariant *intent = g_variant_new("(iss)", type, action ?: "", className ?: ""); + if (action_jstr) (*env)->ReleaseStringUTFChars(env, action_jstr, action); + if (className_jstr) (*env)->ReleaseStringUTFChars(env, className_jstr, className); + return intent; +} + +JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction(JNIEnv *env, jobject this, jlong builder_ptr, jstring name_jstr, jint type, jstring action, jstring className) +{ + GVariantBuilder *builder = _PTR(builder_ptr); + g_variant_builder_open(builder, G_VARIANT_TYPE("a{sv}")); + if (name_jstr) { + const char *name = (*env)->GetStringUTFChars(env, name_jstr, NULL); + g_variant_builder_add(builder, "{sv}", "label", g_variant_new_string(name)); + (*env)->ReleaseStringUTFChars(env, name_jstr, name); + } + g_variant_builder_add(builder, "{sv}", "action", g_variant_new_string("button-action")); + g_variant_builder_add(builder, "{sv}", "target", serialize_intent(env, type, action, className)); + g_variant_builder_close(builder); +} + +static void notification_action_invoked(XdpPortal *portal, gchar *id_str, gchar *action, GVariant *parameter, gpointer user_data) +{ + int id = atoi(id_str); + int type; + const char *actionName; + const char *className; + GVariant *target; + JNIEnv *env = get_jni_env(); + + GVariantIter *iter = g_variant_iter_new(parameter); + g_variant_iter_next(iter, "v", &target); + g_variant_get(target, "(iss)", &type, &actionName, &className); + jmethodID notificationActionCallback = _STATIC_METHOD((*env)->FindClass(env, "android/app/NotificationManager"), "notificationActionCallback", "(IILjava/lang/String;Ljava/lang/String;)V"); + (*env)->CallStaticVoidMethod(env, (*env)->FindClass(env, "android/app/NotificationManager"), notificationActionCallback, id, type, _JSTRING(actionName), _JSTRING(className)); + g_variant_iter_free(iter); + g_variant_unref(target); +} + +// gnome session locks up when we send notification update before last update was processed +static int callback_pending = 0; +static void natification_callback(GObject* source_object, GAsyncResult* res, gpointer data) +{ + callback_pending = 0; +} + +JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotification(JNIEnv *env, jobject this, jlong builder_ptr, jint id, jstring title_jstr, jstring text_jstr, jint type, jstring action, jstring className) +{ + if (callback_pending) { + return; + } + if (!portal) { + portal = xdp_portal_new(); + g_signal_connect(portal, "notification-action-invoked", G_CALLBACK(notification_action_invoked), NULL); + } + + GVariantBuilder *builder = _PTR(builder_ptr); + GVariant *buttons = g_variant_builder_end(builder); + + g_variant_builder_init(builder, G_VARIANT_TYPE("a{sv}")); + if (title_jstr) { + const char *title = (*env)->GetStringUTFChars(env, title_jstr, NULL); + g_variant_builder_add(builder, "{sv}", "title", g_variant_new_string(title)); + (*env)->ReleaseStringUTFChars(env, title_jstr, title); + } + if (text_jstr) { + const char *text = (*env)->GetStringUTFChars(env, text_jstr, NULL); + g_variant_builder_add(builder, "{sv}", "body", g_variant_new_string(text)); + (*env)->ReleaseStringUTFChars(env, text_jstr, text); + } + g_variant_builder_add(builder, "{sv}", "default-action", g_variant_new_string("default-action")); + g_variant_builder_add(builder, "{sv}", "default-action-target", serialize_intent(env, type, action, className)); + g_variant_builder_add(builder, "{sv}", "buttons", buttons); + GVariant *variant = g_variant_builder_end(builder); + g_variant_builder_unref(builder); + char *id_string = g_strdup_printf("%d", id); + xdp_portal_remove_notification(portal, id_string); + callback_pending = 1; + xdp_portal_add_notification(portal, id_string, variant, XDP_NOTIFICATION_FLAG_NONE, NULL, natification_callback, NULL); + g_free(id_string); +} diff --git a/src/api-impl-jni/generated_headers/android_app_NotificationManager.h b/src/api-impl-jni/generated_headers/android_app_NotificationManager.h new file mode 100644 index 00000000..565237cd --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_app_NotificationManager.h @@ -0,0 +1,37 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_app_NotificationManager */ + +#ifndef _Included_android_app_NotificationManager +#define _Included_android_app_NotificationManager +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: android_app_NotificationManager + * Method: nativeInitBuilder + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_android_app_NotificationManager_nativeInitBuilder + (JNIEnv *, jobject); + +/* + * Class: android_app_NotificationManager + * Method: nativeAddAction + * Signature: (JLjava/lang/String;ILjava/lang/String;Ljava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeAddAction + (JNIEnv *, jobject, jlong, jstring, jint, jstring, jstring); + +/* + * Class: android_app_NotificationManager + * Method: nativeShowNotification + * Signature: (JILjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_android_app_NotificationManager_nativeShowNotification + (JNIEnv *, jobject, jlong, jint, jstring, jstring, jint, jstring, jstring); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl/android/app/Notification.java b/src/api-impl/android/app/Notification.java index 3b83630b..96fc7cd9 100644 --- a/src/api-impl/android/app/Notification.java +++ b/src/api-impl/android/app/Notification.java @@ -1,5 +1,8 @@ package android.app; +import java.util.ArrayList; +import java.util.List; + import android.content.Context; import android.graphics.Bitmap; import android.media.AudioAttributes; @@ -44,8 +47,21 @@ public class Notification { public Bundle extras; + String text; + String title; + List actions = new ArrayList(); + PendingIntent intent; + + public String toString() { + return "Notification [" + title + ", " + text + ", " + actions + "]"; + } + public static class Builder { - public Builder(Context context) {} + private Notification notification; + + public Builder(Context context) { + notification = new Notification(); + } public Builder setWhen(long when) {return this;} @@ -67,13 +83,22 @@ public class Notification { public Builder setDefaults(int defaults) {return this;} - public Builder setContentTitle(CharSequence title) {return this;} + public Builder setContentTitle(CharSequence title) { + notification.title = title != null ? title.toString() : null; + return this; + } - public Builder setContentText(CharSequence text) {return this;} + public Builder setContentText(CharSequence text) { + notification.text = text != null ? text.toString() : null; + return this; + } public Builder setContentInfo(CharSequence info) {return this;} - public Builder setContentIntent(PendingIntent intent) {return this;} + public Builder setContentIntent(PendingIntent intent) { + notification.intent = intent; + return this; + } public Builder setDeleteIntent(PendingIntent intent) {return this;} @@ -111,26 +136,44 @@ public class Notification { public Builder setSound(Uri sound, AudioAttributes audioAttributes) {return this;} - public Builder addAction(Action action) {return this;} + public Builder addAction(Action action) { + notification.actions.add(action); + return this; + } public Builder setStyle(Style style) {return this;} public Builder setExtras(Bundle extras) {return this;} public Notification build() { - return new Notification(); + return notification; } } public static class Action { + + int icon; + String title; + PendingIntent intent; + + public String toString() { + return "Action [" + icon + ", " + title + ", " + intent + "]"; + } + public static final class Builder { - public Builder(int icon, CharSequence title, PendingIntent intent) {} + private Action action; + public Builder(int icon, CharSequence title, PendingIntent intent) { + action = new Action(); + action.icon = icon; + action.title = String.valueOf(title); + action.intent = intent; + } public Builder addExtras(Bundle extras) {return this;} public Action build() { - return new Action(); + return action; } } } diff --git a/src/api-impl/android/app/NotificationManager.java b/src/api-impl/android/app/NotificationManager.java index 6313b4d8..c4b76e38 100644 --- a/src/api-impl/android/app/NotificationManager.java +++ b/src/api-impl/android/app/NotificationManager.java @@ -1,13 +1,59 @@ package android.app; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; + public class NotificationManager { public void cancelAll() {} public void notify(String tag, int id, Notification notification) { System.out.println("notify(" + tag + ", " + id + ", " + notification + ") called"); + long builder = nativeInitBuilder(); + for (Notification.Action action : notification.actions) { + int intentType = -1; + String actionName = null; + String className = null; + if (action.intent != null) { + intentType = action.intent.type; + actionName = action.intent.intent.getAction(); + className = action.intent.intent.getComponent() != null ? action.intent.intent.getComponent().getClassName() : null; + } + nativeAddAction(builder, action.title, intentType, actionName, className); + } + int intentType = -1; + String actionName = null; + String className = null; + if (notification.intent != null) { + intentType = notification.intent.type; + actionName = notification.intent.intent.getAction(); + className = notification.intent.intent.getComponent() != null ? notification.intent.intent.getComponent().getClassName() : null; + } + nativeShowNotification(builder, id, notification.title, notification.text, intentType, actionName, className); } public void notify(int id, Notification notification) { - System.out.println("notify(" + id + ", " + notification + ") called"); + notify(null, id, notification); } + + public void cancel(String tag, int id) {} + + protected static void notificationActionCallback(int id, int intentType, String action, String className) { + Context context = Context.this_application; + Intent intent = new Intent(action); + if (className != null && !className.isEmpty()) { + intent.setComponent(new ComponentName(context, className)); + } + if (intentType == 0) { // type Activity + context.startActivity(intent); + } else if (intentType == 1) { // type Service + context.startService(intent); + } else if (intentType == 2) { // type Broadcast + context.sendBroadcast(intent); + } + } + + protected native long nativeInitBuilder(); + protected native void nativeAddAction(long builder, String title, int intentType, String action, String className); + protected native void nativeShowNotification(long builder, int id, String title, String text, int intentType, String action, String className); } diff --git a/src/api-impl/android/app/PendingIntent.java b/src/api-impl/android/app/PendingIntent.java index 9e914eb0..bc2f4755 100644 --- a/src/api-impl/android/app/PendingIntent.java +++ b/src/api-impl/android/app/PendingIntent.java @@ -5,8 +5,18 @@ import android.content.Intent; import android.content.IntentSender; public class PendingIntent { + + private int requestCode; + Intent intent; + int type; // 0: activity, 1: service, 2: broadcast + + private PendingIntent(int requestCode, Intent intent, int type) { + this.requestCode = requestCode; + this.intent = intent; + this.type = type; + } public static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags) { - return new PendingIntent(); + return new PendingIntent(requestCode, intent, 2); } public IntentSender getIntentSender() { @@ -16,11 +26,16 @@ public class PendingIntent { public void send(Context context, int code, Intent intent) {} public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags) { - return new PendingIntent(); + return new PendingIntent(requestCode, intent, 0); } public static PendingIntent getService(Context context, int requestCode, Intent intent, int flags) { - return new PendingIntent(); + return new PendingIntent(requestCode, intent, 1); + } + + public String toString() { + return "PendingIntent [requestCode=" + requestCode + ", intent=" + intent + ", type=" + + new String[] { "activity", "service", "broadcast" }[type] + "]"; } public class CanceledException extends Exception { diff --git a/src/api-impl/android/content/BroadcastReceiver.java b/src/api-impl/android/content/BroadcastReceiver.java index 1a041bc9..2da51380 100644 --- a/src/api-impl/android/content/BroadcastReceiver.java +++ b/src/api-impl/android/content/BroadcastReceiver.java @@ -1,4 +1,6 @@ package android.content; -public class BroadcastReceiver { +public abstract class BroadcastReceiver { + + public abstract void onReceive(Context context, Intent intent); } diff --git a/src/api-impl/android/content/Context.java b/src/api-impl/android/content/Context.java index ea608693..e08e5aa9 100644 --- a/src/api-impl/android/content/Context.java +++ b/src/api-impl/android/content/Context.java @@ -82,6 +82,8 @@ public class Context extends Object { File obb_dir = null; File cache_dir = null; + private static Map receiverMap = new HashMap(); + static { assets = new AssetManager(); dm = new DisplayMetrics(); @@ -202,6 +204,7 @@ public class Context extends Object { } public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + receiverMap.put(filter, receiver); return new Intent(); } @@ -467,7 +470,13 @@ public class Context extends Object { return new File(databaseDir, dbName); } - public void sendBroadcast(Intent intent) {} + public void sendBroadcast(Intent intent) { + for (IntentFilter filter : receiverMap.keySet()) { + if (filter.matchAction(intent.getAction())) { + receiverMap.get(filter).onReceive(this, intent); + } + } + } public boolean stopService(Intent intent) {return false;} diff --git a/src/api-impl/android/content/IntentFilter.java b/src/api-impl/android/content/IntentFilter.java index 607504b7..c1205d8c 100644 --- a/src/api-impl/android/content/IntentFilter.java +++ b/src/api-impl/android/content/IntentFilter.java @@ -1,9 +1,25 @@ package android.content; -public class IntentFilter { - public IntentFilter() {} - public IntentFilter(String dummy) {} +import java.util.HashSet; +import java.util.Set; - public void addAction(String action) {} - public int countActions() { return 0; /*maybe?*/ } +public class IntentFilter { + + private Set actions = new HashSet<>(); + + public IntentFilter() {} + public IntentFilter(String action) { + addAction(action); + } + + public void addAction(String action) { + actions.add(action); + } + public int countActions() { + return actions.size(); + } + + public final boolean matchAction(String action) { + return actions.contains(action); + } }