diff --git a/mobile/android/qt6/AndroidManifest.xml b/mobile/android/qt6/AndroidManifest.xml
index 39f90e1954c..cb1a78ee1e2 100644
--- a/mobile/android/qt6/AndroidManifest.xml
+++ b/mobile/android/qt6/AndroidManifest.xml
@@ -72,5 +72,14 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/qtprovider_paths"/>
+
+
+
+
+
+
+
diff --git a/mobile/android/qt6/build.gradle b/mobile/android/qt6/build.gradle
index 1656e3faaf2..0fb63ba364f 100644
--- a/mobile/android/qt6/build.gradle
+++ b/mobile/android/qt6/build.gradle
@@ -6,6 +6,8 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:8.8.0'
+ // Add Firebase Google Services plugin
+ classpath 'com.google.gms:google-services:4.4.0'
}
}
@@ -29,6 +31,10 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
+
+ // Firebase dependencies for Push Notifications
+ implementation platform('com.google.firebase:firebase-bom:33.7.0')
+ implementation 'com.google.firebase:firebase-messaging'
}
android {
@@ -101,3 +107,6 @@ android {
}
}
}
+
+// Apply Google Services plugin (must be at the bottom)
+apply plugin: 'com.google.gms.google-services'
diff --git a/mobile/android/qt6/google-services.json b/mobile/android/qt6/google-services.json
new file mode 100644
index 00000000000..29eb3e43cc9
--- /dev/null
+++ b/mobile/android/qt6/google-services.json
@@ -0,0 +1,155 @@
+{
+ "project_info": {
+ "project_number": "854811651919",
+ "firebase_url": "https://status-react-app.firebaseio.com",
+ "project_id": "status-react-app",
+ "storage_bucket": "status-react-app.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:854811651919:android:b1652e09df58f195005f3a",
+ "android_client_info": {
+ "package_name": "app.status.mobile"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "im.status.ethereum"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:854811651919:android:11ee7444ded8a00a",
+ "android_client_info": {
+ "package_name": "im.status.ethereum"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "im.status.ethereum"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:854811651919:android:15dbe4af1e06ca3e005f3a",
+ "android_client_info": {
+ "package_name": "im.status.ethereum.debug"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "im.status.ethereum"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:854811651919:android:1d0d69fe8c1bb89b005f3a",
+ "android_client_info": {
+ "package_name": "im.status.ethereum.pr"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAOF4W1j8GYeXzzVKRfNKlXywD6bx0rJtQ"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "854811651919-gua52csicclb5p9gr4eeu33ukk0aaphj.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "854811651919-30s20e3l0me0ins0vc4185jbnj7ja49o.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "im.status.ethereum"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/mobile/android/qt6/src/app/status/mobile/PushNotificationHelper.java b/mobile/android/qt6/src/app/status/mobile/PushNotificationHelper.java
new file mode 100644
index 00000000000..916a2d532da
--- /dev/null
+++ b/mobile/android/qt6/src/app/status/mobile/PushNotificationHelper.java
@@ -0,0 +1,293 @@
+package app.status.mobile;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat;
+
+/**
+ * Helper class for managing push notifications
+ *
+ * This class provides:
+ * - JNI bridge between Java and C++/Qt
+ * - Notification display functionality
+ * - Notification channel management
+ * - Deep link handling
+ */
+public class PushNotificationHelper {
+ private static final String TAG = "PushNotificationHelper";
+ private static final String CHANNEL_ID = "status-messages";
+ private static final String CHANNEL_NAME = "Status Messages";
+
+ // Store application context (set by PushNotificationService)
+ private static Context sApplicationContext = null;
+
+ /**
+ * Initialize with application context
+ * Should be called from PushNotificationService.onCreate()
+ */
+ public static void initialize(Context context) {
+ if (context != null) {
+ sApplicationContext = context.getApplicationContext();
+ Log.d(TAG, "PushNotificationHelper initialized with context");
+ }
+ }
+
+ /**
+ * Check if notification permission is granted (Android 13+)
+ * Returns true if permission is granted or not required (Android 12-)
+ */
+ public static boolean hasNotificationPermission(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+
+ int result = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ );
+ boolean hasPermission = result == PackageManager.PERMISSION_GRANTED;
+ return hasPermission;
+ }
+ return true;
+ }
+
+ /**
+ * Request notification permission (Android 13+ only)
+ * For Android 12 and below, this does nothing as permission is not required
+ */
+ public static void requestNotificationPermission(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+
+ if (context instanceof Activity) {
+ Activity activity = (Activity) context;
+ if (!hasNotificationPermission(context)) {
+ Log.d(TAG, "Requesting POST_NOTIFICATIONS permission...");
+ ActivityCompat.requestPermissions(
+ activity,
+ new String[]{Manifest.permission.POST_NOTIFICATIONS},
+ 1001 // Request code
+ );
+ } else {
+ Log.d(TAG, "Notification permission already granted");
+ }
+ } else {
+ Log.w(TAG, "Cannot request permission: context is not an Activity");
+ }
+ } else {
+ Log.d(TAG, "Android 12- : notification permission not required");
+ }
+ }
+
+ /**
+ * Request FCM token at app startup
+ * This method can be called from C++ layer to explicitly request the token
+ * The token is delivered via onFCMTokenReceived callback
+ */
+ public static void requestFCMToken(Context context) {
+ Log.d(TAG, "Requesting FCM token from Firebase...");
+
+ // Store context if not already set
+ if (sApplicationContext == null && context != null) {
+ sApplicationContext = context.getApplicationContext();
+ }
+
+ // Check notification permission first
+ if (!hasNotificationPermission(context)) {
+ Log.w(TAG, "Notification permission not granted. Token request may fail.");
+ }
+
+ // Request token with completion listener
+ // This works whether the token is cached or needs to be fetched
+ com.google.firebase.messaging.FirebaseMessaging.getInstance().getToken()
+ .addOnCompleteListener(task -> {
+ if (task.isSuccessful() && task.getResult() != null) {
+ String token = task.getResult();
+ Log.d(TAG, "FCM token obtained: " + token);
+ // Pass token to native layer
+ onFCMTokenReceived(token);
+ } else {
+ Log.e(TAG, "Failed to get FCM token", task.getException());
+ }
+ });
+ }
+
+ /**
+ * Called from FirebaseMessagingService when new token is received
+ * This method calls into C++/Qt layer via JNI
+ */
+ public static void onFCMTokenReceived(String token) {
+ Log.d(TAG, "FCM Token ready to pass to native layer: " + token);
+
+ // Call native C++ method (implemented in C++ via JNI registration)
+ nativeOnFCMTokenReceived(token);
+ }
+
+ /**
+ * Called from FirebaseMessagingService when push notification received
+ * Passes encrypted data to status-go for processing
+ */
+ public static void onPushNotificationReceived(String encryptedMessage,
+ String chatId,
+ String publicKey) {
+ Log.d(TAG, "Push notification received, passing to native layer");
+
+ // Call native C++ method to forward to status-go
+ nativeOnPushNotificationReceived(encryptedMessage, chatId, publicKey);
+ }
+
+ /**
+ * Called from C++/Qt layer to display a notification
+ * This is called after status-go processes and decrypts the message
+ *
+ * @param title Notification title (chat name or sender name)
+ * @param message Notification message text
+ * @param identifier JSON string containing notification metadata for handling clicks
+ */
+ public static void showNotification(String title, String message, String identifier) {
+ Log.d(TAG, "showNotification called - Title: " + title + ", Message: " + message);
+
+ try {
+ // Get application context from Qt
+ Context context = getApplicationContext();
+ if (context == null) {
+ Log.e(TAG, "Failed to get application context");
+ return;
+ }
+
+ // Create notification channel (required for Android O+)
+ createNotificationChannel(context);
+
+ // Parse identifier to get deep link
+ String deepLink = extractDeepLinkFromIdentifier(identifier);
+
+ // Create intent for when notification is tapped
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(android.net.Uri.parse(deepLink));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context,
+ identifier.hashCode(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
+ );
+
+ // Build notification
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(android.R.drawable.ic_dialog_info) // TODO: Use app icon
+ .setContentTitle(title)
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent);
+
+ // Show notification
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ notificationManager.notify(identifier.hashCode(), builder.build());
+
+ Log.d(TAG, "Notification displayed successfully");
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error showing notification", e);
+ }
+ }
+
+ /**
+ * Clear all notifications for a specific chat
+ * Called when user opens a chat
+ */
+ public static void clearNotifications(String chatId) {
+ Log.d(TAG, "Clearing notifications for chat: " + chatId);
+
+ try {
+ Context context = getApplicationContext();
+ if (context == null) return;
+
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ // For now, just cancel by chat ID hash
+ // In a full implementation, we'd track notification IDs
+ notificationManager.cancel(chatId.hashCode());
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error clearing notifications", e);
+ }
+ }
+
+ /**
+ * Create notification channel (required for Android O+)
+ */
+ private static void createNotificationChannel(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_HIGH
+ );
+ channel.setDescription("Status chat message notifications");
+ channel.enableVibration(true);
+ channel.setShowBadge(true);
+
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+ }
+
+ /**
+ * Extract deep link from notification identifier JSON
+ * The identifier contains metadata like: {"chatId": "...", "deepLink": "status-app://..."}
+ */
+ private static String extractDeepLinkFromIdentifier(String identifier) {
+ try {
+ org.json.JSONObject json = new org.json.JSONObject(identifier);
+ if (json.has("chatId")) {
+ String chatId = json.getString("chatId");
+ return "status-app://chat/" + chatId;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to parse identifier, using default deep link", e);
+ }
+ return "status-app://";
+ }
+
+ /**
+ * Get application context
+ * Returns the context stored during initialization
+ */
+ private static Context getApplicationContext() {
+ if (sApplicationContext == null) {
+ Log.e(TAG, "PushNotificationHelper not initialized! Call initialize() first.");
+ }
+ return sApplicationContext;
+ }
+
+ // ============================================================================
+ // Native methods (implemented in C++ and registered via JNI)
+ // ============================================================================
+
+ /**
+ * Called when FCM token is received
+ * Implemented in pushnotification_android.cpp
+ */
+ private static native void nativeOnFCMTokenReceived(String token);
+
+ /**
+ * Called when push notification is received
+ * Implemented in pushnotification_android.cpp
+ */
+ private static native void nativeOnPushNotificationReceived(
+ String encryptedMessage,
+ String chatId,
+ String publicKey
+ );
+}
+
diff --git a/mobile/android/qt6/src/app/status/mobile/PushNotificationService.java b/mobile/android/qt6/src/app/status/mobile/PushNotificationService.java
new file mode 100644
index 00000000000..dbf75672b6f
--- /dev/null
+++ b/mobile/android/qt6/src/app/status/mobile/PushNotificationService.java
@@ -0,0 +1,110 @@
+package app.status.mobile;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.firebase.messaging.FirebaseMessagingService;
+import com.google.firebase.messaging.RemoteMessage;
+
+/**
+ * FCM Service for receiving push notifications and device tokens
+ *
+ * This service handles:
+ * - New FCM token generation (onNewToken)
+ * - Incoming push notifications (onMessageReceived)
+ *
+ * The token is passed to C++/Qt layer via JNI for status-go registration
+ */
+public class PushNotificationService extends FirebaseMessagingService {
+ private static final String TAG = "PushNotificationService";
+
+ /**
+ * Called when the service is created by an FCM event
+ * Note: This is NOT called on app startup, only when FCM events occur
+ */
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "PushNotificationService created by FCM event");
+ // Initialize helper if not already done
+ PushNotificationHelper.initialize(this);
+ }
+
+ /**
+ * Called when FCM generates a new token for this device
+ * This happens:
+ * - On first app install
+ * - When user reinstalls app
+ * - When user clears app data
+ * - When Firebase decides to rotate the token
+ */
+ @Override
+ public void onNewToken(@NonNull String token) {
+ super.onNewToken(token);
+ Log.d(TAG, "New FCM token received: " + token);
+
+ // Pass token to C++ layer which will forward to status-go
+ PushNotificationHelper.onFCMTokenReceived(token);
+ }
+
+ /**
+ * Called when a push notification is received
+ *
+ * For Status, this will contain encrypted message data:
+ * - encryptedMessage: The encrypted message payload
+ * - chatId: The chat identifier
+ * - publicKey: Sender's public key
+ *
+ * status-go will decrypt and process the message, then emit
+ * a localNotifications signal that our Nim layer handles
+ */
+ @Override
+ public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
+ super.onMessageReceived(remoteMessage);
+
+ Log.d(TAG, "Push notification received from: " + remoteMessage.getFrom());
+
+ // Check if message contains data payload
+ if (remoteMessage.getData().size() > 0) {
+ Log.d(TAG, "Message data payload: " + remoteMessage.getData());
+
+ // Extract push notification data
+ String encryptedMessage = remoteMessage.getData().get("encryptedMessage");
+ String chatId = remoteMessage.getData().get("chatId");
+ String publicKey = remoteMessage.getData().get("publicKey");
+
+ if (encryptedMessage != null && chatId != null && publicKey != null) {
+ // Pass to C++ layer to forward to status-go
+ PushNotificationHelper.onPushNotificationReceived(
+ encryptedMessage,
+ chatId,
+ publicKey
+ );
+ } else {
+ Log.w(TAG, "Push notification missing required fields");
+ }
+ }
+
+ // Check if message contains a notification payload
+ if (remoteMessage.getNotification() != null) {
+ String title = remoteMessage.getNotification().getTitle();
+ String body = remoteMessage.getNotification().getBody();
+ Log.d(TAG, "Message Notification - Title: " + title + ", Body: " + body);
+
+ // For Status, we typically don't use the notification payload
+ // as status-go generates local notifications after decrypting
+ // But we log it for debugging
+ }
+ }
+
+ /**
+ * Called when a message is deleted on the server
+ * This can happen if the message couldn't be delivered within the TTL
+ */
+ @Override
+ public void onDeletedMessages() {
+ super.onDeletedMessages();
+ Log.d(TAG, "Messages were deleted on the server");
+ // For Status, we don't need to handle this as messages are on Waku
+ }
+}
+
diff --git a/src/app/android/push_notifications.nim b/src/app/android/push_notifications.nim
new file mode 100644
index 00000000000..216d5706426
--- /dev/null
+++ b/src/app/android/push_notifications.nim
@@ -0,0 +1,136 @@
+import chronicles, json
+import ../../backend/backend
+import ../../backend/settings
+import ../../statusq_bridge
+
+logScope:
+ topics = "android-push-notifications"
+
+# Push notification token types (from protobuf.PushNotificationRegistration_TokenType)
+const
+ UNKNOWN_TOKEN_TYPE* = 0
+ APN_TOKEN* = 1
+ FIREBASE_TOKEN* = 2
+
+# Global state - stores the FCM token until user logs in
+var g_fcmToken: string = ""
+var g_tokenRegistered: bool = false
+
+# Global callback handlers - must use cdecl calling convention
+proc onPushNotificationTokenReceived(token: cstring) {.cdecl, exportc.} =
+ # Initialize Nim GC for foreign thread calls
+ when declared(setupForeignThreadGc):
+ setupForeignThreadGc()
+ when declared(nimGC_setStackBottom):
+ var locals {.volatile, noinit.}: pointer
+ locals = addr(locals)
+ nimGC_setStackBottom(locals)
+
+ let tokenStr = $token
+ # Store the token globally - we'll register it after user logs in
+ g_fcmToken = tokenStr
+ g_tokenRegistered = false
+
+ debug "FCM token received, will register after user login"
+
+proc onPushNotificationReceived(encryptedMessage: cstring, chatId: cstring, publicKey: cstring) {.cdecl, exportc.} =
+ # Initialize Nim GC for foreign thread calls
+ when declared(setupForeignThreadGc):
+ setupForeignThreadGc()
+ when declared(nimGC_setStackBottom):
+ var locals {.volatile, noinit.}: pointer
+ locals = addr(locals)
+ nimGC_setStackBottom(locals)
+
+ debug "Push notification received",
+ encryptedMessage = $encryptedMessage,
+ chatId = $chatId,
+ publicKey = $publicKey
+
+ # NOTE: In most cases, you don't need to process this manually!
+ #
+ # The push notification serves as a WAKE-UP CALL:
+ # 1. It wakes up the app (if backgrounded/closed)
+ # 2. status-go connects to Waku automatically
+ # 3. Waku delivers messages through normal flow
+ # 4. status-go decrypts and generates local notifications
+ #
+ # The encrypted data here is for:
+ # - Quick preview before Waku sync (future enhancement)
+ # - Message prioritization
+ # - Offline handling (future enhancement)
+ #
+ # For now, just logging is sufficient. status-go handles the rest!
+
+proc registerPushNotificationToken*(): bool =
+ ## Register the stored FCM token with status-go
+ ## This should be called after user login when messenger is ready
+ ## Returns true if registration was attempted, false if no token available
+
+ if g_fcmToken.len == 0:
+ debug "No FCM token available to register"
+ return false
+
+ if g_tokenRegistered:
+ debug "FCM token already registered"
+ return false
+
+ debug "Registering FCM token with status-go (post-login)...", token=g_fcmToken
+
+ try:
+ # First, ensure messenger notifications are enabled
+ # This is required for the push notification client to be initialized
+ # TODO: This should be done by the user through the onboarding/settings
+ debug "Enabling messenger notifications in settings..."
+ let enableResponse = saveSettings("messenger-notifications-enabled?", true)
+ if not enableResponse.error.isNil:
+ error "Failed to enable messenger notifications", error=enableResponse.error
+ return false
+ debug "Messenger notifications enabled"
+
+ # Now register with status-go using the proper backend API
+ # Parameters:
+ # - deviceToken: FCM token from Firebase
+ # - apnTopic: empty string for Android (only used for iOS)
+ # - tokenType: FIREBASE_TOKEN (2) for Android
+ debug "Registering FCM token with status-go..."
+ let response = registerForPushNotifications(g_fcmToken, "", FIREBASE_TOKEN)
+
+ debug "Successfully registered for push notifications", response=response
+ g_tokenRegistered = true
+ return true
+ except Exception as e:
+ error "Failed to register for push notifications", error=e.msg
+ return false
+
+proc requestNotificationPermission*() =
+ ## Request notification permission (Android 13+ only)
+ ## On Android 12 and below, this is a no-op
+ ## Should be called before requesting FCM token
+ debug "Requesting notification permission..."
+ statusq_requestNotificationPermission()
+
+proc hasNotificationPermission*(): bool =
+ ## Check if notification permission is granted
+ ## Returns true on Android 12- (permission not required)
+ ## Returns actual permission status on Android 13+
+ statusq_hasNotificationPermission()
+
+proc initializeAndroidPushNotifications*() =
+ ## Initialize push notifications on Android
+ ## This should be called once during app startup
+ debug "Initializing Android push notifications..."
+
+ # Register our callbacks with StatusQ C++ layer
+ statusq_initPushNotifications(
+ onPushNotificationTokenReceived,
+ onPushNotificationReceived
+ )
+
+ # Request notification permission (Android 13+ only)
+ # This will show a system dialog on Android 13+
+ # On Android 12 and below, this does nothing
+ requestNotificationPermission()
+
+ debug "Android push notifications initialized"
+
diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim
index 1d2c1658229..5034a7bd65e 100644
--- a/src/app/boot/app_controller.nim
+++ b/src/app/boot/app_controller.nim
@@ -48,6 +48,9 @@ import app/core/[main]
import constants as main_constants
+when defined(android):
+ import app/android/push_notifications
+
logScope:
topics = "app-controller"
@@ -438,6 +441,11 @@ proc load(self: AppController) =
proc userLoggedIn*(self: AppController): string =
try:
self.generalService.startMessenger()
+
+ # After messenger is started, register push notification token if available
+ when defined(android):
+ discard registerPushNotificationToken()
+
return ""
except Exception as e:
let errDescription = e.msg
diff --git a/src/backend/backend.nim b/src/backend/backend.nim
index 8a270807be1..17c751703bf 100644
--- a/src/backend/backend.nim
+++ b/src/backend/backend.nim
@@ -326,3 +326,18 @@ rpc(fetchMarketTokenPageAsync, "wallet"):
rpc(unsubscribeFromLeaderboard, "wallet"):
discard
+
+# Push Notifications
+rpc(registerForPushNotifications, "wakuext"):
+ deviceToken: string
+ apnTopic: string
+ tokenType: int
+
+rpc(unregisterFromPushNotifications, "wakuext"):
+ discard
+
+rpc(enablePushNotificationsFromContactsOnly, "wakuext"):
+ discard
+
+rpc(disablePushNotificationsFromContactsOnly, "wakuext"):
+ discard
diff --git a/src/nim_status_client.nim b/src/nim_status_client.nim
index 98edb11d40b..da2b6a0c4cc 100644
--- a/src/nim_status_client.nim
+++ b/src/nim_status_client.nim
@@ -21,6 +21,9 @@ featureGuard KEYCARD_ENABLED:
var keycardServiceQObjPointer: pointer
var keycardServiceV2QObjPointer: pointer
+when defined(android):
+ import app/android/push_notifications
+
when defined(macosx) and defined(arm64):
import posix
@@ -252,6 +255,10 @@ proc mainProc() =
singletonInstance.engine.setRootContextProperty("featureFlagsRootContextProperty", newQVariant(singletonInstance.featureFlags()))
statusq_registerQmlTypes()
+
+ # Initialize push notifications (Android only)
+ when defined(android):
+ initializeAndroidPushNotifications()
app.installEventFilter(urlSchemeEvent)
diff --git a/src/statusq_bridge.nim b/src/statusq_bridge.nim
index b25947b0998..64802f2f355 100644
--- a/src/statusq_bridge.nim
+++ b/src/statusq_bridge.nim
@@ -1,3 +1,19 @@
# Declarations of methods exposed from StatusQ
proc statusq_registerQmlTypes*() {.cdecl, importc.}
+
+when defined(android):
+ # Push notification callback types
+ type
+ PushNotificationTokenCallback* = proc(token: cstring) {.cdecl.}
+ PushNotificationReceivedCallback* = proc(encryptedMessage: cstring, chatId: cstring, publicKey: cstring) {.cdecl.}
+
+ # Android push notification initialization
+ proc statusq_initPushNotifications*(
+ tokenCallback: PushNotificationTokenCallback,
+ receivedCallback: PushNotificationReceivedCallback
+ ) {.cdecl, importc.}
+
+ # Android notification permission (Android 13+)
+ proc statusq_requestNotificationPermission*() {.cdecl, importc.}
+ proc statusq_hasNotificationPermission*(): bool {.cdecl, importc.}
diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt
index a48c67e61b1..c15333e4933 100644
--- a/ui/StatusQ/CMakeLists.txt
+++ b/ui/StatusQ/CMakeLists.txt
@@ -186,6 +186,7 @@ add_library(StatusQ ${LIB_TYPE}
include/StatusQ/typesregistration.h
include/StatusQ/undefinedfilter.h
include/StatusQ/urlutils.h
+ include/StatusQ/pushnotification_android.h
src/audioutils.cpp
src/clipboardutils.cpp
src/constantrole.cpp
@@ -250,6 +251,7 @@ elseif (${CMAKE_SYSTEM_NAME} MATCHES "Android")
target_sources(StatusQ PRIVATE
src/keychain_android.cpp
src/safutils_android.cpp
+ src/pushnotification_android.cpp
)
else ()
target_sources(StatusQ PRIVATE
diff --git a/ui/StatusQ/include/StatusQ/pushnotification_android.h b/ui/StatusQ/include/StatusQ/pushnotification_android.h
new file mode 100644
index 00000000000..fa0086f2682
--- /dev/null
+++ b/ui/StatusQ/include/StatusQ/pushnotification_android.h
@@ -0,0 +1,178 @@
+#pragma once
+
+#include
+#include
+
+#ifdef Q_OS_ANDROID
+#include
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// C API for Nim integration
+typedef void (*PushNotificationTokenCallback)(const char* token);
+typedef void (*PushNotificationReceivedCallback)(const char* encryptedMessage, const char* chatId, const char* publicKey);
+
+/**
+ * Initialize push notifications with callbacks
+ * This is called from Nim code with cdecl callback functions
+ *
+ * @param tokenCallback Function to call when FCM token is received
+ * @param receivedCallback Function to call when push notification is received
+ */
+void statusq_initPushNotifications(
+ PushNotificationTokenCallback tokenCallback,
+ PushNotificationReceivedCallback receivedCallback
+);
+
+/**
+ * Show a notification (called from Nim via existing OSNotification)
+ *
+ * @param title Notification title
+ * @param message Notification message
+ * @param identifier JSON metadata
+ */
+void statusq_showAndroidNotification(const char* title, const char* message, const char* identifier);
+
+#ifdef __cplusplus
+}
+#endif
+
+#ifdef Q_OS_ANDROID
+
+/**
+ * Android Push Notification Bridge
+ *
+ * This class bridges between:
+ * - Java Android code (FCM, NotificationManager)
+ * - Qt/C++ layer
+ * - Nim backend
+ *
+ * It handles:
+ * - FCM token registration
+ * - Notification display
+ * - JNI callbacks from Java
+ */
+class PushNotificationAndroid : public QObject
+{
+ Q_OBJECT
+
+public:
+ /**
+ * Get singleton instance
+ */
+ static PushNotificationAndroid* instance();
+
+ /**
+ * Initialize push notifications with callbacks
+ * - Stores Nim callbacks
+ * - Registers JNI native methods
+ * - Requests FCM token
+ *
+ * @param tokenCallback Callback for FCM token reception
+ * @param receivedCallback Callback for push notification reception
+ */
+ void initialize(PushNotificationTokenCallback tokenCallback,
+ PushNotificationReceivedCallback receivedCallback);
+
+ /**
+ * Check if notification permission is granted (Android 13+ only)
+ * @return true if permission granted or not required (Android 12-)
+ */
+ bool hasNotificationPermission();
+
+ /**
+ * Request notification permission (Android 13+ only)
+ * Shows system permission dialog on Android 13+
+ * Does nothing on Android 12 and below
+ */
+ void requestNotificationPermission();
+
+ /**
+ * Request FCM token from Firebase
+ * The token will be delivered via tokenReceived() signal
+ */
+ void requestFCMToken();
+
+ /**
+ * Show a notification (called from Nim layer)
+ *
+ * @param title Notification title
+ * @param message Notification message
+ * @param identifier JSON string with metadata (chatId, etc.)
+ */
+ void showNotification(const QString& title, const QString& message, const QString& identifier);
+
+ /**
+ * Clear notifications for a specific chat
+ *
+ * @param chatId The chat identifier
+ */
+ void clearNotifications(const QString& chatId);
+
+signals:
+ /**
+ * Emitted when FCM token is received from Firebase
+ *
+ * @param token The FCM device token
+ */
+ void tokenReceived(const QString& token);
+
+ /**
+ * Emitted when a push notification is received (via FCM)
+ * This is for the encrypted payload that needs to be processed by status-go
+ *
+ * @param encryptedMessage The encrypted message data
+ * @param chatId The chat identifier
+ * @param publicKey The sender's public key
+ */
+ void pushNotificationReceived(const QString& encryptedMessage,
+ const QString& chatId,
+ const QString& publicKey);
+
+ /**
+ * Emitted when user taps a notification
+ *
+ * @param identifier The notification identifier (contains chatId, etc.)
+ */
+ void notificationTapped(const QString& identifier);
+
+private:
+ PushNotificationAndroid(QObject* parent = nullptr);
+ ~PushNotificationAndroid() = default;
+
+ /**
+ * Register JNI native methods
+ * This allows Java code to call C++ methods
+ */
+ void registerNativeMethods();
+
+ /**
+ * Check if Google Play Services is available
+ */
+ bool isGooglePlayServicesAvailable();
+
+ // JNI callback handlers (called from Java)
+ friend void jni_onFCMTokenReceived(JNIEnv* env, jobject obj, jstring token);
+ friend void jni_onPushNotificationReceived(JNIEnv* env, jobject obj,
+ jstring encryptedMessage,
+ jstring chatId,
+ jstring publicKey);
+
+ static PushNotificationAndroid* s_instance;
+ bool m_initialized;
+};
+
+// JNI callback functions (implemented in .cpp)
+// Note: These are C++ functions, not extern "C", because they're registered via JNI RegisterNatives
+// which handles the calling convention. They're declared as friends above.
+void jni_onFCMTokenReceived(JNIEnv* env, jobject obj, jstring token);
+void jni_onPushNotificationReceived(JNIEnv* env, jobject obj,
+ jstring encryptedMessage,
+ jstring chatId,
+ jstring publicKey);
+
+#endif // Q_OS_ANDROID
+
diff --git a/ui/StatusQ/src/externc.cpp b/ui/StatusQ/src/externc.cpp
index baa2ab6218b..8289330dec9 100644
--- a/ui/StatusQ/src/externc.cpp
+++ b/ui/StatusQ/src/externc.cpp
@@ -1,8 +1,13 @@
#include
+#include
#include
#include
+#ifdef Q_OS_ANDROID
+#include
+#endif
+
extern "C" {
Q_DECL_EXPORT void statusq_registerQmlTypes() {
@@ -13,4 +18,65 @@ Q_DECL_EXPORT float statusq_getMobileUIScaleFactor(float baseWidth, float baseDp
return MobileUI::getSmartScaleFactor(baseWidth, baseDpi, baseScale);
}
+// ============================================================================
+// Android Push Notifications C API
+// ============================================================================
+
+Q_DECL_EXPORT void statusq_initPushNotifications(
+ PushNotificationTokenCallback tokenCallback,
+ PushNotificationReceivedCallback receivedCallback)
+{
+#ifdef Q_OS_ANDROID
+ qDebug() << "[StatusQ C API] Initializing Android push notifications...";
+ PushNotificationAndroid::instance()->initialize(tokenCallback, receivedCallback);
+#else
+ Q_UNUSED(tokenCallback);
+ Q_UNUSED(receivedCallback);
+ qDebug() << "[StatusQ C API] Push notifications not available on this platform";
+#endif
+}
+
+Q_DECL_EXPORT void statusq_requestNotificationPermission()
+{
+#ifdef Q_OS_ANDROID
+ qDebug() << "[StatusQ C API] Requesting notification permission...";
+ PushNotificationAndroid::instance()->requestNotificationPermission();
+#else
+ qDebug() << "[StatusQ C API] Permission request not needed on this platform";
+#endif
+}
+
+Q_DECL_EXPORT bool statusq_hasNotificationPermission()
+{
+#ifdef Q_OS_ANDROID
+ return PushNotificationAndroid::instance()->hasNotificationPermission();
+#else
+ return true; // Other platforms don't require permission
+#endif
+}
+
+Q_DECL_EXPORT void statusq_showAndroidNotification(
+ const char* title,
+ const char* message,
+ const char* identifier)
+{
+#ifdef Q_OS_ANDROID
+ if (!title || !message || !identifier) {
+ qWarning() << "[StatusQ C API] Invalid notification parameters";
+ return;
+ }
+
+ PushNotificationAndroid::instance()->showNotification(
+ QString::fromUtf8(title),
+ QString::fromUtf8(message),
+ QString::fromUtf8(identifier)
+ );
+#else
+ Q_UNUSED(title);
+ Q_UNUSED(message);
+ Q_UNUSED(identifier);
+ qDebug() << "[StatusQ C API] showNotification not available on this platform";
+#endif
+}
+
} // extern "C"
diff --git a/ui/StatusQ/src/pushnotification_android.cpp b/ui/StatusQ/src/pushnotification_android.cpp
new file mode 100644
index 00000000000..7850f095b89
--- /dev/null
+++ b/ui/StatusQ/src/pushnotification_android.cpp
@@ -0,0 +1,373 @@
+#include "StatusQ/pushnotification_android.h"
+
+#ifdef Q_OS_ANDROID
+
+#include
+#include
+#include
+#include
+
+// Static instance and callbacks
+PushNotificationAndroid* PushNotificationAndroid::s_instance = nullptr;
+static PushNotificationTokenCallback s_tokenCallback = nullptr;
+static PushNotificationReceivedCallback s_receivedCallback = nullptr;
+
+PushNotificationAndroid::PushNotificationAndroid(QObject* parent)
+ : QObject(parent)
+ , m_initialized(false)
+{
+}
+
+PushNotificationAndroid* PushNotificationAndroid::instance()
+{
+ if (!s_instance) {
+ s_instance = new PushNotificationAndroid(qApp);
+ }
+ return s_instance;
+}
+
+void PushNotificationAndroid::initialize(PushNotificationTokenCallback tokenCallback,
+ PushNotificationReceivedCallback receivedCallback)
+{
+ if (m_initialized) {
+ qDebug() << "[PushNotificationAndroid] Already initialized";
+ return;
+ }
+
+ qDebug() << "[PushNotificationAndroid] Initializing...";
+
+ // Store callbacks
+ s_tokenCallback = tokenCallback;
+ s_receivedCallback = receivedCallback;
+
+ // Check if Google Play Services is available
+ if (!isGooglePlayServicesAvailable()) {
+ qWarning() << "[PushNotificationAndroid] Google Play Services not available";
+ return;
+ }
+
+ // Register JNI native methods
+ registerNativeMethods();
+
+ // Request FCM token
+ requestFCMToken();
+
+ m_initialized = true;
+ qDebug() << "[PushNotificationAndroid] Initialization complete";
+}
+
+bool PushNotificationAndroid::hasNotificationPermission()
+{
+ QJniEnvironment env;
+ if (!env.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Invalid JNI environment";
+ return false;
+ }
+
+ QJniObject activity = QNativeInterface::QAndroidApplication::context();
+ if (!activity.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Failed to get Android context";
+ return false;
+ }
+
+ jboolean result = QJniObject::callStaticMethod(
+ "app/status/mobile/PushNotificationHelper",
+ "hasNotificationPermission",
+ "(Landroid/content/Context;)Z",
+ activity.object()
+ );
+
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return false;
+ }
+
+ return result;
+}
+
+void PushNotificationAndroid::requestNotificationPermission()
+{
+ qDebug() << "[PushNotificationAndroid] Requesting notification permission...";
+
+ QJniEnvironment env;
+ if (!env.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Invalid JNI environment";
+ return;
+ }
+
+ QJniObject activity = QNativeInterface::QAndroidApplication::context();
+ if (!activity.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Failed to get Android context";
+ return;
+ }
+
+ QJniObject::callStaticMethod(
+ "app/status/mobile/PushNotificationHelper",
+ "requestNotificationPermission",
+ "(Landroid/content/Context;)V",
+ activity.object()
+ );
+
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ qWarning() << "[PushNotificationAndroid] Exception requesting permission";
+ return;
+ }
+
+ qDebug() << "[PushNotificationAndroid] Permission request sent";
+}
+
+void PushNotificationAndroid::requestFCMToken()
+{
+ qDebug() << "[PushNotificationAndroid] Requesting FCM token via Java layer...";
+
+ QJniEnvironment env;
+ if (!env.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Invalid JNI environment";
+ return;
+ }
+
+ // Get Android application context
+ QJniObject activity = QNativeInterface::QAndroidApplication::context();
+ if (!activity.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Failed to get Android context";
+ return;
+ }
+
+ // Check permission first
+ if (!hasNotificationPermission()) {
+ qWarning() << "[PushNotificationAndroid] Notification permission not granted!";
+ qWarning() << "[PushNotificationAndroid] Call requestNotificationPermission() first";
+ }
+
+ // Call PushNotificationHelper.requestFCMToken() which handles the Task listener
+ QJniObject::callStaticMethod(
+ "app/status/mobile/PushNotificationHelper",
+ "requestFCMToken",
+ "(Landroid/content/Context;)V",
+ activity.object()
+ );
+
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ qWarning() << "[PushNotificationAndroid] Exception requesting FCM token";
+ return;
+ }
+
+ qDebug() << "[PushNotificationAndroid] FCM token request sent, waiting for callback...";
+}
+
+void PushNotificationAndroid::showNotification(const QString& title,
+ const QString& message,
+ const QString& identifier)
+{
+ qDebug() << "[PushNotificationAndroid] Showing notification:" << title;
+
+ QJniEnvironment env;
+ if (!env.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Invalid JNI environment";
+ return;
+ }
+
+ // Call Java helper method to display notification
+ QJniObject::callStaticMethod(
+ "app/status/mobile/PushNotificationHelper",
+ "showNotification",
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
+ QJniObject::fromString(title).object(),
+ QJniObject::fromString(message).object(),
+ QJniObject::fromString(identifier).object()
+ );
+
+ // Check for exceptions
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ qWarning() << "[PushNotificationAndroid] Exception showing notification";
+ }
+}
+
+void PushNotificationAndroid::clearNotifications(const QString& chatId)
+{
+ qDebug() << "[PushNotificationAndroid] Clearing notifications for chat:" << chatId;
+
+ QJniEnvironment env;
+ if (!env.isValid()) {
+ return;
+ }
+
+ QJniObject::callStaticMethod(
+ "app/status/mobile/PushNotificationHelper",
+ "clearNotifications",
+ "(Ljava/lang/String;)V",
+ QJniObject::fromString(chatId).object()
+ );
+
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+}
+
+void PushNotificationAndroid::registerNativeMethods()
+{
+ qDebug() << "[PushNotificationAndroid] Registering JNI native methods...";
+
+ QJniEnvironment env;
+ if (!env.isValid()) {
+ qWarning() << "[PushNotificationAndroid] Invalid JNI environment";
+ return;
+ }
+
+ // Find the PushNotificationHelper class
+ jclass helperClass = env->FindClass("app/status/mobile/PushNotificationHelper");
+ if (!helperClass) {
+ qWarning() << "[PushNotificationAndroid] Could not find PushNotificationHelper class";
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return;
+ }
+
+ // Define native methods
+ JNINativeMethod methods[] = {
+ {
+ const_cast("nativeOnFCMTokenReceived"),
+ const_cast("(Ljava/lang/String;)V"),
+ reinterpret_cast(jni_onFCMTokenReceived)
+ },
+ {
+ const_cast("nativeOnPushNotificationReceived"),
+ const_cast("(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"),
+ reinterpret_cast(jni_onPushNotificationReceived)
+ }
+ };
+
+ // Register methods
+ jint result = env->RegisterNatives(helperClass, methods, 2);
+ if (result != JNI_OK) {
+ qWarning() << "[PushNotificationAndroid] Failed to register native methods:" << result;
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ } else {
+ qDebug() << "[PushNotificationAndroid] Native methods registered successfully";
+ }
+
+ env->DeleteLocalRef(helperClass);
+}
+
+bool PushNotificationAndroid::isGooglePlayServicesAvailable()
+{
+ QJniEnvironment env;
+ if (!env.isValid()) {
+ return false;
+ }
+
+ // Check if Google Play Services is available
+ // This is a simplified check - in production, you might want more thorough checking
+ try {
+ QJniObject context = QJniObject::callStaticObjectMethod(
+ "org/qtproject/qt/android/QtNative",
+ "activity",
+ "()Landroid/app/Activity;"
+ );
+
+ if (!context.isValid()) {
+ return false;
+ }
+
+ // Try to get FirebaseApp - if this works, Firebase is available
+ QJniObject firebaseApp = QJniObject::callStaticObjectMethod(
+ "com/google/firebase/FirebaseApp",
+ "getInstance",
+ "()Lcom/google/firebase/FirebaseApp;"
+ );
+
+ return firebaseApp.isValid();
+
+ } catch (...) {
+ qWarning() << "[PushNotificationAndroid] Exception checking Play Services";
+ return false;
+ }
+}
+
+// ============================================================================
+// JNI Callback Implementations
+// ============================================================================
+
+void jni_onFCMTokenReceived(JNIEnv* env, jobject obj, jstring token)
+{
+ Q_UNUSED(obj);
+
+ if (!env || !token) {
+ qWarning() << "[PushNotificationAndroid] Invalid parameters in onFCMTokenReceived";
+ return;
+ }
+
+ // Convert jstring to QString and const char*
+ const char* tokenChars = env->GetStringUTFChars(token, nullptr);
+ QString tokenStr = QString::fromUtf8(tokenChars);
+
+ qDebug() << "[PushNotificationAndroid] FCM Token received:" << tokenStr;
+
+ // Call Nim callback if registered
+ if (s_tokenCallback != nullptr) {
+ qDebug() << "[PushNotificationAndroid] Calling Nim callback with token";
+ s_tokenCallback(tokenChars);
+ } else {
+ qWarning() << "[PushNotificationAndroid] No callback registered for token!";
+ }
+
+ env->ReleaseStringUTFChars(token, tokenChars);
+
+ // Also emit signal for backward compatibility
+ if (PushNotificationAndroid::s_instance) {
+ emit PushNotificationAndroid::s_instance->tokenReceived(tokenStr);
+ }
+}
+
+void jni_onPushNotificationReceived(JNIEnv* env, jobject obj,
+ jstring encryptedMessage,
+ jstring chatId,
+ jstring publicKey)
+{
+ Q_UNUSED(obj);
+
+ if (!env || !encryptedMessage || !chatId || !publicKey) {
+ qWarning() << "[PushNotificationAndroid] Invalid parameters in onPushNotificationReceived";
+ return;
+ }
+
+ // Convert jstrings to const char*
+ const char* encMsgChars = env->GetStringUTFChars(encryptedMessage, nullptr);
+ const char* chatIdChars = env->GetStringUTFChars(chatId, nullptr);
+ const char* pubKeyChars = env->GetStringUTFChars(publicKey, nullptr);
+
+ qDebug() << "[PushNotificationAndroid] Push notification received for chat:" << chatIdChars;
+
+ // Call Nim callback if registered
+ if (s_receivedCallback != nullptr) {
+ qDebug() << "[PushNotificationAndroid] Calling Nim callback with notification data";
+ s_receivedCallback(encMsgChars, chatIdChars, pubKeyChars);
+ } else {
+ qWarning() << "[PushNotificationAndroid] No callback registered for notifications!";
+ }
+
+ env->ReleaseStringUTFChars(encryptedMessage, encMsgChars);
+ env->ReleaseStringUTFChars(chatId, chatIdChars);
+ env->ReleaseStringUTFChars(publicKey, pubKeyChars);
+
+ // Also emit signal for backward compatibility
+ if (PushNotificationAndroid::s_instance) {
+ emit PushNotificationAndroid::s_instance->pushNotificationReceived(
+ QString::fromUtf8(encMsgChars),
+ QString::fromUtf8(chatIdChars),
+ QString::fromUtf8(pubKeyChars)
+ );
+ }
+}
+
+#endif // Q_OS_ANDROID
+