diff --git a/nymea-app/platformintegration/android/java-firebase/io/guh/nymeaapp/NymeaAppNotificationService.java b/nymea-app/platformintegration/android/java-firebase/io/guh/nymeaapp/NymeaAppNotificationService.java index 10daf500..2682c200 100644 --- a/nymea-app/platformintegration/android/java-firebase/io/guh/nymeaapp/NymeaAppNotificationService.java +++ b/nymea-app/platformintegration/android/java-firebase/io/guh/nymeaapp/NymeaAppNotificationService.java @@ -26,6 +26,8 @@ import java.util.Random; public class NymeaAppNotificationService extends FirebaseMessagingService { private static final String TAG = "nymea-app: NymeaAppNotificationService"; + private static final String DEFAULT_CHANNEL_ID = "default-channel"; + private static final String DEFAULT_CHANNEL_NAME = "nymea notifications"; private int hashId(String id) { int hash = 7; @@ -59,13 +61,28 @@ public class NymeaAppNotificationService extends FirebaseMessagingService { super.onMessageReceived(remoteMessage); + RemoteMessage.Notification notification = remoteMessage.getNotification(); + String title = notification != null ? notification.getTitle() : null; + String body = notification != null ? notification.getBody() : null; + if (title == null) { + title = remoteMessage.getData().get("title"); + } + if (body == null) { + body = remoteMessage.getData().get("body"); + } + Log.d(TAG, "Notification from: " + remoteMessage.getFrom()); - Log.d(TAG, "Notification title: " + remoteMessage.getNotification().getTitle()); - Log.d(TAG, "Notification body: " + remoteMessage.getNotification().getBody()); + Log.d(TAG, "Notification title: " + title); + Log.d(TAG, "Notification body: " + body); Log.d(TAG, "Notification priority: " + remoteMessage.getPriority()); Log.d(TAG, "Notification data: " + remoteMessage.getData()); Log.d(TAG, "Notification message ID: " + remoteMessage.getMessageId()); + if (title == null && body == null && remoteMessage.getData().isEmpty()) { + Log.w(TAG, "No notification payload received, skipping notification creation."); + return; + } + Intent intent = new Intent(this, NymeaAppActivity.class); //intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.setAction(Intent.ACTION_SEND); @@ -79,21 +96,36 @@ public class NymeaAppNotificationService extends FirebaseMessagingService { // Because of this, we need to dynamically fetch the resource from the package resources int resId = getResources().getIdentifier("notificationicon", "drawable", getPackageName()); Log.d(TAG, "Notification icon resource: " + resId + " Package:" + getPackageName()); + if (resId == 0) { + resId = getApplicationInfo().icon; + Log.w(TAG, "Notification icon resource missing, using application icon: " + resId); + } NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + Log.w(TAG, "NotificationManager not available, cannot display notification."); + return; + } + + String channelId = resolveStringResource("notification_channel_id", DEFAULT_CHANNEL_ID); + String channelName = resolveStringResource("notification_channel_name", DEFAULT_CHANNEL_NAME); // Since android Oreo notification channel is needed. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel("default-channel", "Default notification channel for nymea-app", NotificationManager.IMPORTANCE_HIGH); - notificationManager.createNotificationChannel(channel); + NotificationChannel existingChannel = notificationManager.getNotificationChannel(channelId); + if (existingChannel == null) { + NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH); + notificationManager.createNotificationChannel(channel); + } } - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) - .setContentTitle(remoteMessage.getNotification().getTitle()) - .setContentText(remoteMessage.getNotification().getBody()) + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, channelId) + .setContentTitle(title) + .setContentText(body) .setSmallIcon(resId) .setAutoCancel(true) - .setContentIntent(pendingIntent); + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH); boolean sound = remoteMessage.getData().get("sound") == null || remoteMessage.getData().get("sound").equals("true"); Log.d(TAG, "Notification sound enabled: " + (sound ? "true" : "false")); @@ -114,4 +146,19 @@ public class NymeaAppNotificationService extends FirebaseMessagingService { Log.d(TAG, "Posting Notification: " + remoteMessage.getMessageId()); notificationManager.notify(0, notificationBuilder.build()); } + + private String resolveStringResource(String resourceName, String fallback) { + int resId = getResources().getIdentifier(resourceName, "string", getPackageName()); + if (resId != 0) { + try { + String resolved = getString(resId); + if (resolved != null && !resolved.isEmpty()) { + return resolved; + } + } catch (Resources.NotFoundException e) { + Log.w(TAG, "String resource not found for " + resourceName + ", using fallback"); + } + } + return fallback; + } } diff --git a/nymea-app/platformintegration/android/platformpermissionsandroid.cpp b/nymea-app/platformintegration/android/platformpermissionsandroid.cpp index 213942a1..1dbdaad7 100644 --- a/nymea-app/platformintegration/android/platformpermissionsandroid.cpp +++ b/nymea-app/platformintegration/android/platformpermissionsandroid.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include "logging.h" NYMEA_LOGGING_CATEGORY(dcPlatformPermissions, "PlatformPermissions") @@ -108,8 +110,30 @@ PlatformPermissions::PermissionStatus PlatformPermissionsAndroid::checkPermissio }); break; } + break; } case PlatformPermissions::PermissionNotifications: { + if (QOperatingSystemVersion::current() < QOperatingSystemVersion(QOperatingSystemVersion::Android, 13)) { + status = PermissionStatusGranted; + break; + } + + auto futureResult = QtAndroidPrivate::checkPermission("android.permission.POST_NOTIFICATIONS"); + QtAndroidPrivate::PermissionResult result = futureResult.result(); + switch (result) { + case QtAndroidPrivate::Authorized: + qCDebug(dcPlatformPermissions()) << "Notifications permission already granted."; + status = PermissionStatusGranted; + break; + case QtAndroidPrivate::Denied: + qCDebug(dcPlatformPermissions()) << "Notifications permission denied."; + status = PermissionStatusDenied; + break; + case QtAndroidPrivate::Undetermined: + qCDebug(dcPlatformPermissions()) << "Notifications permission not yet requested. Requesting..."; + status = PermissionStatusNotDetermined; + break; + } break; } default: @@ -154,8 +178,7 @@ void PlatformPermissionsAndroid::requestPermission(PlatformPermissions::Permissi } case PlatformPermissions::PermissionLocalNetwork: { QFuture permission_request = QtAndroidPrivate::requestPermission("android.permission.POST_NOTIFICATIONS"); - switch(permission_request.result()) - { + switch(permission_request.result()) { case QtAndroidPrivate::Undetermined: qWarning() << "Permission for posting notifications undetermined!"; break; @@ -169,6 +192,31 @@ void PlatformPermissionsAndroid::requestPermission(PlatformPermissions::Permissi break; } + case PlatformPermissions::PermissionNotifications: { + if (QOperatingSystemVersion::current() < QOperatingSystemVersion(QOperatingSystemVersion::Android, 13)) { + qCDebug(dcPlatformPermissions()) << "Notifications permission implicitly granted on Android < 13."; + emit s_instance->notificationsPermissionChanged(); + break; + } + + QFuture permission_request = QtAndroidPrivate::requestPermission("android.permission.POST_NOTIFICATIONS"); + auto result = permission_request.result(); + switch(result) { + case QtAndroidPrivate::Undetermined: + qWarning() << "Permission for posting notifications undetermined!"; + s_instance->m_requestedButDeniedPermissions.append(platformPermission); + break; + case QtAndroidPrivate::Authorized: + qDebug() << "Permission for posting notifications authorized"; + break; + case QtAndroidPrivate::Denied: + qWarning() << "Permission for posting notifications denied!"; + s_instance->m_requestedButDeniedPermissions.append(platformPermission); + break; + } + emit s_instance->notificationsPermissionChanged(); + break; + } default: qCWarning(dcPlatformPermissions()) << "Requested platform permission" << platformPermission << "but is not implemented yet."; break; @@ -276,4 +324,3 @@ void PlatformPermissionsAndroid::requestPermission(PlatformPermissions::Permissi // emit s_instance->backgroundLocationPermissionChanged(); // emit s_instance->notificationsPermissionChanged(); // } - diff --git a/nymea-app/platformintegration/platformpermissions.cpp b/nymea-app/platformintegration/platformpermissions.cpp index b6ba1ae5..178fa914 100644 --- a/nymea-app/platformintegration/platformpermissions.cpp +++ b/nymea-app/platformintegration/platformpermissions.cpp @@ -33,11 +33,14 @@ PlatformPermissions *PlatformPermissions::instance() { #ifdef Q_OS_ANDROID - return new PlatformPermissionsAndroid(); + static PlatformPermissionsAndroid instance; + return &instance; #elif defined Q_OS_IOS - return new PlatformPermissionsIOS(); + static PlatformPermissionsIOS instance; + return &instance; #else - return new PlatformPermissions(); + static PlatformPermissions instance; + return &instance; #endif } @@ -85,4 +88,3 @@ PlatformPermissions::PermissionStatus PlatformPermissions::checkPermission(Permi Q_UNUSED(permission) return PermissionStatusGranted; } - diff --git a/nymea-app/pushnotifications.cpp b/nymea-app/pushnotifications.cpp index 3e5ca749..03b5489e 100644 --- a/nymea-app/pushnotifications.cpp +++ b/nymea-app/pushnotifications.cpp @@ -24,6 +24,7 @@ #include "pushnotifications.h" #include "platformhelper.h" +#include "platformintegration/platformpermissions.h" #include #include @@ -86,6 +87,9 @@ void PushNotifications::setEnabled(bool enabled) void PushNotifications::registerForPush() { #if defined Q_OS_ANDROID && defined WITH_FIREBASE + // Ensure we have runtime permission to post notifications (Android 13+). + PlatformPermissions::instance()->requestPermission(PlatformPermissions::PermissionNotifications); + qDebug() << "Checking for play services"; jboolean playServicesAvailable = QJniObject::callStaticMethod("io.guh.nymeaapp.NymeaAppNotificationService", "checkPlayServices", "()Z"); if (playServicesAvailable) { @@ -102,8 +106,9 @@ void PushNotifications::registerForPush() firebase::messaging::Initialize(*m_firebaseApp, this); firebase::messaging::SetListener(this); - // (Optional, Android 13+): Benachrichtigungs-Erlaubnis anfragen - // firebase::messaging::RequestPermission(); + // Android 13+ requires the POST_NOTIFICATIONS runtime permission. Request it here so + // Firebase is allowed to show notifications when the app is backgrounded or closed. + firebase::messaging::RequestPermission(); diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml index 6141ef4d..abd50216 100644 --- a/packaging/android/AndroidManifest.xml +++ b/packaging/android/AndroidManifest.xml @@ -89,6 +89,7 @@ + diff --git a/packaging/android/res/values/strings.xml b/packaging/android/res/values/strings.xml index 0a8e0018..a4c0a262 100644 --- a/packaging/android/res/values/strings.xml +++ b/packaging/android/res/values/strings.xml @@ -1,4 +1,6 @@ nymea:app + default-channel + nymea notifications