From f7a29d8731bc3b8a8ac902630b51d4448f0f7e5f Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Mon, 28 Jul 2025 23:01:58 +0200 Subject: [PATCH] implement Google Cloud Messaging using DBus activatable GActions This needs https://gitlab.com/android_translation_layer/gcm_service running in the background. For D-Bus activation, a D-Bus service file needs to be manually installed under ~/.local/share/dbus-1/services. Tested with FCM-Toolbox app. --- .../content/android_content_Context.c | 15 ++++++ .../android_content_Context.h | 8 +++ src/api-impl-jni/handle_cache.c | 9 ++++ src/api-impl-jni/handle_cache.h | 11 +++++ src/api-impl-jni/util.c | 49 ++++++++++++++++++- src/api-impl-jni/util.h | 2 +- src/api-impl/android/app/Service.java | 5 ++ .../android/content/BroadcastReceiver.java | 15 ++++++ src/api-impl/android/content/Context.java | 40 ++++++++++++++- .../android/content/pm/PackageParser.java | 3 ++ 10 files changed, 152 insertions(+), 5 deletions(-) diff --git a/src/api-impl-jni/content/android_content_Context.c b/src/api-impl-jni/content/android_content_Context.c index bb95cd82..6c6e91af 100644 --- a/src/api-impl-jni/content/android_content_Context.c +++ b/src/api-impl-jni/content/android_content_Context.c @@ -175,3 +175,18 @@ JNIEXPORT void JNICALL Java_android_content_Context_nativeRegisterUnifiedPush(JN (*env)->ReleaseStringUTFChars(env, token_jstr, token); (*env)->ReleaseStringUTFChars(env, application_jstr, application); } + +JNIEXPORT void JNICALL Java_android_content_Context_nativeStartExternalService(JNIEnv *env, jclass this, jstring package_jstr, jobject intent) +{ + GVariant *variant = intent_serialize(env, intent); + const char *package = (*env)->GetStringUTFChars(env, package_jstr, NULL); + char *object_path = g_strdup_printf("/%s", package); + g_strdelimit(object_path, ".", '/'); + GDBusConnection *connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); + GActionGroup *action_group = G_ACTION_GROUP(g_dbus_action_group_get(connection, package, object_path)); + g_action_group_activate_action(action_group, "startService", variant); + g_object_unref(action_group); + g_object_unref(connection); + g_free(object_path); + (*env)->ReleaseStringUTFChars(env, package_jstr, package); +} diff --git a/src/api-impl-jni/generated_headers/android_content_Context.h b/src/api-impl-jni/generated_headers/android_content_Context.h index bef33831..f63fd855 100644 --- a/src/api-impl-jni/generated_headers/android_content_Context.h +++ b/src/api-impl-jni/generated_headers/android_content_Context.h @@ -49,6 +49,14 @@ JNIEXPORT void JNICALL Java_android_content_Context_nativeExportUnifiedPush JNIEXPORT void JNICALL Java_android_content_Context_nativeRegisterUnifiedPush (JNIEnv *, jclass, jstring, jstring); +/* + * Class: android_content_Context + * Method: nativeStartExternalService + * Signature: (Ljava/lang/String;Landroid/content/Intent;)V + */ +JNIEXPORT void JNICALL Java_android_content_Context_nativeStartExternalService + (JNIEnv *, jclass, jstring, jobject); + #ifdef __cplusplus } #endif diff --git a/src/api-impl-jni/handle_cache.c b/src/api-impl-jni/handle_cache.c index 7235fee7..0541f220 100644 --- a/src/api-impl-jni/handle_cache.c +++ b/src/api-impl-jni/handle_cache.c @@ -122,6 +122,8 @@ void set_up_handle_cache(JNIEnv *env) handle_cache.intent.constructor = _METHOD(handle_cache.intent.class, "", "()V"); handle_cache.intent.putExtraCharSequence = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;Ljava/lang/CharSequence;)Landroid/content/Intent;"); handle_cache.intent.putExtraByteArray = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;[B)Landroid/content/Intent;"); + handle_cache.intent.putExtraInt = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;I)Landroid/content/Intent;"); + handle_cache.intent.putExtraLong = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;J)Landroid/content/Intent;"); handle_cache.intent.getDataString = _METHOD(handle_cache.intent.class, "getDataString", "()Ljava/lang/String;"); handle_cache.intent.setClassName = _METHOD(handle_cache.intent.class, "setClassName", "(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;"); @@ -136,4 +138,11 @@ void set_up_handle_cache(JNIEnv *env) handle_cache.uri.class = _REF((*env)->FindClass(env, "android/net/Uri")); handle_cache.uri.parse = _STATIC_METHOD(handle_cache.uri.class, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"); + + handle_cache.bundle.class = _REF((*env)->FindClass(env, "android/os/Bundle")); + handle_cache.bundle.get = _METHOD(handle_cache.bundle.class, "get", "(Ljava/lang/String;)Ljava/lang/Object;"); + handle_cache.bundle.keySet = _METHOD(handle_cache.bundle.class, "keySet", "()Ljava/util/Set;"); + + handle_cache.set.class = _REF((*env)->FindClass(env, "java/util/Set")); + handle_cache.set.toArray = _METHOD(handle_cache.set.class, "toArray", "()[Ljava/lang/Object;"); } diff --git a/src/api-impl-jni/handle_cache.h b/src/api-impl-jni/handle_cache.h index 1001b926..4d0d3a32 100644 --- a/src/api-impl-jni/handle_cache.h +++ b/src/api-impl-jni/handle_cache.h @@ -125,6 +125,8 @@ struct handle_cache { jmethodID constructor; jmethodID putExtraCharSequence; jmethodID putExtraByteArray; + jmethodID putExtraInt; + jmethodID putExtraLong; jmethodID getDataString; jmethodID setClassName; } intent; @@ -146,6 +148,15 @@ struct handle_cache { jclass class; jmethodID parse; } uri; + struct { + jclass class; + jmethodID keySet; + jmethodID get; + } bundle; + struct { + jclass class; + jmethodID toArray; + } set; }; extern struct handle_cache handle_cache; diff --git a/src/api-impl-jni/util.c b/src/api-impl-jni/util.c index 6d7da275..85d8db03 100644 --- a/src/api-impl-jni/util.c +++ b/src/api-impl-jni/util.c @@ -249,11 +249,37 @@ GVariant *intent_serialize(JNIEnv *env, jobject intent) { jobject component = _GET_OBJ_FIELD(intent, "component", "Landroid/content/ComponentName;"); jstring className_jstr = component ? _GET_OBJ_FIELD(component, "mClass", "Ljava/lang/String;") : NULL; jstring data_jstr = (*env)->CallObjectMethod(env, intent, handle_cache.intent.getDataString); + jstring sender_package_jstr = (*env)->CallObjectMethod(env, _GET_STATIC_OBJ_FIELD(handle_cache.context.class, "this_application", "Landroid/app/Application;"), handle_cache.context.get_package_name); + + GVariantBuilder extras_builder; + g_variant_builder_init(&extras_builder, G_VARIANT_TYPE_VARDICT); + jobject extras = _GET_OBJ_FIELD(intent, "extras", "Landroid/os/Bundle;"); + jobject extras_key_set = (*env)->CallObjectMethod(env, extras, handle_cache.bundle.keySet); + jobjectArray extras_keys = (*env)->CallObjectMethod(env, extras_key_set, handle_cache.set.toArray); + jsize extras_keys_length = (*env)->GetArrayLength(env, extras_keys); + for (jint i = 0; i < extras_keys_length; i++) { + jstring key_jstr = (*env)->GetObjectArrayElement(env, extras_keys, i); + jobject value_jobj = (*env)->CallObjectMethod(env, extras, handle_cache.bundle.get, key_jstr); + if (!(*env)->IsSameObject(env, _CLASS(key_jstr), _CLASS(value_jobj))) { + printf("skipping non-string extra: %s\n", (*env)->GetStringUTFChars(env, key_jstr, NULL)); + continue; + } + const char *key = (*env)->GetStringUTFChars(env, key_jstr, NULL); + const char *value = (*env)->GetStringUTFChars(env, value_jobj, NULL); + g_variant_builder_add(&extras_builder, "{sv}", key, g_variant_new_string(value)); + (*env)->ReleaseStringUTFChars(env, key_jstr, key); + (*env)->ReleaseStringUTFChars(env, value_jobj, value); + (*env)->DeleteLocalRef(env, key_jstr); + (*env)->DeleteLocalRef(env, value_jobj); + } const char *action = action_jstr ? (*env)->GetStringUTFChars(env, action_jstr, NULL) : NULL; const char *className = className_jstr ? (*env)->GetStringUTFChars(env, className_jstr, NULL) : NULL; const char *data = data_jstr ? (*env)->GetStringUTFChars(env, data_jstr, NULL) : NULL; - GVariant *variant = g_variant_new(INTENT_G_VARIANT_TYPE_STRING, action ?: "", className ?: "", data ?: ""); + const char *sender_package = sender_package_jstr ? (*env)->GetStringUTFChars(env, sender_package_jstr, NULL) : NULL; + GVariant *variant = g_variant_new(INTENT_G_VARIANT_TYPE_STRING, action ?: "", className ?: "", data ?: "", &extras_builder, sender_package); + if (sender_package_jstr) + (*env)->ReleaseStringUTFChars(env, sender_package_jstr, sender_package); if (action_jstr) (*env)->ReleaseStringUTFChars(env, action_jstr, action); if (className_jstr) @@ -267,7 +293,8 @@ jobject intent_deserialize(JNIEnv *env, GVariant *variant) { const char *action; const char *className; const char *data; - g_variant_get(variant, INTENT_G_VARIANT_TYPE_STRING, &action, &className, &data); + GVariantIter *extras; + g_variant_get(variant, INTENT_G_VARIANT_TYPE_STRING, &action, &className, &data, &extras, NULL); if (action && action[0] == '\0') action = NULL; if (className && className[0] == '\0') @@ -281,6 +308,24 @@ jobject intent_deserialize(JNIEnv *env, GVariant *variant) { (*env)->CallObjectMethod(env, intent, handle_cache.intent.setClassName, _GET_STATIC_OBJ_FIELD(handle_cache.context.class, "this_application", "Landroid/app/Application;"), _JSTRING(className)); if (data) _SET_OBJ_FIELD(intent, "data", "Landroid/net/Uri;", (*env)->CallStaticObjectMethod(env, handle_cache.uri.class, handle_cache.uri.parse, _JSTRING(data))); + const char *key; + GVariant *value; + while (g_variant_iter_loop(extras, "{sv}", &key, &value)) { + if (g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) { + (*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraCharSequence, _JSTRING(key), _JSTRING(g_variant_get_string(value, NULL))); + } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_INT32)) { + (*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraInt, _JSTRING(key), g_variant_get_int32(value)); + } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_INT64)) { + (*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraLong, _JSTRING(key), g_variant_get_int64(value)); + } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING)) { + gsize size; + const int8_t *message = g_variant_get_fixed_array(value, &size, 1); + jbyteArray bytesMessage = (*env)->NewByteArray(env, size); + (*env)->SetByteArrayRegion(env, bytesMessage, 0, size, message); + (*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraByteArray, _JSTRING(key), bytesMessage); + } + } + g_variant_iter_free(extras); return intent; } diff --git a/src/api-impl-jni/util.h b/src/api-impl-jni/util.h index fe4c69b0..3c72e97b 100644 --- a/src/api-impl-jni/util.h +++ b/src/api-impl-jni/util.h @@ -57,7 +57,7 @@ void atl_safe_gtk_widget_set_visible(GtkWidget *widget, gboolean visible); void atl_safe_gtk_widget_queue_allocate(GtkWidget *widget); void atl_safe_gtk_widget_queue_resize(GtkWidget *widget); -#define INTENT_G_VARIANT_TYPE_STRING "(sss)" // (action, className, data) +#define INTENT_G_VARIANT_TYPE_STRING "(sssa{sv}s)" // (action, className, data, extras, sender_package) GVariant *intent_serialize(JNIEnv *env, jobject intent); jobject intent_deserialize(JNIEnv *env, GVariant *variant); const char *intent_actionname_from_type(int type); diff --git a/src/api-impl/android/app/Service.java b/src/api-impl/android/app/Service.java index c0581611..4b1bd863 100644 --- a/src/api-impl/android/app/Service.java +++ b/src/api-impl/android/app/Service.java @@ -60,6 +60,11 @@ public abstract class Service extends ContextWrapper { System.out.println("Service.stopSelf() called"); } + public boolean stopSelfResult(int startId) { + System.out.println("Service.stopSelfResult(" + startId + ") called"); + return true; + } + public void attachBaseContext(Context newBase) { System.out.println("Service.attachBaseContext(" + newBase + ") called"); } diff --git a/src/api-impl/android/content/BroadcastReceiver.java b/src/api-impl/android/content/BroadcastReceiver.java index 2da51380..323deb3e 100644 --- a/src/api-impl/android/content/BroadcastReceiver.java +++ b/src/api-impl/android/content/BroadcastReceiver.java @@ -2,5 +2,20 @@ package android.content; public abstract class BroadcastReceiver { + public static class PendingResult { + + public void setResultCode(int resultCode) {} + + public void finish() {} + } + public abstract void onReceive(Context context, Intent intent); + + public boolean isOrderedBroadcast() { + return true; + } + + public PendingResult goAsync() { + return new PendingResult(); + } } diff --git a/src/api-impl/android/content/Context.java b/src/api-impl/android/content/Context.java index d324a740..6160662b 100644 --- a/src/api-impl/android/content/Context.java +++ b/src/api-impl/android/content/Context.java @@ -39,8 +39,11 @@ import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Looper; +import android.os.Message; +import android.os.Messenger; import android.os.ParcelFileDescriptor; import android.os.PowerManager; +import android.os.RemoteException; import android.os.UserManager; import android.os.Vibrator; import android.telephony.TelephonyManager; @@ -151,6 +154,7 @@ public class Context extends Object { private static native void nativeOpenFile(int fd); private static native void nativeExportUnifiedPush(String packageName); private static native void nativeRegisterUnifiedPush(String token, String application); + private static native void nativeStartExternalService(String packageName, Intent service); static Application createApplication(long native_window) throws Exception { Application application; @@ -467,15 +471,47 @@ public class Context extends Object { public ComponentName startService(Intent intent) { ComponentName component = intent.getComponent(); if (component == null) { - Slog.w(TAG, "startService: component is null for intent: " + intent); + int priority = Integer.MIN_VALUE; + for (PackageParser.Service service: pkg.services) { + for (PackageParser.IntentInfo intentInfo: service.intents) { + if (intentInfo.matchAction(intent.getAction()) && intentInfo.priority > priority) { + component = new ComponentName(pkg.packageName, service.className); + priority = intentInfo.priority; + break; + } + } + } + } + if (intent.getAction() != null && intent.getAction().startsWith("com.google.android.c2dm")) { + nativeStartExternalService("com.google.android.c2dm", intent); + // Newer applications use a Messenger instead of a BroadcastReceiver for the return Intent. + // To support new and old apps with a common interface, we wrap the Messenger in a BroadcastReceiver + final Messenger messenger = (Messenger)intent.getParcelableExtra("google.messenger"); + if (messenger != null) { + receiverMap.put(new IntentFilter("com.google.android.c2dm.intent.REGISTRATION"), new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent resultIntent) { + try { + messenger.send(Message.obtain(null, 0, resultIntent)); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + }); + } return null; } + if (component == null) { + Slog.w(TAG, "startService: no matching service found for intent: " + intent); + return null; + } + final String className = component.getClassName(); new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { try { - Class cls = Class.forName(component.getClassName()).asSubclass(Service.class); + Class cls = Class.forName(className).asSubclass(Service.class); if (!runningServices.containsKey(cls)) { Service service = cls.getConstructor().newInstance(); service.attachBaseContext(new Context()); diff --git a/src/api-impl/android/content/pm/PackageParser.java b/src/api-impl/android/content/pm/PackageParser.java index 142ee546..1cf56b60 100644 --- a/src/api-impl/android/content/pm/PackageParser.java +++ b/src/api-impl/android/content/pm/PackageParser.java @@ -2220,6 +2220,8 @@ public class PackageParser { outInfo.logo = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestIntentFilter_logo, 0); + outInfo.priority = sa.getInt( + com.android.internal.R.styleable.AndroidManifestIntentFilter_priority, 0); sa.recycle(); int outerDepth = parser.getDepth(); int type; @@ -3881,6 +3883,7 @@ public class PackageParser { public int icon; public int logo; public int preferred; + public int priority; } public final static class ActivityIntentInfo extends IntentInfo {