From 24a0a2c3996f3ba9a2d6ac1b04b091e20d518db1 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Wed, 3 Sep 2025 16:25:27 -0700 Subject: [PATCH 01/15] No service required --- android/build.gradle | 2 +- android/src/main/AndroidManifest.xml | 10 +++- .../main/java/com/courier/android/Courier.kt | 7 +++ .../android/models/PushNotification.kt | 13 +++++ .../android/service/CourierFcmService.kt | 30 ++++++++++ app/src/main/AndroidManifest.xml | 9 --- .../com/courier/example/ExampleService.kt | 58 +++++++++---------- .../main/java/com/courier/example/Extras.kt | 52 ++++++++--------- .../java/com/courier/example/MainActivity.kt | 20 +++---- 9 files changed, 125 insertions(+), 76 deletions(-) create mode 100644 android/src/main/java/com/courier/android/models/PushNotification.kt create mode 100644 android/src/main/java/com/courier/android/service/CourierFcmService.kt diff --git a/android/build.gradle b/android/build.gradle index f337228a..20b1b751 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -62,9 +62,9 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.google.firebase:firebase-messaging-ktx:23.4.1' // Exportable APIs - api 'com.google.firebase:firebase-messaging-ktx:23.4.1' api 'androidx.recyclerview:recyclerview:1.3.2' api 'com.google.android.material:material:1.12.0' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a5918e68..16860dc2 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,12 @@ - + + + + + + + \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index 4816e4da..8e0fee68 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -10,6 +10,7 @@ import com.courier.android.client.CourierClient import com.courier.android.models.CourierAgent import com.courier.android.models.CourierAuthenticationListener import com.courier.android.models.CourierException +import com.courier.android.models.CourierPushHandler import com.courier.android.modules.InboxModule import com.courier.android.modules.linkInbox import com.courier.android.modules.refreshFcmToken @@ -49,6 +50,12 @@ class Courier private constructor(val context: Context) : Application.ActivityLi companion object { + internal var internalHandler: CourierPushHandler? = null + + fun setPushHandler(handler: CourierPushHandler) { + internalHandler = handler + } + // Core private const val VERSION = "5.2.12" var agent: CourierAgent = CourierAgent.NativeAndroid(VERSION) diff --git a/android/src/main/java/com/courier/android/models/PushNotification.kt b/android/src/main/java/com/courier/android/models/PushNotification.kt new file mode 100644 index 00000000..edc3ac26 --- /dev/null +++ b/android/src/main/java/com/courier/android/models/PushNotification.kt @@ -0,0 +1,13 @@ +package com.courier.android.models + +data class CourierMessage( + val title: String?, + val body: String?, + val data: Map +) + +interface CourierPushHandler { + fun onDelivered(msg: CourierMessage) {} + fun onToken(token: String) {} + fun showNotification(msg: CourierMessage) {} +} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierFcmService.kt b/android/src/main/java/com/courier/android/service/CourierFcmService.kt new file mode 100644 index 00000000..712863a2 --- /dev/null +++ b/android/src/main/java/com/courier/android/service/CourierFcmService.kt @@ -0,0 +1,30 @@ +package com.courier.android.service + +import com.courier.android.Courier +import com.courier.android.models.CourierMessage +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage + +internal class CourierFcmService : FirebaseMessagingService() { + + override fun onMessageReceived(message: RemoteMessage) { + + val title = message.data["title"] ?: message.notification?.title ?: "Empty Title" + val body = message.data["body"] ?: message.notification?.body ?: "Empty Body" + + val cm = CourierMessage( + title = title, + body = body, + data = message.data + ) + + Courier.internalHandler?.onDelivered(cm) + Courier.internalHandler?.showNotification(cm) + + } + + override fun onNewToken(token: String) { + Courier.internalHandler?.onToken(token) + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f341a7f3..5aab50e9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,15 +39,6 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index 85151f14..e8773c0f 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -3,36 +3,36 @@ package com.courier.example import android.annotation.SuppressLint import com.courier.android.notifications.presentNotification import com.courier.android.service.CourierService -import com.google.firebase.messaging.RemoteMessage +//import com.google.firebase.messaging.RemoteMessage // Warning is suppressed // You do not need to worry about this warning // The CourierService will handle the function automatically -@SuppressLint("MissingFirebaseInstanceTokenRefresh") -class ExampleService: CourierService() { - - override fun showNotification(message: RemoteMessage) { - super.showNotification(message) - - // This is a simple function you can use, however, - // it is recommended that you create your own - // notification and handle it's intent properly - message.presentNotification( - context = this, - handlingClass = MainActivity::class.java, - ) - - // Courier will handle delivery tracking of notifications automatically if you extend your service class with `CourierService()` - // If you do present a custom notification, you should use this function when the notification is clicked to ensure the status is updated properly - /** - Courier.trackNotification( - message = message, - event = CourierPushEvent.DELIVERED, - onSuccess = { Courier.log("Event tracked") }, - onFailure = { Courier.log(it.toString()) } - ) - **/ - - } - -} \ No newline at end of file +//@SuppressLint("MissingFirebaseInstanceTokenRefresh") +//class ExampleService: CourierService() { +// +// override fun showNotification(message: RemoteMessage) { +// super.showNotification(message) +// +// // This is a simple function you can use, however, +// // it is recommended that you create your own +// // notification and handle it's intent properly +// message.presentNotification( +// context = this, +// handlingClass = MainActivity::class.java, +// ) +// +// // Courier will handle delivery tracking of notifications automatically if you extend your service class with `CourierService()` +// // If you do present a custom notification, you should use this function when the notification is clicked to ensure the status is updated properly +// /** +// Courier.trackNotification( +// message = message, +// event = CourierPushEvent.DELIVERED, +// onSuccess = { Courier.log("Event tracked") }, +// onFailure = { Courier.log(it.toString()) } +// ) +// **/ +// +// } +// +//} \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/Extras.kt b/app/src/main/java/com/courier/example/Extras.kt index ca2d9f18..f5a9f407 100644 --- a/app/src/main/java/com/courier/example/Extras.kt +++ b/app/src/main/java/com/courier/example/Extras.kt @@ -7,7 +7,7 @@ import androidx.appcompat.app.AlertDialog import com.courier.android.models.CourierPreferenceTopic import com.courier.android.models.InboxAction import com.courier.android.models.InboxMessage -import com.google.firebase.messaging.RemoteMessage +//import com.google.firebase.messaging.RemoteMessage import com.google.gson.GsonBuilder import org.json.JSONArray import org.json.JSONObject @@ -54,40 +54,40 @@ suspend fun showAlert(context: Context, title: String, subtitle: String? = null, } -fun RemoteMessage.toJson(): JSONObject { - val json = JSONObject() - javaClass.declaredFields.forEach { field -> - field.isAccessible = true - val value = field.get(this) - json.put(field.name, value.toJsonValue()) - } - return json -} - -fun RemoteMessage.toJsonString(indentation: Int = 2): String { - return this.toJson().toString(indentation) -} +//fun RemoteMessage.toJson(): JSONObject { +// val json = JSONObject() +// javaClass.declaredFields.forEach { field -> +// field.isAccessible = true +// val value = field.get(this) +// json.put(field.name, value.toJsonValue()) +// } +// return json +//} +// +//fun RemoteMessage.toJsonString(indentation: Int = 2): String { +// return this.toJson().toString(indentation) +//} private fun Any?.toJsonValue(): Any? { return when (this) { - is RemoteMessage -> toJson() - is RemoteMessage.Notification -> toJsonObject() - is Map<*, *> -> toJsonObject() +// is RemoteMessage -> toJson() +// is RemoteMessage.Notification -> toJsonObject() +// is Map<*, *> -> toJsonObject() is List<*> -> toJsonArray() is Array<*> -> toJsonArray() else -> this } } -fun RemoteMessage.Notification.toJsonObject(): JSONObject { - val json = JSONObject() - javaClass.declaredFields.forEach { field -> - field.isAccessible = true - val value = field.get(this) - json.put(field.name, value.toJsonValue()) - } - return json -} +//fun RemoteMessage.Notification.toJsonObject(): JSONObject { +// val json = JSONObject() +// javaClass.declaredFields.forEach { field -> +// field.isAccessible = true +// val value = field.get(this) +// json.put(field.name, value.toJsonValue()) +// } +// return json +//} fun Map<*, *>.toJsonObject(): JSONObject { val json = JSONObject() diff --git a/app/src/main/java/com/courier/example/MainActivity.kt b/app/src/main/java/com/courier/example/MainActivity.kt index dcb7f953..82e074b2 100644 --- a/app/src/main/java/com/courier/example/MainActivity.kt +++ b/app/src/main/java/com/courier/example/MainActivity.kt @@ -20,7 +20,7 @@ import com.courier.example.fragments.AuthFragment import com.courier.example.fragments.InboxFragment import com.courier.example.fragments.PreferencesFragment import com.courier.example.fragments.PushFragment -import com.google.firebase.messaging.RemoteMessage +//import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.launch @@ -119,14 +119,14 @@ class MainActivity : CourierActivity() { inboxListener.remove() } - override fun onPushNotificationClicked(message: RemoteMessage) { - Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message clicked:\n${message.data}", Toast.LENGTH_LONG).show() - } - - override fun onPushNotificationDelivered(message: RemoteMessage) { - Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message delivered:\n${message.data}", Toast.LENGTH_LONG).show() - } +// override fun onPushNotificationClicked(message: RemoteMessage) { +// Log.d("Courier", message.toJsonString()) +// Toast.makeText(this, "Message clicked:\n${message.data}", Toast.LENGTH_LONG).show() +// } +// +// override fun onPushNotificationDelivered(message: RemoteMessage) { +// Log.d("Courier", message.toJsonString()) +// Toast.makeText(this, "Message delivered:\n${message.data}", Toast.LENGTH_LONG).show() +// } } \ No newline at end of file From 9567d004c2250e6cf0b1ec9b658b2da3c726fb25 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Thu, 4 Sep 2025 14:01:01 -0700 Subject: [PATCH 02/15] Backup --- android/build.gradle | 1 + android/src/main/AndroidManifest.xml | 5 +- .../main/java/com/courier/android/Courier.kt | 7 +- .../android/activity/CourierActivity.kt | 19 +- .../android/models/PushNotification.kt | 2 - .../android/notifications/CourierIntent.kt | 20 ++ .../notifications/RemoteMessageExtensions.kt | 40 ++++ .../android/service/CourierFcmService.kt | 171 +++++++++++++++++- .../service/CourierNotificationBroadcast.kt | 41 +++++ .../com/courier/android/utils/Extensions.kt | 11 ++ app/src/main/AndroidManifest.xml | 9 + .../com/courier/example/ExampleService.kt | 73 +++++++- .../java/com/courier/example/MainActivity.kt | 16 +- 13 files changed, 381 insertions(+), 34 deletions(-) create mode 100644 android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt diff --git a/android/build.gradle b/android/build.gradle index 20b1b751..c7e338f2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -60,6 +60,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + implementation 'androidx.work:work-runtime-ktx:2.9.0' implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.google.firebase:firebase-messaging-ktx:23.4.1' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 16860dc2..374ed132 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,9 +1,10 @@ - + + android:exported="true" + tools:ignore="ExportedService"> diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index 8e0fee68..1a335b8b 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -50,12 +50,6 @@ class Courier private constructor(val context: Context) : Application.ActivityLi companion object { - internal var internalHandler: CourierPushHandler? = null - - fun setPushHandler(handler: CourierPushHandler) { - internalHandler = handler - } - // Core private const val VERSION = "5.2.12" var agent: CourierAgent = CourierAgent.NativeAndroid(VERSION) @@ -150,6 +144,7 @@ class Courier private constructor(val context: Context) : Application.ActivityLi // Push internal var tokens: MutableMap = mutableMapOf() + internal var pushHandlerActivity: Class? = null // Stores a local copy of the fcmToken internal var fcmToken: String? = null diff --git a/android/src/main/java/com/courier/android/activity/CourierActivity.kt b/android/src/main/java/com/courier/android/activity/CourierActivity.kt index 0c9ba231..ff8f930a 100644 --- a/android/src/main/java/com/courier/android/activity/CourierActivity.kt +++ b/android/src/main/java/com/courier/android/activity/CourierActivity.kt @@ -1,13 +1,15 @@ package com.courier.android.activity +import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.courier.android.Courier +import com.courier.android.models.CourierMessage import com.courier.android.models.CourierTrackingEvent import com.courier.android.utils.onPushNotificationEvent +import com.courier.android.utils.toPushNotification import com.courier.android.utils.trackPushNotificationClick -import com.google.firebase.messaging.RemoteMessage open class CourierActivity : AppCompatActivity() { @@ -23,10 +25,16 @@ open class CourierActivity : AppCompatActivity() { // Handle delivered messages on the main thread Courier.shared.onPushNotificationEvent { event -> if (event.trackingEvent == CourierTrackingEvent.DELIVERED) { - onPushNotificationDelivered(event.remoteMessage) + val pushNotification = event.remoteMessage.toPushNotification() + onPushNotificationDelivered(pushNotification) } } + // Set the current push handler activity + // This is used by the default notification presentation to handle clicks + // Developers can override this if needed + Courier.shared.pushHandlerActivity = this::class.java + } override fun onNewIntent(intent: Intent) { @@ -36,12 +44,13 @@ open class CourierActivity : AppCompatActivity() { private fun checkIntentForPushNotificationClick(intent: Intent?) { intent?.trackPushNotificationClick { message -> - onPushNotificationClicked(message) + val pushNotification = message.toPushNotification() + onPushNotificationClicked(pushNotification) } } - open fun onPushNotificationClicked(message: RemoteMessage) {} + open fun onPushNotificationClicked(message: CourierMessage) {} - open fun onPushNotificationDelivered(message: RemoteMessage) {} + open fun onPushNotificationDelivered(message: CourierMessage) {} } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/models/PushNotification.kt b/android/src/main/java/com/courier/android/models/PushNotification.kt index edc3ac26..148b747e 100644 --- a/android/src/main/java/com/courier/android/models/PushNotification.kt +++ b/android/src/main/java/com/courier/android/models/PushNotification.kt @@ -7,7 +7,5 @@ data class CourierMessage( ) interface CourierPushHandler { - fun onDelivered(msg: CourierMessage) {} - fun onToken(token: String) {} fun showNotification(msg: CourierMessage) {} } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt b/android/src/main/java/com/courier/android/notifications/CourierIntent.kt index 30669348..5a94ac9f 100644 --- a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt +++ b/android/src/main/java/com/courier/android/notifications/CourierIntent.kt @@ -4,7 +4,9 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import com.courier.android.Courier +import com.courier.android.models.CourierMessage import com.google.firebase.messaging.RemoteMessage +import com.google.gson.Gson internal class CourierIntent(private val context: Context, cls: Class<*>?, message: RemoteMessage) : Intent(context, cls) { @@ -22,4 +24,22 @@ internal class CourierIntent(private val context: Context, cls: Class<*>?, messa PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE ) +} + +internal class CourierPushNotificationIntent(private val context: Context, cls: Class<*>?, message: CourierMessage) : Intent(context, cls) { + + init { + putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, Gson().toJson(message)) + addCategory(CATEGORY_LAUNCHER) + addFlags(FLAG_ACTIVITY_SINGLE_TOP) + action = ACTION_MAIN + } + + internal val pendingIntent get() = PendingIntent.getActivity( + context, + 0, + this, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE + ) + } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt index 0c3208c8..db8225f4 100644 --- a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt +++ b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt @@ -1,11 +1,16 @@ package com.courier.android.notifications +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.media.RingtoneManager import android.os.Build import androidx.core.app.NotificationCompat +import com.courier.android.activity.CourierActivity +import com.courier.android.models.CourierMessage import com.google.firebase.messaging.RemoteMessage fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { @@ -44,4 +49,39 @@ fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>? } +} + +fun CourierMessage.present(context: Context, target: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { + + try { + + val channelId = "default" + val pendingIntent = CourierPushNotificationIntent(context, target, this).pendingIntent + val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + + val notificationBuilder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(icon) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(pendingIntent) + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) + notificationManager.createNotificationChannel(channel) + } + + val uuid = System.currentTimeMillis().toInt() + notificationManager.notify(uuid, notificationBuilder.build()) + + } catch (e: Exception) { + + print(e) + + } + } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierFcmService.kt b/android/src/main/java/com/courier/android/service/CourierFcmService.kt index 712863a2..a34120dd 100644 --- a/android/src/main/java/com/courier/android/service/CourierFcmService.kt +++ b/android/src/main/java/com/courier/android/service/CourierFcmService.kt @@ -1,30 +1,181 @@ package com.courier.android.service +import android.Manifest +import android.annotation.SuppressLint +import android.app.Notification +import android.app.Service +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.provider.ContactsContract.Intents.Insert.ACTION +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission +import androidx.core.app.NotificationManagerCompat +import androidx.work.Configuration +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import androidx.work.workDataOf import com.courier.android.Courier +import com.courier.android.activity.CourierActivity +import com.courier.android.client.CourierClient import com.courier.android.models.CourierMessage +import com.courier.android.models.CourierTrackingEvent +import com.courier.android.modules.setFcmToken +import com.courier.android.notifications.present +import com.courier.android.notifications.presentNotification +import com.courier.android.utils.error +import com.courier.android.utils.log +import com.courier.android.utils.toPushNotification +import com.courier.android.utils.trackAndBroadcastTheEvent import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +object CourierEvents { + // Intent action (explicit broadcasts still carry this for clarity) + const val ACTION_COURIER_EVENT = "com.courier.android.ACTION_COURIER_EVENT" + + // Event types + const val EVENT_MESSAGE_RECEIVED = "message_received" + const val EVENT_TOKEN_REFRESHED = "token_refreshed" + + // Extras + const val EXTRA_EVENT = "courier:event" // one of the EVENT_* constants + const val EXTRA_TITLE = "courier:title" + const val EXTRA_BODY = "courier:body" + const val EXTRA_DATA = "courier:data" // Bundle + const val EXTRA_TOKEN = "courier:token" + + // App manifest key that points to the receiver FQCN + const val MD_EVENT_RECEIVER = "com.courier.EVENT_RECEIVER" +} + internal class CourierFcmService : FirebaseMessagingService() { + override fun onCreate() { + super.onCreate() + + // Init the SDK if needed + Courier.initialize(context = this) + + } + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) - val title = message.data["title"] ?: message.notification?.title ?: "Empty Title" - val body = message.data["body"] ?: message.notification?.body ?: "Empty Body" + try { - val cm = CourierMessage( - title = title, - body = body, - data = message.data - ) + // Broadcast the message to the app + // This will allow us to handle when it's delivered + Courier.shared.trackAndBroadcastTheEvent( + trackingEvent = CourierTrackingEvent.DELIVERED, + message = message + ) + + } catch (e: Exception) { + + CourierClient.default.error(e.toString()) + + } - Courier.internalHandler?.onDelivered(cm) - Courier.internalHandler?.showNotification(cm) + // Try and show the notification +// val pushNotification = message.toPushNotification() + + val dataBundle = Bundle().apply { message.data.forEach { (k, v) -> putString(k, v) } } + + broadcastToApp( + context = this, + event = CourierEvents.EVENT_MESSAGE_RECEIVED, + title = message.data["title"] ?: message.notification?.title, + body = message.data["body"] ?: message.notification?.body, + data = dataBundle + ) } override fun onNewToken(token: String) { - Courier.internalHandler?.onToken(token) + super.onNewToken(token) + + try { + + Courier.shared.setFcmToken( + token = token, + onSuccess = { Courier.shared.client?.log("Courier FCM token updated") }, + onFailure = { Courier.shared.client?.error(it.toString()) } + ) + + } catch (e: Exception) { + + Courier.shared.client?.error(e.toString()) + + } + + broadcastToApp( + context = this, + event = CourierEvents.EVENT_TOKEN_REFRESHED, + token = token + ) + + } + + private fun broadcastToApp( + context: Context, + event: String, + title: String? = null, + body: String? = null, + data: Bundle? = null, + token: String? = null + ) { + val receiverClass = resolveReceiverClass(context) ?: return + val intent = Intent(CourierEvents.ACTION_COURIER_EVENT).apply { + component = ComponentName(context, receiverClass) // explicit → reliable in bg/killed + putExtra(CourierEvents.EXTRA_EVENT, event) + when (event) { + CourierEvents.EVENT_MESSAGE_RECEIVED -> { + putExtra(CourierEvents.EXTRA_TITLE, title) + putExtra(CourierEvents.EXTRA_BODY, body) + putExtra(CourierEvents.EXTRA_DATA, data ?: Bundle()) + } + CourierEvents.EVENT_TOKEN_REFRESHED -> { + putExtra(CourierEvents.EXTRA_TOKEN, token) + } + } + } + sendBroadcast(intent) + } + + private fun broadcastToApp(event: String, extras: Intent) { + val fqcn = readReceiverFqcn() ?: run { + Log.w("CourierSDK", "No ${CourierEvents.MD_RECEIVER_FQCN} meta-data set; dropping event.") + return + } + val cls = try { Class.forName(fqcn).asSubclass(BroadcastReceiver::class.java) } + catch (t: Throwable) { Log.e("CourierSDK","Bad receiver FQCN: $fqcn", t); return } + + val intent = Intent(CourierEvents.ACTION).apply { + component = ComponentName(this@CourierFcmService, cls) // explicit → reliable + putExtra(CourierEvents.EXTRA_EVENT, event) + putExtras(extras) + } + sendBroadcast(intent) + } + + private fun readReceiverFqcn(): String? { + val pm = packageManager + val flags = if (Build.VERSION.SDK_INT >= 33) + PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) + else @Suppress("DEPRECATION") PackageManager.GET_META_DATA + + val appInfo = if (Build.VERSION.SDK_INT >= 33) + pm.getApplicationInfo(packageName, flags) + else @Suppress("DEPRECATION") pm.getApplicationInfo(packageName, flags as Int) + + return appInfo.metaData?.getString(CourierEvents.MD_RECEIVER_FQCN) } } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt b/android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt new file mode 100644 index 00000000..2e471f8b --- /dev/null +++ b/android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt @@ -0,0 +1,41 @@ +package com.courier.android.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle + +abstract class CourierNotificationBroadcast : BroadcastReceiver() { + + /** Override to handle new messages */ + protected open fun onCourierMessage( + context: Context, + title: String?, + body: String?, + data: Map + ) {} + + /** Override to handle new/rotated FCM tokens */ + protected open fun onCourierNewToken( + context: Context, + token: String + ) {} + + final override fun onReceive(context: Context, intent: Intent) { + if (intent.action != CourierEvents.ACTION_COURIER_EVENT) return + + when (intent.getStringExtra(CourierEvents.EXTRA_EVENT)) { + CourierEvents.EVENT_MESSAGE_RECEIVED -> { + val title = intent.getStringExtra(CourierEvents.EXTRA_TITLE) + val body = intent.getStringExtra(CourierEvents.EXTRA_BODY) + val bundle = intent.getBundleExtra(CourierEvents.EXTRA_DATA) ?: Bundle() + val data = bundle.keySet().associateWith { bundle.getString(it).orEmpty() } + onCourierMessage(context, title, body, data) + } + CourierEvents.EVENT_TOKEN_REFRESHED -> { + val token = intent.getStringExtra(CourierEvents.EXTRA_TOKEN) ?: return + onCourierNewToken(context, token) + } + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/utils/Extensions.kt b/android/src/main/java/com/courier/android/utils/Extensions.kt index bee8cdf4..1ff458bf 100644 --- a/android/src/main/java/com/courier/android/utils/Extensions.kt +++ b/android/src/main/java/com/courier/android/utils/Extensions.kt @@ -21,6 +21,7 @@ import com.courier.android.Courier.Companion.coroutineScope import com.courier.android.Courier.Companion.eventBus import com.courier.android.client.CourierClient import com.courier.android.models.CourierException +import com.courier.android.models.CourierMessage import com.courier.android.models.CourierTrackingEvent import com.courier.android.models.CourierPushNotificationEvent import com.courier.android.models.SemanticProperties @@ -37,6 +38,16 @@ import java.util.Date import java.util.Locale import java.util.TimeZone +internal fun RemoteMessage.toPushNotification(): CourierMessage { + val title = data["title"] ?: notification?.title ?: "Empty Title" + val body = data["body"] ?: notification?.body ?: "Empty Body" + return CourierMessage( + title = title, + body = body, + data = data + ) +} + fun Intent.trackPushNotificationClick(onClick: (message: RemoteMessage) -> Unit) { try { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5aab50e9..b2168c2e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index e8773c0f..a2a627b8 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -1,7 +1,19 @@ package com.courier.example import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import com.courier.android.models.CourierMessage +import com.courier.android.notifications.present import com.courier.android.notifications.presentNotification +import com.courier.android.service.CourierNotificationBroadcast import com.courier.android.service.CourierService //import com.google.firebase.messaging.RemoteMessage @@ -35,4 +47,63 @@ import com.courier.android.service.CourierService // // } // -//} \ No newline at end of file +//} + +// App module (no Firebase imports) +class MyCourierReceiver : CourierNotificationBroadcast() { + + override fun onCourierMessage( + context: Context, + title: String?, + body: String?, + data: Map + ) { + // Example: post your user-visible notification + showNotification(context, title ?: "New message", body.orEmpty(), data) + } + + override fun onCourierNewToken(context: Context, token: String) { + // Upload token to backend, logging, etc. + } + + private fun showNotification( + ctx: Context, + title: String, + body: String, + data: Map + ) { + val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager + val channelId = "my-courier" + if (android.os.Build.VERSION.SDK_INT >= 26 && + nm.getNotificationChannel(channelId) == null + ) { + nm.createNotificationChannel( + android.app.NotificationChannel( + channelId, "Courier", + android.app.NotificationManager.IMPORTANCE_HIGH + ) + ) + } + + val tap = android.app.PendingIntent.getActivity( + ctx, + 0, + Intent(ctx, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + putExtra("courier_data", HashMap(data)) + }, + android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE + ) + + val notif = androidx.core.app.NotificationCompat.Builder(ctx, channelId) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .setPriority(androidx.core.app.NotificationCompat.PRIORITY_HIGH) + .setContentIntent(tap) + .build() + + nm.notify(System.currentTimeMillis().toInt(), notif) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/MainActivity.kt b/app/src/main/java/com/courier/example/MainActivity.kt index 82e074b2..d1f639ab 100644 --- a/app/src/main/java/com/courier/example/MainActivity.kt +++ b/app/src/main/java/com/courier/example/MainActivity.kt @@ -11,6 +11,7 @@ import com.courier.android.activity.CourierActivity import com.courier.android.client.CourierClient import com.courier.android.models.CourierException import com.courier.android.models.CourierInboxListener +import com.courier.android.models.CourierMessage import com.courier.android.modules.addInboxListener import com.courier.android.modules.signIn import com.courier.android.modules.tenantId @@ -20,7 +21,6 @@ import com.courier.example.fragments.AuthFragment import com.courier.example.fragments.InboxFragment import com.courier.example.fragments.PreferencesFragment import com.courier.example.fragments.PushFragment -//import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.launch @@ -119,14 +119,14 @@ class MainActivity : CourierActivity() { inboxListener.remove() } -// override fun onPushNotificationClicked(message: RemoteMessage) { + override fun onPushNotificationClicked(message: CourierMessage) { // Log.d("Courier", message.toJsonString()) -// Toast.makeText(this, "Message clicked:\n${message.data}", Toast.LENGTH_LONG).show() -// } -// -// override fun onPushNotificationDelivered(message: RemoteMessage) { + Toast.makeText(this, "Message clicked:\n${message.data}", Toast.LENGTH_LONG).show() + } + + override fun onPushNotificationDelivered(message: CourierMessage) { // Log.d("Courier", message.toJsonString()) -// Toast.makeText(this, "Message delivered:\n${message.data}", Toast.LENGTH_LONG).show() -// } + Toast.makeText(this, "Message delivered:\n${message.data}", Toast.LENGTH_LONG).show() + } } \ No newline at end of file From 9fa0902a3235846fdda48a28b214a98f26d518dd Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Thu, 4 Sep 2025 16:16:44 -0700 Subject: [PATCH 03/15] Broadcaster not working --- android/src/main/AndroidManifest.xml | 10 +- .../main/java/com/courier/android/Courier.kt | 17 ++- .../android/models/PushNotification.kt | 8 +- .../android/service/CourierFcmService.kt | 94 +------------ .../android/service/CourierFirebaseProxy.kt | 101 ++++++++++++++ .../service/CourierNotificationBroadcast.kt | 41 ------ .../courier/android/service/CourierService.kt | 132 +++++++++++------- .../com/courier/android/utils/Extensions.kt | 2 +- app/src/main/AndroidManifest.xml | 13 +- .../com/courier/example/ExampleService.kt | 119 ++++++++-------- .../java/com/courier/example/MainActivity.kt | 4 +- 11 files changed, 285 insertions(+), 256 deletions(-) create mode 100644 android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt delete mode 100644 android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 374ed132..c6fbc87b 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,10 +1,14 @@ + + + + + + android:name=".service.CourierFirebaseProxy" + android:exported="false"> diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index 1a335b8b..f9e66093 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -4,18 +4,20 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.Application import android.content.Context +import android.content.Intent import android.os.Build import android.os.Bundle import com.courier.android.client.CourierClient import com.courier.android.models.CourierAgent import com.courier.android.models.CourierAuthenticationListener import com.courier.android.models.CourierException -import com.courier.android.models.CourierPushHandler import com.courier.android.modules.InboxModule import com.courier.android.modules.linkInbox import com.courier.android.modules.refreshFcmToken import com.courier.android.modules.unlinkInbox +import com.courier.android.service.CourierActions import com.courier.android.utils.NotificationEventBus +import com.courier.android.utils.log import com.courier.android.utils.warn import com.google.firebase.FirebaseApp import kotlinx.coroutines.CoroutineScope @@ -100,6 +102,19 @@ class Courier private constructor(val context: Context) : Application.ActivityLi mInstance?.refreshFcmToken() } + verifyManifestReceivers(context) + + } + + private fun verifyManifestReceivers(context: Context) { + val intent = Intent(CourierActions.MESSAGE_RECEIVED) + val receivers = context.packageManager.queryBroadcastReceivers(intent, 0) + + if (receivers.isNotEmpty()) { + shared.client?.log("Found ${receivers.size} Courier receivers in manifest") + } else { + shared.client?.log("No Courier receivers found - add receiver to AndroidManifest.xml") + } } private fun Courier.registerLifecycleCallbacks() { diff --git a/android/src/main/java/com/courier/android/models/PushNotification.kt b/android/src/main/java/com/courier/android/models/PushNotification.kt index 148b747e..b75b1ff1 100644 --- a/android/src/main/java/com/courier/android/models/PushNotification.kt +++ b/android/src/main/java/com/courier/android/models/PushNotification.kt @@ -3,9 +3,5 @@ package com.courier.android.models data class CourierMessage( val title: String?, val body: String?, - val data: Map -) - -interface CourierPushHandler { - fun showNotification(msg: CourierMessage) {} -} \ No newline at end of file +// val data: Map +) \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierFcmService.kt b/android/src/main/java/com/courier/android/service/CourierFcmService.kt index a34120dd..20296d86 100644 --- a/android/src/main/java/com/courier/android/service/CourierFcmService.kt +++ b/android/src/main/java/com/courier/android/service/CourierFcmService.kt @@ -36,25 +36,6 @@ import com.courier.android.utils.trackAndBroadcastTheEvent import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -object CourierEvents { - // Intent action (explicit broadcasts still carry this for clarity) - const val ACTION_COURIER_EVENT = "com.courier.android.ACTION_COURIER_EVENT" - - // Event types - const val EVENT_MESSAGE_RECEIVED = "message_received" - const val EVENT_TOKEN_REFRESHED = "token_refreshed" - - // Extras - const val EXTRA_EVENT = "courier:event" // one of the EVENT_* constants - const val EXTRA_TITLE = "courier:title" - const val EXTRA_BODY = "courier:body" - const val EXTRA_DATA = "courier:data" // Bundle - const val EXTRA_TOKEN = "courier:token" - - // App manifest key that points to the receiver FQCN - const val MD_EVENT_RECEIVER = "com.courier.EVENT_RECEIVER" -} - internal class CourierFcmService : FirebaseMessagingService() { override fun onCreate() { @@ -86,15 +67,13 @@ internal class CourierFcmService : FirebaseMessagingService() { // Try and show the notification // val pushNotification = message.toPushNotification() - val dataBundle = Bundle().apply { message.data.forEach { (k, v) -> putString(k, v) } } + val dataBundle = Bundle().apply { + message.data.forEach { (k, v) -> + putString(k, v) + } + } - broadcastToApp( - context = this, - event = CourierEvents.EVENT_MESSAGE_RECEIVED, - title = message.data["title"] ?: message.notification?.title, - body = message.data["body"] ?: message.notification?.body, - data = dataBundle - ) + val test = dataBundle } @@ -115,67 +94,6 @@ internal class CourierFcmService : FirebaseMessagingService() { } - broadcastToApp( - context = this, - event = CourierEvents.EVENT_TOKEN_REFRESHED, - token = token - ) - - } - - private fun broadcastToApp( - context: Context, - event: String, - title: String? = null, - body: String? = null, - data: Bundle? = null, - token: String? = null - ) { - val receiverClass = resolveReceiverClass(context) ?: return - val intent = Intent(CourierEvents.ACTION_COURIER_EVENT).apply { - component = ComponentName(context, receiverClass) // explicit → reliable in bg/killed - putExtra(CourierEvents.EXTRA_EVENT, event) - when (event) { - CourierEvents.EVENT_MESSAGE_RECEIVED -> { - putExtra(CourierEvents.EXTRA_TITLE, title) - putExtra(CourierEvents.EXTRA_BODY, body) - putExtra(CourierEvents.EXTRA_DATA, data ?: Bundle()) - } - CourierEvents.EVENT_TOKEN_REFRESHED -> { - putExtra(CourierEvents.EXTRA_TOKEN, token) - } - } - } - sendBroadcast(intent) - } - - private fun broadcastToApp(event: String, extras: Intent) { - val fqcn = readReceiverFqcn() ?: run { - Log.w("CourierSDK", "No ${CourierEvents.MD_RECEIVER_FQCN} meta-data set; dropping event.") - return - } - val cls = try { Class.forName(fqcn).asSubclass(BroadcastReceiver::class.java) } - catch (t: Throwable) { Log.e("CourierSDK","Bad receiver FQCN: $fqcn", t); return } - - val intent = Intent(CourierEvents.ACTION).apply { - component = ComponentName(this@CourierFcmService, cls) // explicit → reliable - putExtra(CourierEvents.EXTRA_EVENT, event) - putExtras(extras) - } - sendBroadcast(intent) - } - - private fun readReceiverFqcn(): String? { - val pm = packageManager - val flags = if (Build.VERSION.SDK_INT >= 33) - PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) - else @Suppress("DEPRECATION") PackageManager.GET_META_DATA - - val appInfo = if (Build.VERSION.SDK_INT >= 33) - pm.getApplicationInfo(packageName, flags) - else @Suppress("DEPRECATION") pm.getApplicationInfo(packageName, flags as Int) - - return appInfo.metaData?.getString(CourierEvents.MD_RECEIVER_FQCN) } } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt b/android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt new file mode 100644 index 00000000..68952e58 --- /dev/null +++ b/android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt @@ -0,0 +1,101 @@ +package com.courier.android.service + +import android.content.Intent +import android.os.Bundle +import com.courier.android.Courier +import com.courier.android.client.CourierClient +import com.courier.android.models.CourierTrackingEvent +import com.courier.android.modules.setFcmToken +import com.courier.android.utils.error +import com.courier.android.utils.log +import com.courier.android.utils.trackAndBroadcastTheEvent +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage + +object CourierActions { + const val MESSAGE_RECEIVED = "com.trycourier.MESSAGE_RECEIVED" + const val TOKEN_UPDATED = "com.trycourier.TOKEN_UPDATED" +} + +// Your auto-generated Firebase service (created at runtime) +class CourierFirebaseProxy : FirebaseMessagingService() { + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + + try { + + // Broadcast the message to the app + // This will allow us to handle when it's delivered + Courier.shared.trackAndBroadcastTheEvent( + trackingEvent = CourierTrackingEvent.DELIVERED, + message = message + ) + + } catch (e: Exception) { + + CourierClient.default.error(e.toString()) + + } + + // Broadcast to ALL manifest-declared receivers listening for this action + broadcastToManifestReceivers(message) + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + + try { + + Courier.shared.setFcmToken( + token = token, + onSuccess = { Courier.shared.client?.log("Courier FCM token updated") }, + onFailure = { Courier.shared.client?.error(it.toString()) } + ) + + } catch (e: Exception) { + + Courier.shared.client?.error(e.toString()) + + } + + } + + private fun broadcastToManifestReceivers(message: RemoteMessage) { + val intent = Intent(CourierActions.MESSAGE_RECEIVED).apply { + + // Add message data + putExtra("title", message.notification?.title) + putExtra("body", message.notification?.body) + putExtra("from", message.from) + + val dataBundle = Bundle().apply { + message.data.forEach { (key, value) -> putString(key, value) } + } + putExtra("data", dataBundle) + + // CRITICAL: This flag makes it work in killed state + addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + } + + // Send explicit broadcasts to all receivers that declared this action + sendBroadcastToManifestReceivers(intent) + } + + private fun sendBroadcastToManifestReceivers(intent: Intent) { + // Get all receivers that can handle this intent from the manifest + val packageManager = packageManager + val receivers = packageManager.queryBroadcastReceivers(intent, 0) + + // Send explicit intent to each receiver (works better for killed apps) + receivers.forEach { resolveInfo -> + val explicitIntent = Intent(intent).apply { + setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name) + } + sendBroadcast(explicitIntent) + } + + // Also send the implicit broadcast as backup + sendBroadcast(intent) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt b/android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt deleted file mode 100644 index 2e471f8b..00000000 --- a/android/src/main/java/com/courier/android/service/CourierNotificationBroadcast.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.courier.android.service - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Bundle - -abstract class CourierNotificationBroadcast : BroadcastReceiver() { - - /** Override to handle new messages */ - protected open fun onCourierMessage( - context: Context, - title: String?, - body: String?, - data: Map - ) {} - - /** Override to handle new/rotated FCM tokens */ - protected open fun onCourierNewToken( - context: Context, - token: String - ) {} - - final override fun onReceive(context: Context, intent: Intent) { - if (intent.action != CourierEvents.ACTION_COURIER_EVENT) return - - when (intent.getStringExtra(CourierEvents.EXTRA_EVENT)) { - CourierEvents.EVENT_MESSAGE_RECEIVED -> { - val title = intent.getStringExtra(CourierEvents.EXTRA_TITLE) - val body = intent.getStringExtra(CourierEvents.EXTRA_BODY) - val bundle = intent.getBundleExtra(CourierEvents.EXTRA_DATA) ?: Bundle() - val data = bundle.keySet().associateWith { bundle.getString(it).orEmpty() } - onCourierMessage(context, title, body, data) - } - CourierEvents.EVENT_TOKEN_REFRESHED -> { - val token = intent.getStringExtra(CourierEvents.EXTRA_TOKEN) ?: return - onCourierNewToken(context, token) - } - } - } -} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierService.kt b/android/src/main/java/com/courier/android/service/CourierService.kt index 5c3c983d..d98df928 100644 --- a/android/src/main/java/com/courier/android/service/CourierService.kt +++ b/android/src/main/java/com/courier/android/service/CourierService.kt @@ -1,69 +1,95 @@ package com.courier.android.service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle import com.courier.android.Courier import com.courier.android.client.CourierClient +import com.courier.android.models.CourierMessage import com.courier.android.models.CourierTrackingEvent import com.courier.android.modules.setFcmToken import com.courier.android.utils.trackAndBroadcastTheEvent import com.courier.android.utils.error import com.courier.android.utils.log +import com.courier.android.utils.toPushNotification import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -open class CourierService: FirebaseMessagingService() { - - override fun onCreate() { - super.onCreate() - - // Init the SDK if needed - Courier.initialize(context = this) - - } - - override fun onMessageReceived(message: RemoteMessage) { - super.onMessageReceived(message) - - try { - - // Broadcast the message to the app - // This will allow us to handle when it's delivered - Courier.shared.trackAndBroadcastTheEvent( - trackingEvent = CourierTrackingEvent.DELIVERED, - message = message - ) - - } catch (e: Exception) { - - CourierClient.default.error(e.toString()) - +//open class CourierService: FirebaseMessagingService() { +// +// override fun onCreate() { +// super.onCreate() +// +// // Init the SDK if needed +// Courier.initialize(context = this) +// +// } +// +// override fun onMessageReceived(message: RemoteMessage) { +// super.onMessageReceived(message) +// +// try { +// +// // Broadcast the message to the app +// // This will allow us to handle when it's delivered +// Courier.shared.trackAndBroadcastTheEvent( +// trackingEvent = CourierTrackingEvent.DELIVERED, +// message = message +// ) +// +// } catch (e: Exception) { +// +// CourierClient.default.error(e.toString()) +// +// } +// +// // Try and show the notification +// showNotification(message) +// +// } +// +// override fun onNewToken(token: String) { +// super.onNewToken(token) +// +// try { +// +// Courier.shared.setFcmToken( +// token = token, +// onSuccess = { Courier.shared.client?.log("Courier FCM token updated") }, +// onFailure = { Courier.shared.client?.error(it.toString()) } +// ) +// +// } catch (e: Exception) { +// +// Courier.shared.client?.error(e.toString()) +// +// } +// +// } +// +// open fun showNotification(message: RemoteMessage) { +// // Empty +// } +// +//} + +abstract class CourierReceiver : BroadcastReceiver() { + + final override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + CourierActions.MESSAGE_RECEIVED -> { + val courierMessage = CourierMessage( + title = intent.getStringExtra("title"), + body = intent.getStringExtra("body"), +// data = intent.getStringExtra("data") + ) + + onCourierMessage(context, courierMessage) + } } - - // Try and show the notification - showNotification(message) - - } - - override fun onNewToken(token: String) { - super.onNewToken(token) - - try { - - Courier.shared.setFcmToken( - token = token, - onSuccess = { Courier.shared.client?.log("Courier FCM token updated") }, - onFailure = { Courier.shared.client?.error(it.toString()) } - ) - - } catch (e: Exception) { - - Courier.shared.client?.error(e.toString()) - - } - - } - - open fun showNotification(message: RemoteMessage) { - // Empty } + // Developer just implements this + abstract fun onCourierMessage(context: Context, message: CourierMessage) } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/utils/Extensions.kt b/android/src/main/java/com/courier/android/utils/Extensions.kt index 1ff458bf..5470e7e8 100644 --- a/android/src/main/java/com/courier/android/utils/Extensions.kt +++ b/android/src/main/java/com/courier/android/utils/Extensions.kt @@ -44,7 +44,7 @@ internal fun RemoteMessage.toPushNotification(): CourierMessage { return CourierMessage( title = title, body = body, - data = data +// data = data ) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b2168c2e..7e53d5e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,13 +40,14 @@ - - + android:name=".MyNotificationReceiver" + android:enabled="true" + android:exported="false"> + + + + diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index a2a627b8..abb95f52 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -8,13 +8,14 @@ import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build +import android.os.Bundle import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import com.courier.android.models.CourierMessage import com.courier.android.notifications.present import com.courier.android.notifications.presentNotification -import com.courier.android.service.CourierNotificationBroadcast -import com.courier.android.service.CourierService +import com.courier.android.service.CourierReceiver +//import com.courier.android.service.CourierService //import com.google.firebase.messaging.RemoteMessage // Warning is suppressed @@ -49,61 +50,69 @@ import com.courier.android.service.CourierService // //} -// App module (no Firebase imports) -class MyCourierReceiver : CourierNotificationBroadcast() { +//// App module (no Firebase imports) +//class MyCourierReceiver : CourierNotificationBroadcast() { +// +// override fun onCourierMessage( +// context: Context, +// title: String?, +// body: String?, +// data: Map +// ) { +// // Example: post your user-visible notification +// showNotification(context, title ?: "New message", body.orEmpty(), data) +// } +// +// override fun onCourierNewToken(context: Context, token: String) { +// // Upload token to backend, logging, etc. +// } +// +// private fun showNotification( +// ctx: Context, +// title: String, +// body: String, +// data: Map +// ) { +// val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager +// val channelId = "my-courier" +// if (android.os.Build.VERSION.SDK_INT >= 26 && +// nm.getNotificationChannel(channelId) == null +// ) { +// nm.createNotificationChannel( +// android.app.NotificationChannel( +// channelId, "Courier", +// android.app.NotificationManager.IMPORTANCE_HIGH +// ) +// ) +// } +// +// val tap = android.app.PendingIntent.getActivity( +// ctx, +// 0, +// Intent(ctx, MainActivity::class.java).apply { +// flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP +// putExtra("courier_data", HashMap(data)) +// }, +// android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE +// ) +// +// val notif = androidx.core.app.NotificationCompat.Builder(ctx, channelId) +// .setSmallIcon(android.R.drawable.ic_dialog_info) +// .setContentTitle(title) +// .setContentText(body) +// .setAutoCancel(true) +// .setPriority(androidx.core.app.NotificationCompat.PRIORITY_HIGH) +// .setContentIntent(tap) +// .build() +// +// nm.notify(System.currentTimeMillis().toInt(), notif) +// } +//} - override fun onCourierMessage( - context: Context, - title: String?, - body: String?, - data: Map - ) { - // Example: post your user-visible notification - showNotification(context, title ?: "New message", body.orEmpty(), data) - } +class MyNotificationReceiver: CourierReceiver() { - override fun onCourierNewToken(context: Context, token: String) { - // Upload token to backend, logging, etc. + override fun onCourierMessage(context: Context, message: CourierMessage) { + message.present(context, MainActivity::class.java) } - private fun showNotification( - ctx: Context, - title: String, - body: String, - data: Map - ) { - val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager - val channelId = "my-courier" - if (android.os.Build.VERSION.SDK_INT >= 26 && - nm.getNotificationChannel(channelId) == null - ) { - nm.createNotificationChannel( - android.app.NotificationChannel( - channelId, "Courier", - android.app.NotificationManager.IMPORTANCE_HIGH - ) - ) - } - - val tap = android.app.PendingIntent.getActivity( - ctx, - 0, - Intent(ctx, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - putExtra("courier_data", HashMap(data)) - }, - android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE - ) - - val notif = androidx.core.app.NotificationCompat.Builder(ctx, channelId) - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentTitle(title) - .setContentText(body) - .setAutoCancel(true) - .setPriority(androidx.core.app.NotificationCompat.PRIORITY_HIGH) - .setContentIntent(tap) - .build() - - nm.notify(System.currentTimeMillis().toInt(), notif) - } } \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/MainActivity.kt b/app/src/main/java/com/courier/example/MainActivity.kt index d1f639ab..b2bc399f 100644 --- a/app/src/main/java/com/courier/example/MainActivity.kt +++ b/app/src/main/java/com/courier/example/MainActivity.kt @@ -121,12 +121,12 @@ class MainActivity : CourierActivity() { override fun onPushNotificationClicked(message: CourierMessage) { // Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message clicked:\n${message.data}", Toast.LENGTH_LONG).show() + Toast.makeText(this, "Message clicked:\n${message.title}", Toast.LENGTH_LONG).show() } override fun onPushNotificationDelivered(message: CourierMessage) { // Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message delivered:\n${message.data}", Toast.LENGTH_LONG).show() + Toast.makeText(this, "Message delivered:\n${message.title}", Toast.LENGTH_LONG).show() } } \ No newline at end of file From 01eff1dec7eaf80121eff02f426478cd3a4d3707 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Fri, 5 Sep 2025 11:39:40 -0700 Subject: [PATCH 04/15] Removed the public dep --- android/src/main/AndroidManifest.xml | 17 +-- .../main/java/com/courier/android/Courier.kt | 63 ++++++++--- .../android/activity/CourierActivity.kt | 53 +++++++-- .../android/models/PushNotification.kt | 7 -- .../android/notifications/CourierIntent.kt | 5 +- .../notifications/RemoteMessageExtensions.kt | 40 ------- .../android/service/CourierFcmService.kt | 99 ---------------- .../android/service/CourierFirebaseProxy.kt | 101 ----------------- .../courier/android/service/CourierService.kt | 95 ---------------- .../com/courier/android/utils/Extensions.kt | 59 ++++------ app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 10 +- .../com/courier/example/ExampleService.kt | 106 +++--------------- .../java/com/courier/example/MainActivity.kt | 10 +- 14 files changed, 139 insertions(+), 527 deletions(-) delete mode 100644 android/src/main/java/com/courier/android/models/PushNotification.kt delete mode 100644 android/src/main/java/com/courier/android/service/CourierFcmService.kt delete mode 100644 android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt delete mode 100644 android/src/main/java/com/courier/android/service/CourierService.kt diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index c6fbc87b..7f7872b0 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,17 +1,4 @@ - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index f9e66093..73775578 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -11,15 +11,22 @@ import com.courier.android.client.CourierClient import com.courier.android.models.CourierAgent import com.courier.android.models.CourierAuthenticationListener import com.courier.android.models.CourierException +import com.courier.android.models.CourierTrackingEvent import com.courier.android.modules.InboxModule import com.courier.android.modules.linkInbox import com.courier.android.modules.refreshFcmToken +import com.courier.android.modules.setFcmToken import com.courier.android.modules.unlinkInbox -import com.courier.android.service.CourierActions +import com.courier.android.notifications.presentNotification import com.courier.android.utils.NotificationEventBus +import com.courier.android.utils.broadcastPushNotification +import com.courier.android.utils.error import com.courier.android.utils.log +import com.courier.android.utils.trackPushNotification +import com.courier.android.utils.trackingUrl import com.courier.android.utils.warn import com.google.firebase.FirebaseApp +import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -102,19 +109,6 @@ class Courier private constructor(val context: Context) : Application.ActivityLi mInstance?.refreshFcmToken() } - verifyManifestReceivers(context) - - } - - private fun verifyManifestReceivers(context: Context) { - val intent = Intent(CourierActions.MESSAGE_RECEIVED) - val receivers = context.packageManager.queryBroadcastReceivers(intent, 0) - - if (receivers.isNotEmpty()) { - shared.client?.log("Found ${receivers.size} Courier receivers in manifest") - } else { - shared.client?.log("No Courier receivers found - add receiver to AndroidManifest.xml") - } } private fun Courier.registerLifecycleCallbacks() { @@ -141,6 +135,47 @@ class Courier private constructor(val context: Context) : Application.ActivityLi } } + // Push Notification Handlers + fun onMessageReceived(remoteMessage: RemoteMessage) = coroutineScope.launch(Dispatchers.IO) { + try { + + val trackingEvent = CourierTrackingEvent.DELIVERED + + // Broadcast the message to the app + shared.broadcastPushNotification( + trackingEvent = trackingEvent, + remoteMessage = remoteMessage + ) + + // Track the push notification delivery + remoteMessage.trackingUrl?.let { trackingUrl -> + shared.trackPushNotification( + trackingEvent = trackingEvent, + trackingUrl = trackingUrl + ) + } + + } catch (e: Exception) { + CourierClient.default.error(e.toString()) + } + } + + fun presentNotification(remoteMessage: RemoteMessage, context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { + remoteMessage.presentNotification(context, handlingClass, icon, settingsTitle) + } + + fun onNewToken(token: String) { + try { + shared.setFcmToken( + token = token, + onSuccess = { shared.client?.log("Courier FCM token updated") }, + onFailure = { shared.client?.error(it.toString()) } + ) + } catch (e: Exception) { + CourierClient.default.error(e.toString()) + } + } + } // Inbox diff --git a/android/src/main/java/com/courier/android/activity/CourierActivity.kt b/android/src/main/java/com/courier/android/activity/CourierActivity.kt index ff8f930a..67ffebbb 100644 --- a/android/src/main/java/com/courier/android/activity/CourierActivity.kt +++ b/android/src/main/java/com/courier/android/activity/CourierActivity.kt @@ -1,15 +1,19 @@ package com.courier.android.activity -import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.courier.android.Courier -import com.courier.android.models.CourierMessage import com.courier.android.models.CourierTrackingEvent +import com.courier.android.utils.broadcastPushNotification +import com.courier.android.utils.getRemoteMessage import com.courier.android.utils.onPushNotificationEvent -import com.courier.android.utils.toPushNotification -import com.courier.android.utils.trackPushNotificationClick +import com.courier.android.utils.trackPushNotification +import com.courier.android.utils.trackingUrl +import com.google.firebase.messaging.RemoteMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch open class CourierActivity : AppCompatActivity() { @@ -24,9 +28,18 @@ open class CourierActivity : AppCompatActivity() { // Handle delivered messages on the main thread Courier.shared.onPushNotificationEvent { event -> - if (event.trackingEvent == CourierTrackingEvent.DELIVERED) { - val pushNotification = event.remoteMessage.toPushNotification() - onPushNotificationDelivered(pushNotification) + when (event.trackingEvent) { + CourierTrackingEvent.CLICKED -> { + onPushNotificationClicked(event.remoteMessage) + } + CourierTrackingEvent.DELIVERED -> { + onPushNotificationDelivered(event.remoteMessage) + } + CourierTrackingEvent.OPENED, + CourierTrackingEvent.READ, + CourierTrackingEvent.UNREAD -> { + // no-op (intentionally ignored) + } } } @@ -43,14 +56,30 @@ open class CourierActivity : AppCompatActivity() { } private fun checkIntentForPushNotificationClick(intent: Intent?) { - intent?.trackPushNotificationClick { message -> - val pushNotification = message.toPushNotification() - onPushNotificationClicked(pushNotification) + intent?.getRemoteMessage()?.let { remoteMessage -> + CoroutineScope(Dispatchers.IO).launch { + + val trackingEvent = CourierTrackingEvent.CLICKED + + // Broadcast the message + Courier.shared.broadcastPushNotification( + trackingEvent = trackingEvent, + remoteMessage = remoteMessage + ) + + // Track the message + remoteMessage.trackingUrl?.let { trackingUrl -> + Courier.shared.trackPushNotification( + trackingEvent = trackingEvent, + trackingUrl = trackingUrl + ) + } + } } } - open fun onPushNotificationClicked(message: CourierMessage) {} + open fun onPushNotificationClicked(remoteMessage: RemoteMessage) {} - open fun onPushNotificationDelivered(message: CourierMessage) {} + open fun onPushNotificationDelivered(remoteMessage: RemoteMessage) {} } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/models/PushNotification.kt b/android/src/main/java/com/courier/android/models/PushNotification.kt deleted file mode 100644 index b75b1ff1..00000000 --- a/android/src/main/java/com/courier/android/models/PushNotification.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.courier.android.models - -data class CourierMessage( - val title: String?, - val body: String?, -// val data: Map -) \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt b/android/src/main/java/com/courier/android/notifications/CourierIntent.kt index 5a94ac9f..9160f25f 100644 --- a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt +++ b/android/src/main/java/com/courier/android/notifications/CourierIntent.kt @@ -4,7 +4,6 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import com.courier.android.Courier -import com.courier.android.models.CourierMessage import com.google.firebase.messaging.RemoteMessage import com.google.gson.Gson @@ -26,10 +25,10 @@ internal class CourierIntent(private val context: Context, cls: Class<*>?, messa } -internal class CourierPushNotificationIntent(private val context: Context, cls: Class<*>?, message: CourierMessage) : Intent(context, cls) { +internal class CourierPushNotificationIntent(private val context: Context, cls: Class<*>?, remoteMessage: RemoteMessage) : Intent(context, cls) { init { - putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, Gson().toJson(message)) + putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, remoteMessage) addCategory(CATEGORY_LAUNCHER) addFlags(FLAG_ACTIVITY_SINGLE_TOP) action = ACTION_MAIN diff --git a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt index db8225f4..0c3208c8 100644 --- a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt +++ b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt @@ -1,16 +1,11 @@ package com.courier.android.notifications -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.media.RingtoneManager import android.os.Build import androidx.core.app.NotificationCompat -import com.courier.android.activity.CourierActivity -import com.courier.android.models.CourierMessage import com.google.firebase.messaging.RemoteMessage fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { @@ -49,39 +44,4 @@ fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>? } -} - -fun CourierMessage.present(context: Context, target: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { - - try { - - val channelId = "default" - val pendingIntent = CourierPushNotificationIntent(context, target, this).pendingIntent - val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - - val notificationBuilder = NotificationCompat.Builder(context, channelId) - .setSmallIcon(icon) - .setContentTitle(title) - .setContentText(body) - .setAutoCancel(true) - .setSound(defaultSoundUri) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setContentIntent(pendingIntent) - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) - notificationManager.createNotificationChannel(channel) - } - - val uuid = System.currentTimeMillis().toInt() - notificationManager.notify(uuid, notificationBuilder.build()) - - } catch (e: Exception) { - - print(e) - - } - } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierFcmService.kt b/android/src/main/java/com/courier/android/service/CourierFcmService.kt deleted file mode 100644 index 20296d86..00000000 --- a/android/src/main/java/com/courier/android/service/CourierFcmService.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.courier.android.service - -import android.Manifest -import android.annotation.SuppressLint -import android.app.Notification -import android.app.Service -import android.content.BroadcastReceiver -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.os.Bundle -import android.provider.ContactsContract.Intents.Insert.ACTION -import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission -import androidx.core.app.NotificationManagerCompat -import androidx.work.Configuration -import androidx.work.Data -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.OutOfQuotaPolicy -import androidx.work.WorkManager -import androidx.work.workDataOf -import com.courier.android.Courier -import com.courier.android.activity.CourierActivity -import com.courier.android.client.CourierClient -import com.courier.android.models.CourierMessage -import com.courier.android.models.CourierTrackingEvent -import com.courier.android.modules.setFcmToken -import com.courier.android.notifications.present -import com.courier.android.notifications.presentNotification -import com.courier.android.utils.error -import com.courier.android.utils.log -import com.courier.android.utils.toPushNotification -import com.courier.android.utils.trackAndBroadcastTheEvent -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage - -internal class CourierFcmService : FirebaseMessagingService() { - - override fun onCreate() { - super.onCreate() - - // Init the SDK if needed - Courier.initialize(context = this) - - } - - override fun onMessageReceived(message: RemoteMessage) { - super.onMessageReceived(message) - - try { - - // Broadcast the message to the app - // This will allow us to handle when it's delivered - Courier.shared.trackAndBroadcastTheEvent( - trackingEvent = CourierTrackingEvent.DELIVERED, - message = message - ) - - } catch (e: Exception) { - - CourierClient.default.error(e.toString()) - - } - - // Try and show the notification -// val pushNotification = message.toPushNotification() - - val dataBundle = Bundle().apply { - message.data.forEach { (k, v) -> - putString(k, v) - } - } - - val test = dataBundle - - } - - override fun onNewToken(token: String) { - super.onNewToken(token) - - try { - - Courier.shared.setFcmToken( - token = token, - onSuccess = { Courier.shared.client?.log("Courier FCM token updated") }, - onFailure = { Courier.shared.client?.error(it.toString()) } - ) - - } catch (e: Exception) { - - Courier.shared.client?.error(e.toString()) - - } - - } - -} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt b/android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt deleted file mode 100644 index 68952e58..00000000 --- a/android/src/main/java/com/courier/android/service/CourierFirebaseProxy.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.courier.android.service - -import android.content.Intent -import android.os.Bundle -import com.courier.android.Courier -import com.courier.android.client.CourierClient -import com.courier.android.models.CourierTrackingEvent -import com.courier.android.modules.setFcmToken -import com.courier.android.utils.error -import com.courier.android.utils.log -import com.courier.android.utils.trackAndBroadcastTheEvent -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage - -object CourierActions { - const val MESSAGE_RECEIVED = "com.trycourier.MESSAGE_RECEIVED" - const val TOKEN_UPDATED = "com.trycourier.TOKEN_UPDATED" -} - -// Your auto-generated Firebase service (created at runtime) -class CourierFirebaseProxy : FirebaseMessagingService() { - - override fun onMessageReceived(message: RemoteMessage) { - super.onMessageReceived(message) - - try { - - // Broadcast the message to the app - // This will allow us to handle when it's delivered - Courier.shared.trackAndBroadcastTheEvent( - trackingEvent = CourierTrackingEvent.DELIVERED, - message = message - ) - - } catch (e: Exception) { - - CourierClient.default.error(e.toString()) - - } - - // Broadcast to ALL manifest-declared receivers listening for this action - broadcastToManifestReceivers(message) - } - - override fun onNewToken(token: String) { - super.onNewToken(token) - - try { - - Courier.shared.setFcmToken( - token = token, - onSuccess = { Courier.shared.client?.log("Courier FCM token updated") }, - onFailure = { Courier.shared.client?.error(it.toString()) } - ) - - } catch (e: Exception) { - - Courier.shared.client?.error(e.toString()) - - } - - } - - private fun broadcastToManifestReceivers(message: RemoteMessage) { - val intent = Intent(CourierActions.MESSAGE_RECEIVED).apply { - - // Add message data - putExtra("title", message.notification?.title) - putExtra("body", message.notification?.body) - putExtra("from", message.from) - - val dataBundle = Bundle().apply { - message.data.forEach { (key, value) -> putString(key, value) } - } - putExtra("data", dataBundle) - - // CRITICAL: This flag makes it work in killed state - addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) - } - - // Send explicit broadcasts to all receivers that declared this action - sendBroadcastToManifestReceivers(intent) - } - - private fun sendBroadcastToManifestReceivers(intent: Intent) { - // Get all receivers that can handle this intent from the manifest - val packageManager = packageManager - val receivers = packageManager.queryBroadcastReceivers(intent, 0) - - // Send explicit intent to each receiver (works better for killed apps) - receivers.forEach { resolveInfo -> - val explicitIntent = Intent(intent).apply { - setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name) - } - sendBroadcast(explicitIntent) - } - - // Also send the implicit broadcast as backup - sendBroadcast(intent) - } -} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/service/CourierService.kt b/android/src/main/java/com/courier/android/service/CourierService.kt deleted file mode 100644 index d98df928..00000000 --- a/android/src/main/java/com/courier/android/service/CourierService.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.courier.android.service - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Bundle -import com.courier.android.Courier -import com.courier.android.client.CourierClient -import com.courier.android.models.CourierMessage -import com.courier.android.models.CourierTrackingEvent -import com.courier.android.modules.setFcmToken -import com.courier.android.utils.trackAndBroadcastTheEvent -import com.courier.android.utils.error -import com.courier.android.utils.log -import com.courier.android.utils.toPushNotification -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage - -//open class CourierService: FirebaseMessagingService() { -// -// override fun onCreate() { -// super.onCreate() -// -// // Init the SDK if needed -// Courier.initialize(context = this) -// -// } -// -// override fun onMessageReceived(message: RemoteMessage) { -// super.onMessageReceived(message) -// -// try { -// -// // Broadcast the message to the app -// // This will allow us to handle when it's delivered -// Courier.shared.trackAndBroadcastTheEvent( -// trackingEvent = CourierTrackingEvent.DELIVERED, -// message = message -// ) -// -// } catch (e: Exception) { -// -// CourierClient.default.error(e.toString()) -// -// } -// -// // Try and show the notification -// showNotification(message) -// -// } -// -// override fun onNewToken(token: String) { -// super.onNewToken(token) -// -// try { -// -// Courier.shared.setFcmToken( -// token = token, -// onSuccess = { Courier.shared.client?.log("Courier FCM token updated") }, -// onFailure = { Courier.shared.client?.error(it.toString()) } -// ) -// -// } catch (e: Exception) { -// -// Courier.shared.client?.error(e.toString()) -// -// } -// -// } -// -// open fun showNotification(message: RemoteMessage) { -// // Empty -// } -// -//} - -abstract class CourierReceiver : BroadcastReceiver() { - - final override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - CourierActions.MESSAGE_RECEIVED -> { - val courierMessage = CourierMessage( - title = intent.getStringExtra("title"), - body = intent.getStringExtra("body"), -// data = intent.getStringExtra("data") - ) - - onCourierMessage(context, courierMessage) - } - } - } - - // Developer just implements this - abstract fun onCourierMessage(context: Context, message: CourierMessage) -} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/utils/Extensions.kt b/android/src/main/java/com/courier/android/utils/Extensions.kt index 5470e7e8..54195163 100644 --- a/android/src/main/java/com/courier/android/utils/Extensions.kt +++ b/android/src/main/java/com/courier/android/utils/Extensions.kt @@ -17,11 +17,9 @@ import androidx.annotation.ColorInt import androidx.appcompat.widget.SwitchCompat import androidx.recyclerview.widget.RecyclerView import com.courier.android.Courier -import com.courier.android.Courier.Companion.coroutineScope import com.courier.android.Courier.Companion.eventBus import com.courier.android.client.CourierClient import com.courier.android.models.CourierException -import com.courier.android.models.CourierMessage import com.courier.android.models.CourierTrackingEvent import com.courier.android.models.CourierPushNotificationEvent import com.courier.android.models.SemanticProperties @@ -38,17 +36,7 @@ import java.util.Date import java.util.Locale import java.util.TimeZone -internal fun RemoteMessage.toPushNotification(): CourierMessage { - val title = data["title"] ?: notification?.title ?: "Empty Title" - val body = data["body"] ?: notification?.body ?: "Empty Body" - return CourierMessage( - title = title, - body = body, -// data = data - ) -} - -fun Intent.trackPushNotificationClick(onClick: (message: RemoteMessage) -> Unit) { +internal fun Intent.getRemoteMessage(): RemoteMessage? { try { @@ -56,44 +44,35 @@ fun Intent.trackPushNotificationClick(onClick: (message: RemoteMessage) -> Unit) val key = Courier.COURIER_PENDING_NOTIFICATION_KEY @Suppress("DEPRECATION") (extras?.get(key) as? RemoteMessage)?.let { message -> - - // Clear the intent extra extras?.remove(key) - - // Broadcast and track the event - Courier.shared.trackAndBroadcastTheEvent( - trackingEvent = CourierTrackingEvent.CLICKED, - message = message - ) - - onClick(message) - + return message } } catch (e: Exception) { - CourierClient.default.error(e.toString()) - } -} + return null -internal fun Courier.trackAndBroadcastTheEvent(trackingEvent: CourierTrackingEvent, message: RemoteMessage) = Courier.coroutineScope.launch(Dispatchers.IO) { - try { +} - // Track the notification - message.data["trackingUrl"]?.let { trackingUrl -> - coroutineScope.launch(Dispatchers.IO) { - CourierClient.default.tracking.postTrackingUrl( - url = trackingUrl, - event = trackingEvent, - ) - } - } +internal val RemoteMessage.trackingUrl: String? + get() = data["trackingUrl"] - // Broadcast the event - eventBus.onPushNotificationEvent(trackingEvent, message) +internal suspend fun Courier.trackPushNotification(trackingEvent: CourierTrackingEvent, trackingUrl: String) { + try { + CourierClient.default.tracking.postTrackingUrl( + url = trackingUrl, + event = trackingEvent, + ) + } catch (e: Exception) { + Courier.shared.client?.error(e.toString()) + } +} +internal suspend fun Courier.broadcastPushNotification(trackingEvent: CourierTrackingEvent, remoteMessage: RemoteMessage) { + try { + eventBus.onPushNotificationEvent(trackingEvent, remoteMessage) } catch (e: Exception) { Courier.shared.client?.error(e.toString()) } diff --git a/app/build.gradle b/app/build.gradle index 95e3d37b..0329d9ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,5 +98,6 @@ dependencies { // Firebase implementation platform('com.google.firebase:firebase-bom:33.1.2') + implementation "com.google.firebase:firebase-messaging" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e53d5e5..0fde434b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,15 +39,13 @@ - - - + - + diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index abb95f52..5cc6e84e 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -1,29 +1,22 @@ package com.courier.example -import android.annotation.SuppressLint -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build -import android.os.Bundle -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import com.courier.android.models.CourierMessage -import com.courier.android.notifications.present -import com.courier.android.notifications.presentNotification -import com.courier.android.service.CourierReceiver -//import com.courier.android.service.CourierService -//import com.google.firebase.messaging.RemoteMessage +import com.courier.android.Courier +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage + +class ExampleService: FirebaseMessagingService() { + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + Courier.onMessageReceived(message) + Courier.presentNotification(message, this, MainActivity::class.java) + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + Courier.onNewToken(token) + } -// Warning is suppressed -// You do not need to worry about this warning -// The CourierService will handle the function automatically -//@SuppressLint("MissingFirebaseInstanceTokenRefresh") -//class ExampleService: CourierService() { -// // override fun showNotification(message: RemoteMessage) { // super.showNotification(message) // @@ -47,72 +40,5 @@ import com.courier.android.service.CourierReceiver // **/ // // } -// -//} - -//// App module (no Firebase imports) -//class MyCourierReceiver : CourierNotificationBroadcast() { -// -// override fun onCourierMessage( -// context: Context, -// title: String?, -// body: String?, -// data: Map -// ) { -// // Example: post your user-visible notification -// showNotification(context, title ?: "New message", body.orEmpty(), data) -// } -// -// override fun onCourierNewToken(context: Context, token: String) { -// // Upload token to backend, logging, etc. -// } -// -// private fun showNotification( -// ctx: Context, -// title: String, -// body: String, -// data: Map -// ) { -// val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager -// val channelId = "my-courier" -// if (android.os.Build.VERSION.SDK_INT >= 26 && -// nm.getNotificationChannel(channelId) == null -// ) { -// nm.createNotificationChannel( -// android.app.NotificationChannel( -// channelId, "Courier", -// android.app.NotificationManager.IMPORTANCE_HIGH -// ) -// ) -// } -// -// val tap = android.app.PendingIntent.getActivity( -// ctx, -// 0, -// Intent(ctx, MainActivity::class.java).apply { -// flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP -// putExtra("courier_data", HashMap(data)) -// }, -// android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE -// ) -// -// val notif = androidx.core.app.NotificationCompat.Builder(ctx, channelId) -// .setSmallIcon(android.R.drawable.ic_dialog_info) -// .setContentTitle(title) -// .setContentText(body) -// .setAutoCancel(true) -// .setPriority(androidx.core.app.NotificationCompat.PRIORITY_HIGH) -// .setContentIntent(tap) -// .build() -// -// nm.notify(System.currentTimeMillis().toInt(), notif) -// } -//} - -class MyNotificationReceiver: CourierReceiver() { - - override fun onCourierMessage(context: Context, message: CourierMessage) { - message.present(context, MainActivity::class.java) - } } \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/MainActivity.kt b/app/src/main/java/com/courier/example/MainActivity.kt index b2bc399f..7930278d 100644 --- a/app/src/main/java/com/courier/example/MainActivity.kt +++ b/app/src/main/java/com/courier/example/MainActivity.kt @@ -11,7 +11,6 @@ import com.courier.android.activity.CourierActivity import com.courier.android.client.CourierClient import com.courier.android.models.CourierException import com.courier.android.models.CourierInboxListener -import com.courier.android.models.CourierMessage import com.courier.android.modules.addInboxListener import com.courier.android.modules.signIn import com.courier.android.modules.tenantId @@ -21,6 +20,7 @@ import com.courier.example.fragments.AuthFragment import com.courier.example.fragments.InboxFragment import com.courier.example.fragments.PreferencesFragment import com.courier.example.fragments.PushFragment +import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.launch @@ -119,14 +119,14 @@ class MainActivity : CourierActivity() { inboxListener.remove() } - override fun onPushNotificationClicked(message: CourierMessage) { + override fun onPushNotificationClicked(remoteMessage: RemoteMessage) { // Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message clicked:\n${message.title}", Toast.LENGTH_LONG).show() + Toast.makeText(this, "Message clicked:\n${remoteMessage.data}", Toast.LENGTH_LONG).show() } - override fun onPushNotificationDelivered(message: CourierMessage) { + override fun onPushNotificationDelivered(remoteMessage: RemoteMessage) { // Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message delivered:\n${message.title}", Toast.LENGTH_LONG).show() + Toast.makeText(this, "Message delivered:\n${remoteMessage.data}", Toast.LENGTH_LONG).show() } } \ No newline at end of file From 528b6b0604851ebda73c887063e546ade518ac9e Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Fri, 5 Sep 2025 12:07:51 -0700 Subject: [PATCH 05/15] Push delay piece --- .../main/java/com/courier/android/Courier.kt | 6 -- .../android/activity/CourierActivity.kt | 8 +- .../android/notifications/CourierIntent.kt | 34 ++------- .../notifications/RemoteMessageExtensions.kt | 74 +++++++++---------- .../com/courier/example/ExampleService.kt | 47 +++++++++++- 5 files changed, 94 insertions(+), 75 deletions(-) diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index 73775578..e2ada731 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -17,7 +17,6 @@ import com.courier.android.modules.linkInbox import com.courier.android.modules.refreshFcmToken import com.courier.android.modules.setFcmToken import com.courier.android.modules.unlinkInbox -import com.courier.android.notifications.presentNotification import com.courier.android.utils.NotificationEventBus import com.courier.android.utils.broadcastPushNotification import com.courier.android.utils.error @@ -160,10 +159,6 @@ class Courier private constructor(val context: Context) : Application.ActivityLi } } - fun presentNotification(remoteMessage: RemoteMessage, context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { - remoteMessage.presentNotification(context, handlingClass, icon, settingsTitle) - } - fun onNewToken(token: String) { try { shared.setFcmToken( @@ -194,7 +189,6 @@ class Courier private constructor(val context: Context) : Application.ActivityLi // Push internal var tokens: MutableMap = mutableMapOf() - internal var pushHandlerActivity: Class? = null // Stores a local copy of the fcmToken internal var fcmToken: String? = null diff --git a/android/src/main/java/com/courier/android/activity/CourierActivity.kt b/android/src/main/java/com/courier/android/activity/CourierActivity.kt index 67ffebbb..5b92d78d 100644 --- a/android/src/main/java/com/courier/android/activity/CourierActivity.kt +++ b/android/src/main/java/com/courier/android/activity/CourierActivity.kt @@ -14,6 +14,7 @@ import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.delay open class CourierActivity : AppCompatActivity() { @@ -43,11 +44,6 @@ open class CourierActivity : AppCompatActivity() { } } - // Set the current push handler activity - // This is used by the default notification presentation to handle clicks - // Developers can override this if needed - Courier.shared.pushHandlerActivity = this::class.java - } override fun onNewIntent(intent: Intent) { @@ -61,6 +57,8 @@ open class CourierActivity : AppCompatActivity() { val trackingEvent = CourierTrackingEvent.CLICKED + delay(3000) // 3000 ms + // Broadcast the message Courier.shared.broadcastPushNotification( trackingEvent = trackingEvent, diff --git a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt b/android/src/main/java/com/courier/android/notifications/CourierIntent.kt index 9160f25f..ecf87c39 100644 --- a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt +++ b/android/src/main/java/com/courier/android/notifications/CourierIntent.kt @@ -5,9 +5,8 @@ import android.content.Context import android.content.Intent import com.courier.android.Courier import com.google.firebase.messaging.RemoteMessage -import com.google.gson.Gson -internal class CourierIntent(private val context: Context, cls: Class<*>?, message: RemoteMessage) : Intent(context, cls) { +class CourierIntent(private val context: Context, cls: Class<*>?, message: RemoteMessage) : Intent(context, cls) { init { putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, message) @@ -16,29 +15,12 @@ internal class CourierIntent(private val context: Context, cls: Class<*>?, messa action = ACTION_MAIN } - internal val pendingIntent get() = PendingIntent.getActivity( - context, - 0, - this, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) - -} - -internal class CourierPushNotificationIntent(private val context: Context, cls: Class<*>?, remoteMessage: RemoteMessage) : Intent(context, cls) { - - init { - putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, remoteMessage) - addCategory(CATEGORY_LAUNCHER) - addFlags(FLAG_ACTIVITY_SINGLE_TOP) - action = ACTION_MAIN - } - - internal val pendingIntent get() = PendingIntent.getActivity( - context, - 0, - this, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) + val pendingIntent: PendingIntent + get() = PendingIntent.getActivity( + context, + 0, + this, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE + ) } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt index 0c3208c8..6f48ea4f 100644 --- a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt +++ b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt @@ -8,40 +8,40 @@ import android.os.Build import androidx.core.app.NotificationCompat import com.google.firebase.messaging.RemoteMessage -fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { - - try { - - val channelId = "default" - val pendingIntent = CourierIntent(context, handlingClass, this).pendingIntent - val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - - val title = data["title"] ?: notification?.title ?: "Empty Title" - val body = data["body"] ?: notification?.body ?: "Empty Body" - - val notificationBuilder = NotificationCompat.Builder(context, channelId) - .setSmallIcon(icon) - .setContentTitle(title) - .setContentText(body) - .setAutoCancel(true) - .setSound(defaultSoundUri) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setContentIntent(pendingIntent) - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) - notificationManager.createNotificationChannel(channel) - } - - val uuid = System.currentTimeMillis().toInt() - notificationManager.notify(uuid, notificationBuilder.build()) - - } catch (e: Exception) { - - print(e) - - } - -} \ No newline at end of file +//fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { +// +// try { +// +// val channelId = "default" +// val pendingIntent = CourierIntent(context, handlingClass, this).pendingIntent +// val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) +// +// val title = data["title"] ?: notification?.title ?: "Empty Title" +// val body = data["body"] ?: notification?.body ?: "Empty Body" +// +// val notificationBuilder = NotificationCompat.Builder(context, channelId) +// .setSmallIcon(icon) +// .setContentTitle(title) +// .setContentText(body) +// .setAutoCancel(true) +// .setSound(defaultSoundUri) +// .setPriority(NotificationCompat.PRIORITY_MAX) +// .setContentIntent(pendingIntent) +// +// val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager +// +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) +// notificationManager.createNotificationChannel(channel) +// } +// +// val uuid = System.currentTimeMillis().toInt() +// notificationManager.notify(uuid, notificationBuilder.build()) +// +// } catch (e: Exception) { +// +// print(e) +// +// } +// +//} \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index 5cc6e84e..0a7f5860 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -1,6 +1,13 @@ package com.courier.example +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.media.RingtoneManager +import android.os.Build +import androidx.core.app.NotificationCompat import com.courier.android.Courier +import com.courier.android.notifications.CourierIntent import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage @@ -9,7 +16,7 @@ class ExampleService: FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) Courier.onMessageReceived(message) - Courier.presentNotification(message, this, MainActivity::class.java) + message.presentNotification(this, MainActivity::class.java) } override fun onNewToken(token: String) { @@ -41,4 +48,42 @@ class ExampleService: FirebaseMessagingService() { // // } +} + +fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { + + try { + + val channelId = "default" + val pendingIntent = CourierIntent(context, handlingClass, this).pendingIntent + val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + + val title = data["title"] ?: notification?.title ?: "Empty Title" + val body = data["body"] ?: notification?.body ?: "Empty Body" + + val notificationBuilder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(icon) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(pendingIntent) + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) + notificationManager.createNotificationChannel(channel) + } + + val uuid = System.currentTimeMillis().toInt() + notificationManager.notify(uuid, notificationBuilder.build()) + + } catch (e: Exception) { + + print(e) + + } + } \ No newline at end of file From c0211f92728afc37e802ba03abee551be6c653c8 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Fri, 5 Sep 2025 13:56:48 -0700 Subject: [PATCH 06/15] Working --- .../android/activity/CourierActivity.kt | 48 +++------ .../com/courier/android/utils/Extensions.kt | 16 +-- .../com/courier/example/ExampleService.kt | 24 ----- .../main/java/com/courier/example/Extras.kt | 101 +++++++++++++----- .../java/com/courier/example/MainActivity.kt | 14 ++- 5 files changed, 110 insertions(+), 93 deletions(-) diff --git a/android/src/main/java/com/courier/android/activity/CourierActivity.kt b/android/src/main/java/com/courier/android/activity/CourierActivity.kt index 5b92d78d..cf93286f 100644 --- a/android/src/main/java/com/courier/android/activity/CourierActivity.kt +++ b/android/src/main/java/com/courier/android/activity/CourierActivity.kt @@ -3,18 +3,15 @@ package com.courier.android.activity import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.courier.android.Courier import com.courier.android.models.CourierTrackingEvent -import com.courier.android.utils.broadcastPushNotification import com.courier.android.utils.getRemoteMessage import com.courier.android.utils.onPushNotificationEvent import com.courier.android.utils.trackPushNotification import com.courier.android.utils.trackingUrl import com.google.firebase.messaging.RemoteMessage -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.delay open class CourierActivity : AppCompatActivity() { @@ -24,23 +21,16 @@ open class CourierActivity : AppCompatActivity() { // Init Courier if needed Courier.initialize(context = this) - // See if there is a pending click event + // Handle initial push checkIntentForPushNotificationClick(intent) // Handle delivered messages on the main thread Courier.shared.onPushNotificationEvent { event -> when (event.trackingEvent) { - CourierTrackingEvent.CLICKED -> { - onPushNotificationClicked(event.remoteMessage) - } CourierTrackingEvent.DELIVERED -> { onPushNotificationDelivered(event.remoteMessage) } - CourierTrackingEvent.OPENED, - CourierTrackingEvent.READ, - CourierTrackingEvent.UNREAD -> { - // no-op (intentionally ignored) - } + else -> Unit } } @@ -51,29 +41,23 @@ open class CourierActivity : AppCompatActivity() { checkIntentForPushNotificationClick(intent) } - private fun checkIntentForPushNotificationClick(intent: Intent?) { - intent?.getRemoteMessage()?.let { remoteMessage -> - CoroutineScope(Dispatchers.IO).launch { - - val trackingEvent = CourierTrackingEvent.CLICKED + private fun checkIntentForPushNotificationClick(intent: Intent?) = lifecycleScope.launch { - delay(3000) // 3000 ms + // Get the notification and tracking event + val remoteMessage = intent?.getRemoteMessage() ?: return@launch + val trackingEvent = CourierTrackingEvent.CLICKED - // Broadcast the message - Courier.shared.broadcastPushNotification( - trackingEvent = trackingEvent, - remoteMessage = remoteMessage - ) + // Broadcast the message + onPushNotificationClicked(remoteMessage) - // Track the message - remoteMessage.trackingUrl?.let { trackingUrl -> - Courier.shared.trackPushNotification( - trackingEvent = trackingEvent, - trackingUrl = trackingUrl - ) - } - } + // Track the message + remoteMessage.trackingUrl?.let { trackingUrl -> + Courier.shared.trackPushNotification( + trackingEvent = trackingEvent, + trackingUrl = trackingUrl + ) } + } open fun onPushNotificationClicked(remoteMessage: RemoteMessage) {} diff --git a/android/src/main/java/com/courier/android/utils/Extensions.kt b/android/src/main/java/com/courier/android/utils/Extensions.kt index 54195163..b7df551a 100644 --- a/android/src/main/java/com/courier/android/utils/Extensions.kt +++ b/android/src/main/java/com/courier/android/utils/Extensions.kt @@ -70,14 +70,6 @@ internal suspend fun Courier.trackPushNotification(trackingEvent: CourierTrackin } } -internal suspend fun Courier.broadcastPushNotification(trackingEvent: CourierTrackingEvent, remoteMessage: RemoteMessage) { - try { - eventBus.onPushNotificationEvent(trackingEvent, remoteMessage) - } catch (e: Exception) { - Courier.shared.client?.error(e.toString()) - } -} - val RemoteMessage.pushNotification: Map get() { @@ -104,6 +96,14 @@ val RemoteMessage.pushNotification: Map } +internal suspend fun Courier.broadcastPushNotification(trackingEvent: CourierTrackingEvent, remoteMessage: RemoteMessage) { + try { + eventBus.onPushNotificationEvent(trackingEvent, remoteMessage) + } catch (e: Exception) { + Courier.shared.client?.error(e.toString()) + } +} + // Returns the last message that was delivered via the event bus fun Courier.onPushNotificationEvent(onEvent: (event: CourierPushNotificationEvent) -> Unit) = Courier.coroutineScope.launch(Dispatchers.Main) { eventBus.events.collectLatest { diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index 0a7f5860..3390971c 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -24,30 +24,6 @@ class ExampleService: FirebaseMessagingService() { Courier.onNewToken(token) } -// override fun showNotification(message: RemoteMessage) { -// super.showNotification(message) -// -// // This is a simple function you can use, however, -// // it is recommended that you create your own -// // notification and handle it's intent properly -// message.presentNotification( -// context = this, -// handlingClass = MainActivity::class.java, -// ) -// -// // Courier will handle delivery tracking of notifications automatically if you extend your service class with `CourierService()` -// // If you do present a custom notification, you should use this function when the notification is clicked to ensure the status is updated properly -// /** -// Courier.trackNotification( -// message = message, -// event = CourierPushEvent.DELIVERED, -// onSuccess = { Courier.log("Event tracked") }, -// onFailure = { Courier.log(it.toString()) } -// ) -// **/ -// -// } - } fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { diff --git a/app/src/main/java/com/courier/example/Extras.kt b/app/src/main/java/com/courier/example/Extras.kt index f5a9f407..b18f79a6 100644 --- a/app/src/main/java/com/courier/example/Extras.kt +++ b/app/src/main/java/com/courier/example/Extras.kt @@ -1,12 +1,15 @@ package com.courier.example import android.content.Context +import android.graphics.Typeface +import android.view.View import android.widget.EditText import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import com.courier.android.models.CourierPreferenceTopic import com.courier.android.models.InboxAction import com.courier.android.models.InboxMessage +import com.google.firebase.messaging.RemoteMessage //import com.google.firebase.messaging.RemoteMessage import com.google.gson.GsonBuilder import org.json.JSONArray @@ -54,40 +57,88 @@ suspend fun showAlert(context: Context, title: String, subtitle: String? = null, } -//fun RemoteMessage.toJson(): JSONObject { -// val json = JSONObject() -// javaClass.declaredFields.forEach { field -> -// field.isAccessible = true -// val value = field.get(this) -// json.put(field.name, value.toJsonValue()) -// } -// return json -//} -// -//fun RemoteMessage.toJsonString(indentation: Int = 2): String { -// return this.toJson().toString(indentation) -//} +fun showPushNotificationPopup( + context: Context, + title: String, + code: String, + action: String = "OK" +) { + + val alert = AlertDialog.Builder(context) + alert.setTitle(title) + + val editText = EditText(context).apply { + setText(code) + typeface = Typeface.MONOSPACE + isSingleLine = false + isFocusable = false // make it read-only + isClickable = false + setTextIsSelectable(true) // allow copy/select + setHorizontallyScrolling(false) + maxLines = 20 // limit height + scrollBarStyle = View.SCROLLBARS_INSIDE_INSET + } + + val layout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding(50, 40, 50, 10) + addView(editText) + } + + alert.setView(layout) + + alert.setPositiveButton(action) { _, _ -> + + } + + alert.show() +} + +fun RemoteMessage.toJson(): JSONObject { + val json = JSONObject() + javaClass.declaredFields.forEach { field -> + field.isAccessible = true + val value = field.get(this) + json.put(field.name, value.toJsonValue()) + } + return json +} + +fun RemoteMessage.toJsonString(indentation: Int = 2): String { + // Build JSON from the RemoteMessage data map + val json = JSONObject(this.data as Map<*, *>) + + // Add optional top-level fields if you want more than just "data" + json.put("from", this.from) + json.put("messageId", this.messageId) + this.messageType?.let { json.put("messageType", it) } + this.sentTime.takeIf { it > 0 }?.let { json.put("sentTime", it) } + this.ttl.takeIf { it > 0 }?.let { json.put("ttl", it) } + + // Pretty-print with indentation + return json.toString(indentation) +} private fun Any?.toJsonValue(): Any? { return when (this) { -// is RemoteMessage -> toJson() -// is RemoteMessage.Notification -> toJsonObject() -// is Map<*, *> -> toJsonObject() + is RemoteMessage -> toJson() + is RemoteMessage.Notification -> toJsonObject() + is Map<*, *> -> toJsonObject() is List<*> -> toJsonArray() is Array<*> -> toJsonArray() else -> this } } -//fun RemoteMessage.Notification.toJsonObject(): JSONObject { -// val json = JSONObject() -// javaClass.declaredFields.forEach { field -> -// field.isAccessible = true -// val value = field.get(this) -// json.put(field.name, value.toJsonValue()) -// } -// return json -//} +fun RemoteMessage.Notification.toJsonObject(): JSONObject { + val json = JSONObject() + javaClass.declaredFields.forEach { field -> + field.isAccessible = true + val value = field.get(this) + json.put(field.name, value.toJsonValue()) + } + return json +} fun Map<*, *>.toJsonObject(): JSONObject { val json = JSONObject() diff --git a/app/src/main/java/com/courier/example/MainActivity.kt b/app/src/main/java/com/courier/example/MainActivity.kt index 7930278d..52fdf6cb 100644 --- a/app/src/main/java/com/courier/example/MainActivity.kt +++ b/app/src/main/java/com/courier/example/MainActivity.kt @@ -120,13 +120,19 @@ class MainActivity : CourierActivity() { } override fun onPushNotificationClicked(remoteMessage: RemoteMessage) { -// Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message clicked:\n${remoteMessage.data}", Toast.LENGTH_LONG).show() + showPushNotificationPopup( + context = this, + title = "Message clicked", + code = remoteMessage.toJsonString(), + ) } override fun onPushNotificationDelivered(remoteMessage: RemoteMessage) { -// Log.d("Courier", message.toJsonString()) - Toast.makeText(this, "Message delivered:\n${remoteMessage.data}", Toast.LENGTH_LONG).show() + showPushNotificationPopup( + context = this, + title = "Message delivered", + code = remoteMessage.toJsonString(), + ) } } \ No newline at end of file From 7ca40c52c1315548eda093cff8075828099e06d9 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Fri, 5 Sep 2025 16:19:25 -0700 Subject: [PATCH 07/15] Push Notification Service Updated --- .../main/java/com/courier/android/Courier.kt | 3 +- .../android/notifications/CourierIntent.kt | 26 ------- .../CourierPushNotificationIntent.kt | 73 +++++++++++++++++++ .../notifications/RemoteMessageExtensions.kt | 73 +++++++++---------- .../com/courier/example/ExampleService.kt | 69 ++++++------------ .../main/java/com/courier/example/Extras.kt | 16 +++- 6 files changed, 145 insertions(+), 115 deletions(-) delete mode 100644 android/src/main/java/com/courier/android/notifications/CourierIntent.kt create mode 100644 android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index e2ada731..7ae565db 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -134,7 +134,7 @@ class Courier private constructor(val context: Context) : Application.ActivityLi } } - // Push Notification Handlers + // Broadcasts and tracks the message in Courier fun onMessageReceived(remoteMessage: RemoteMessage) = coroutineScope.launch(Dispatchers.IO) { try { @@ -159,6 +159,7 @@ class Courier private constructor(val context: Context) : Application.ActivityLi } } + // Saves the notification token to Courier token management fun onNewToken(token: String) { try { shared.setFcmToken( diff --git a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt b/android/src/main/java/com/courier/android/notifications/CourierIntent.kt deleted file mode 100644 index ecf87c39..00000000 --- a/android/src/main/java/com/courier/android/notifications/CourierIntent.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.courier.android.notifications - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import com.courier.android.Courier -import com.google.firebase.messaging.RemoteMessage - -class CourierIntent(private val context: Context, cls: Class<*>?, message: RemoteMessage) : Intent(context, cls) { - - init { - putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, message) - addCategory(CATEGORY_LAUNCHER) - addFlags(FLAG_ACTIVITY_SINGLE_TOP) - action = ACTION_MAIN - } - - val pendingIntent: PendingIntent - get() = PendingIntent.getActivity( - context, - 0, - this, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) - -} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt b/android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt new file mode 100644 index 00000000..ba972475 --- /dev/null +++ b/android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt @@ -0,0 +1,73 @@ +package com.courier.android.notifications + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Parcelable +import com.courier.android.Courier + +/** + * Convenience intent for launching an Activity from a push notification tap. + * + * This wraps an [Intent] targeted at the given `Activity` and attaches the + * original push payload so your Activity can retrieve it on launch. + * + * What it does: + * - Sets `action = ACTION_MAIN` and adds `CATEGORY_LAUNCHER` so the intent + * behaves like a normal app launch from the launcher. + * - Adds `FLAG_ACTIVITY_SINGLE_TOP` so taps route to the existing Activity + * instance (and trigger `onNewIntent`) instead of creating a new one. + * - Puts the provided `message` into the extras under + * [Courier.COURIER_PENDING_NOTIFICATION_KEY] for later retrieval. + * - Exposes a `pendingIntent` suitable for use in a notification. + * + * Notes: + * - `pendingIntent` uses `FLAG_CANCEL_CURRENT` to replace any previous intent + * with the same request code, and `FLAG_MUTABLE` so extras can be inspected + * on Android 12+. If you do not need to modify the PendingIntent after + * creation, consider `FLAG_IMMUTABLE` for tighter security. + * - Your target Activity should handle the payload in `onCreate` and + * `onNewIntent` to cover both cold-start and warm-start cases. + * + * Example: + * ``` + * val intent = CourierPushNotificationIntent( + * context = context, + * target = MainActivity::class.java, + * message = remoteMessage // Parcelable payload usually an FCM RemoteMessage + * ) + * + * val notification = NotificationCompat.Builder(context, CHANNEL_ID) + * .setContentTitle("New message") + * .setContentText("Tap to view") + * .setContentIntent(intent.pendingIntent) + * .setAutoCancel(true) + * .build() + * ``` + * + * @param context Android context used to create the underlying [Intent] and [PendingIntent]. + * @param target The Activity class to open when the user taps the notification. + * @param payload Parcelable payload attached under [Courier.COURIER_PENDING_NOTIFICATION_KEY]. + * + * @see android.content.Intent.ACTION_MAIN + * @see android.content.Intent.CATEGORY_LAUNCHER + * @see android.app.PendingIntent + */ +class CourierPushNotificationIntent(val context: Context, target: Class<*>?, payload: Parcelable) : Intent(context, target) { + + init { + putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, payload) + addCategory(CATEGORY_LAUNCHER) + addFlags(FLAG_ACTIVITY_SINGLE_TOP) + action = ACTION_MAIN + } + + val pendingIntent: PendingIntent + get() = PendingIntent.getActivity( + context, + 0, + this, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE + ) + +} \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt index 6f48ea4f..2e465971 100644 --- a/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt +++ b/android/src/main/java/com/courier/android/notifications/RemoteMessageExtensions.kt @@ -6,42 +6,37 @@ import android.content.Context import android.media.RingtoneManager import android.os.Build import androidx.core.app.NotificationCompat -import com.google.firebase.messaging.RemoteMessage - -//fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { -// -// try { -// -// val channelId = "default" -// val pendingIntent = CourierIntent(context, handlingClass, this).pendingIntent -// val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) -// -// val title = data["title"] ?: notification?.title ?: "Empty Title" -// val body = data["body"] ?: notification?.body ?: "Empty Body" -// -// val notificationBuilder = NotificationCompat.Builder(context, channelId) -// .setSmallIcon(icon) -// .setContentTitle(title) -// .setContentText(body) -// .setAutoCancel(true) -// .setSound(defaultSoundUri) -// .setPriority(NotificationCompat.PRIORITY_MAX) -// .setContentIntent(pendingIntent) -// -// val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager -// -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -// val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) -// notificationManager.createNotificationChannel(channel) -// } -// -// val uuid = System.currentTimeMillis().toInt() -// notificationManager.notify(uuid, notificationBuilder.build()) -// -// } catch (e: Exception) { -// -// print(e) -// -// } -// -//} \ No newline at end of file + +fun CourierPushNotificationIntent.presentNotification(title: String?, body: String?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { + + try { + + val channelId = "default" + val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + + val notificationBuilder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(icon) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(pendingIntent) + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) + notificationManager.createNotificationChannel(channel) + } + + val uuid = System.currentTimeMillis().toInt() + notificationManager.notify(uuid, notificationBuilder.build()) + + } catch (e: Exception) { + + print(e) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index 3390971c..a8292520 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -1,13 +1,8 @@ package com.courier.example -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.media.RingtoneManager -import android.os.Build -import androidx.core.app.NotificationCompat import com.courier.android.Courier -import com.courier.android.notifications.CourierIntent +import com.courier.android.notifications.CourierPushNotificationIntent +import com.courier.android.notifications.presentNotification import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage @@ -15,50 +10,34 @@ class ExampleService: FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) + + // Notify the Courier SDK that a push was delivered Courier.onMessageReceived(message) - message.presentNotification(this, MainActivity::class.java) + + // Create the PendingIntent that runs when the user taps the notification + // This intent targets your Activity and carries the original message payload + val intent = CourierPushNotificationIntent( + context = this, + target = MainActivity::class.java, + payload = message + ) + + // Show the notification to the user. + // Prefer data-only FCM so this service runs even in background/killed state. + // Fall back to notification fields if data keys are missing. + intent.presentNotification( + title = message.data["title"] ?: message.notification?.title, + body = message.data["body"] ?: message.notification?.body, + ) + } override fun onNewToken(token: String) { super.onNewToken(token) - Courier.onNewToken(token) - } - -} - -fun RemoteMessage.presentNotification(context: Context, handlingClass: Class<*>?, icon: Int = android.R.drawable.ic_dialog_info, settingsTitle: String = "Notification settings") { - - try { - val channelId = "default" - val pendingIntent = CourierIntent(context, handlingClass, this).pendingIntent - val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - - val title = data["title"] ?: notification?.title ?: "Empty Title" - val body = data["body"] ?: notification?.body ?: "Empty Body" - - val notificationBuilder = NotificationCompat.Builder(context, channelId) - .setSmallIcon(icon) - .setContentTitle(title) - .setContentText(body) - .setAutoCancel(true) - .setSound(defaultSoundUri) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setContentIntent(pendingIntent) - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(channelId, settingsTitle, NotificationManager.IMPORTANCE_HIGH) - notificationManager.createNotificationChannel(channel) - } - - val uuid = System.currentTimeMillis().toInt() - notificationManager.notify(uuid, notificationBuilder.build()) - - } catch (e: Exception) { - - print(e) + // Register/refresh this device’s FCM token with Courier. + // The SDK caches and updates the token automatically and links it to the current user. + Courier.onNewToken(token) } diff --git a/app/src/main/java/com/courier/example/Extras.kt b/app/src/main/java/com/courier/example/Extras.kt index b18f79a6..48a605c5 100644 --- a/app/src/main/java/com/courier/example/Extras.kt +++ b/app/src/main/java/com/courier/example/Extras.kt @@ -2,6 +2,7 @@ package com.courier.example import android.content.Context import android.graphics.Typeface +import android.text.InputType import android.view.View import android.widget.EditText import android.widget.LinearLayout @@ -71,11 +72,18 @@ fun showPushNotificationPopup( setText(code) typeface = Typeface.MONOSPACE isSingleLine = false - isFocusable = false // make it read-only - isClickable = false - setTextIsSelectable(true) // allow copy/select + + // Make non-editable but allow selection/copy + keyListener = null // disables typing/paste edits + inputType = InputType.TYPE_NULL // no IME + isCursorVisible = false + isFocusable = false + isFocusableInTouchMode = false + isLongClickable = true + setTextIsSelectable(true) // allow copy/select + setHorizontallyScrolling(false) - maxLines = 20 // limit height + maxLines = 20 scrollBarStyle = View.SCROLLBARS_INSIDE_INSET } From 4c913d4ab61321a3570c3efa928a8912a74fbe87 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Fri, 5 Sep 2025 16:38:24 -0700 Subject: [PATCH 08/15] Removed public firebase params --- android/build.gradle | 4 +- .../main/java/com/courier/android/Courier.kt | 8 ++-- .../android/activity/CourierActivity.kt | 11 +++--- .../models/CourierPushNotificationEvent.kt | 4 +- .../com/courier/android/modules/PushModule.kt | 4 +- .../com/courier/android/utils/Extensions.kt | 39 ++++--------------- .../android/utils/NotificationEventBus.kt | 7 ++-- .../com/courier/example/ExampleService.kt | 2 +- .../main/java/com/courier/example/Extras.kt | 14 +------ .../java/com/courier/example/MainActivity.kt | 8 ++-- 10 files changed, 31 insertions(+), 70 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index c7e338f2..b5318bfd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -63,7 +63,9 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.9.0' implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.google.code.gson:gson:2.11.0' - implementation 'com.google.firebase:firebase-messaging-ktx:23.4.1' + + // Internal usage of FCM api + implementation 'com.google.firebase:firebase-messaging-ktx:24.1.2' // Exportable APIs api 'androidx.recyclerview:recyclerview:1.3.2' diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index 7ae565db..daa0e278 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.Application import android.content.Context -import android.content.Intent import android.os.Build import android.os.Bundle import com.courier.android.client.CourierClient @@ -25,7 +24,6 @@ import com.courier.android.utils.trackPushNotification import com.courier.android.utils.trackingUrl import com.courier.android.utils.warn import com.google.firebase.FirebaseApp -import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -135,7 +133,7 @@ class Courier private constructor(val context: Context) : Application.ActivityLi } // Broadcasts and tracks the message in Courier - fun onMessageReceived(remoteMessage: RemoteMessage) = coroutineScope.launch(Dispatchers.IO) { + fun onMessageReceived(data: Map) = coroutineScope.launch(Dispatchers.IO) { try { val trackingEvent = CourierTrackingEvent.DELIVERED @@ -143,11 +141,11 @@ class Courier private constructor(val context: Context) : Application.ActivityLi // Broadcast the message to the app shared.broadcastPushNotification( trackingEvent = trackingEvent, - remoteMessage = remoteMessage + data = data ) // Track the push notification delivery - remoteMessage.trackingUrl?.let { trackingUrl -> + data.trackingUrl?.let { trackingUrl -> shared.trackPushNotification( trackingEvent = trackingEvent, trackingUrl = trackingUrl diff --git a/android/src/main/java/com/courier/android/activity/CourierActivity.kt b/android/src/main/java/com/courier/android/activity/CourierActivity.kt index cf93286f..6012eaa6 100644 --- a/android/src/main/java/com/courier/android/activity/CourierActivity.kt +++ b/android/src/main/java/com/courier/android/activity/CourierActivity.kt @@ -6,11 +6,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.courier.android.Courier import com.courier.android.models.CourierTrackingEvent -import com.courier.android.utils.getRemoteMessage +import com.courier.android.utils.getPushNotificationData import com.courier.android.utils.onPushNotificationEvent import com.courier.android.utils.trackPushNotification import com.courier.android.utils.trackingUrl -import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.launch open class CourierActivity : AppCompatActivity() { @@ -28,7 +27,7 @@ open class CourierActivity : AppCompatActivity() { Courier.shared.onPushNotificationEvent { event -> when (event.trackingEvent) { CourierTrackingEvent.DELIVERED -> { - onPushNotificationDelivered(event.remoteMessage) + onPushNotificationDelivered(event.data) } else -> Unit } @@ -44,7 +43,7 @@ open class CourierActivity : AppCompatActivity() { private fun checkIntentForPushNotificationClick(intent: Intent?) = lifecycleScope.launch { // Get the notification and tracking event - val remoteMessage = intent?.getRemoteMessage() ?: return@launch + val remoteMessage = intent?.getPushNotificationData() ?: return@launch val trackingEvent = CourierTrackingEvent.CLICKED // Broadcast the message @@ -60,8 +59,8 @@ open class CourierActivity : AppCompatActivity() { } - open fun onPushNotificationClicked(remoteMessage: RemoteMessage) {} + open fun onPushNotificationClicked(pushNotification: Map) {} - open fun onPushNotificationDelivered(remoteMessage: RemoteMessage) {} + open fun onPushNotificationDelivered(pushNotification: Map) {} } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/models/CourierPushNotificationEvent.kt b/android/src/main/java/com/courier/android/models/CourierPushNotificationEvent.kt index 5c142014..2396d2f1 100644 --- a/android/src/main/java/com/courier/android/models/CourierPushNotificationEvent.kt +++ b/android/src/main/java/com/courier/android/models/CourierPushNotificationEvent.kt @@ -1,8 +1,6 @@ package com.courier.android.models -import com.google.firebase.messaging.RemoteMessage - data class CourierPushNotificationEvent( val trackingEvent: CourierTrackingEvent, - val remoteMessage: RemoteMessage + val data: Map ) \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/modules/PushModule.kt b/android/src/main/java/com/courier/android/modules/PushModule.kt index 24d78258..96d215b5 100644 --- a/android/src/main/java/com/courier/android/modules/PushModule.kt +++ b/android/src/main/java/com/courier/android/modules/PushModule.kt @@ -12,8 +12,8 @@ import com.courier.android.models.CourierException import com.courier.android.models.CourierPushProvider import com.courier.android.utils.error import com.courier.android.utils.log -import com.google.firebase.ktx.Firebase -import com.google.firebase.messaging.ktx.messaging +import com.google.firebase.Firebase +import com.google.firebase.messaging.messaging import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await diff --git a/android/src/main/java/com/courier/android/utils/Extensions.kt b/android/src/main/java/com/courier/android/utils/Extensions.kt index b7df551a..04b04429 100644 --- a/android/src/main/java/com/courier/android/utils/Extensions.kt +++ b/android/src/main/java/com/courier/android/utils/Extensions.kt @@ -9,6 +9,7 @@ import android.content.res.Resources import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.net.Uri +import android.os.Parcelable import android.util.TypedValue import android.view.View import android.widget.Button @@ -36,7 +37,7 @@ import java.util.Date import java.util.Locale import java.util.TimeZone -internal fun Intent.getRemoteMessage(): RemoteMessage? { +internal fun Intent.getPushNotificationData(): Map? { try { @@ -45,7 +46,7 @@ internal fun Intent.getRemoteMessage(): RemoteMessage? { @Suppress("DEPRECATION") (extras?.get(key) as? RemoteMessage)?.let { message -> extras?.remove(key) - return message + return message.data } } catch (e: Exception) { @@ -56,8 +57,8 @@ internal fun Intent.getRemoteMessage(): RemoteMessage? { } -internal val RemoteMessage.trackingUrl: String? - get() = data["trackingUrl"] +internal val Map.trackingUrl: String? + get() = this["trackingUrl"] internal suspend fun Courier.trackPushNotification(trackingEvent: CourierTrackingEvent, trackingUrl: String) { try { @@ -70,35 +71,9 @@ internal suspend fun Courier.trackPushNotification(trackingEvent: CourierTrackin } } -val RemoteMessage.pushNotification: Map - get() { - - val rawData = data.toMutableMap() - val payload = mutableMapOf() - - // Add existing values to base map - // then remove the unneeded keys - val baseKeys = listOf("title", "subtitle", "body", "badge", "sound") - baseKeys.forEach { key -> - payload[key] = data[key] - rawData.remove(key) - } - - // Add extras - for ((key, value) in rawData) { - payload[key] = value - } - - // Add the raw data - payload["raw"] = data - - return payload - - } - -internal suspend fun Courier.broadcastPushNotification(trackingEvent: CourierTrackingEvent, remoteMessage: RemoteMessage) { +internal suspend fun Courier.broadcastPushNotification(trackingEvent: CourierTrackingEvent, data: Map) { try { - eventBus.onPushNotificationEvent(trackingEvent, remoteMessage) + eventBus.onPushNotificationEvent(trackingEvent, data) } catch (e: Exception) { Courier.shared.client?.error(e.toString()) } diff --git a/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt b/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt index 9b354673..49d7a65a 100644 --- a/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt +++ b/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt @@ -3,7 +3,6 @@ package com.courier.android.utils import android.util.Log import com.courier.android.models.CourierTrackingEvent import com.courier.android.models.CourierPushNotificationEvent -import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -16,9 +15,9 @@ class NotificationEventBus { private val _events = MutableSharedFlow() val events = _events.asSharedFlow() - suspend fun onPushNotificationEvent(trackingEvent: CourierTrackingEvent, message: RemoteMessage) { - Log.d(TAG, "onPushNotificationEvent: $trackingEvent = $message") - val event = CourierPushNotificationEvent(trackingEvent, message) + suspend fun onPushNotificationEvent(trackingEvent: CourierTrackingEvent, data: Map) { + Log.d(TAG, "onPushNotificationEvent: $trackingEvent = $data") + val event = CourierPushNotificationEvent(trackingEvent, data) _events.emit(event) } diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index a8292520..a4820481 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -12,7 +12,7 @@ class ExampleService: FirebaseMessagingService() { super.onMessageReceived(message) // Notify the Courier SDK that a push was delivered - Courier.onMessageReceived(message) + Courier.onMessageReceived(message.data) // Create the PendingIntent that runs when the user taps the notification // This intent targets your Activity and carries the original message payload diff --git a/app/src/main/java/com/courier/example/Extras.kt b/app/src/main/java/com/courier/example/Extras.kt index 48a605c5..3d4c449f 100644 --- a/app/src/main/java/com/courier/example/Extras.kt +++ b/app/src/main/java/com/courier/example/Extras.kt @@ -112,18 +112,8 @@ fun RemoteMessage.toJson(): JSONObject { return json } -fun RemoteMessage.toJsonString(indentation: Int = 2): String { - // Build JSON from the RemoteMessage data map - val json = JSONObject(this.data as Map<*, *>) - - // Add optional top-level fields if you want more than just "data" - json.put("from", this.from) - json.put("messageId", this.messageId) - this.messageType?.let { json.put("messageType", it) } - this.sentTime.takeIf { it > 0 }?.let { json.put("sentTime", it) } - this.ttl.takeIf { it > 0 }?.let { json.put("ttl", it) } - - // Pretty-print with indentation +fun Map.toJsonString(indentation: Int = 2): String { + val json = JSONObject(this) return json.toString(indentation) } diff --git a/app/src/main/java/com/courier/example/MainActivity.kt b/app/src/main/java/com/courier/example/MainActivity.kt index 52fdf6cb..79131bdc 100644 --- a/app/src/main/java/com/courier/example/MainActivity.kt +++ b/app/src/main/java/com/courier/example/MainActivity.kt @@ -119,19 +119,19 @@ class MainActivity : CourierActivity() { inboxListener.remove() } - override fun onPushNotificationClicked(remoteMessage: RemoteMessage) { + override fun onPushNotificationClicked(pushNotification: Map) { showPushNotificationPopup( context = this, title = "Message clicked", - code = remoteMessage.toJsonString(), + code = pushNotification.toJsonString(), ) } - override fun onPushNotificationDelivered(remoteMessage: RemoteMessage) { + override fun onPushNotificationDelivered(pushNotification: Map) { showPushNotificationPopup( context = this, title = "Message delivered", - code = remoteMessage.toJsonString(), + code = pushNotification.toJsonString(), ) } From 00c2628b89bd18be4747512af1b532067679b64a Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 8 Sep 2025 09:48:43 -0700 Subject: [PATCH 09/15] Polishing intent --- .../src/main/java/com/courier/android/Courier.kt | 4 ++-- .../com/courier/android/activity/CourierActivity.kt | 4 ++-- .../java/com/courier/android/utils/Extensions.kt | 13 ++++++++----- .../main/java/com/courier/example/ExampleService.kt | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index daa0e278..d34e1b86 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -139,14 +139,14 @@ class Courier private constructor(val context: Context) : Application.ActivityLi val trackingEvent = CourierTrackingEvent.DELIVERED // Broadcast the message to the app - shared.broadcastPushNotification( + broadcastPushNotification( trackingEvent = trackingEvent, data = data ) // Track the push notification delivery data.trackingUrl?.let { trackingUrl -> - shared.trackPushNotification( + trackPushNotification( trackingEvent = trackingEvent, trackingUrl = trackingUrl ) diff --git a/android/src/main/java/com/courier/android/activity/CourierActivity.kt b/android/src/main/java/com/courier/android/activity/CourierActivity.kt index 6012eaa6..0b833eb9 100644 --- a/android/src/main/java/com/courier/android/activity/CourierActivity.kt +++ b/android/src/main/java/com/courier/android/activity/CourierActivity.kt @@ -24,7 +24,7 @@ open class CourierActivity : AppCompatActivity() { checkIntentForPushNotificationClick(intent) // Handle delivered messages on the main thread - Courier.shared.onPushNotificationEvent { event -> + onPushNotificationEvent { event -> when (event.trackingEvent) { CourierTrackingEvent.DELIVERED -> { onPushNotificationDelivered(event.data) @@ -51,7 +51,7 @@ open class CourierActivity : AppCompatActivity() { // Track the message remoteMessage.trackingUrl?.let { trackingUrl -> - Courier.shared.trackPushNotification( + trackPushNotification( trackingEvent = trackingEvent, trackingUrl = trackingUrl ) diff --git a/android/src/main/java/com/courier/android/utils/Extensions.kt b/android/src/main/java/com/courier/android/utils/Extensions.kt index 04b04429..0c81eef5 100644 --- a/android/src/main/java/com/courier/android/utils/Extensions.kt +++ b/android/src/main/java/com/courier/android/utils/Extensions.kt @@ -9,7 +9,6 @@ import android.content.res.Resources import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.net.Uri -import android.os.Parcelable import android.util.TypedValue import android.view.View import android.widget.Button @@ -36,6 +35,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import androidx.core.net.toUri internal fun Intent.getPushNotificationData(): Map? { @@ -60,7 +60,7 @@ internal fun Intent.getPushNotificationData(): Map? { internal val Map.trackingUrl: String? get() = this["trackingUrl"] -internal suspend fun Courier.trackPushNotification(trackingEvent: CourierTrackingEvent, trackingUrl: String) { +internal suspend fun trackPushNotification(trackingEvent: CourierTrackingEvent, trackingUrl: String) { try { CourierClient.default.tracking.postTrackingUrl( url = trackingUrl, @@ -71,7 +71,10 @@ internal suspend fun Courier.trackPushNotification(trackingEvent: CourierTrackin } } -internal suspend fun Courier.broadcastPushNotification(trackingEvent: CourierTrackingEvent, data: Map) { +internal suspend fun broadcastPushNotification( + trackingEvent: CourierTrackingEvent, + data: Map +) { try { eventBus.onPushNotificationEvent(trackingEvent, data) } catch (e: Exception) { @@ -80,7 +83,7 @@ internal suspend fun Courier.broadcastPushNotification(trackingEvent: CourierTra } // Returns the last message that was delivered via the event bus -fun Courier.onPushNotificationEvent(onEvent: (event: CourierPushNotificationEvent) -> Unit) = Courier.coroutineScope.launch(Dispatchers.Main) { +fun onPushNotificationEvent(onEvent: (event: CourierPushNotificationEvent) -> Unit) = Courier.coroutineScope.launch(Dispatchers.Main) { eventBus.events.collectLatest { onEvent(it) } @@ -183,7 +186,7 @@ internal fun TextView.setCourierFont(font: CourierStyles.Font?, @ColorInt fallba } internal fun Context.launchCourierWebsite() { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.courier.com/")) + val browserIntent = Intent(Intent.ACTION_VIEW, "https://www.courier.com/".toUri()) startActivity(browserIntent) } diff --git a/app/src/main/java/com/courier/example/ExampleService.kt b/app/src/main/java/com/courier/example/ExampleService.kt index a4820481..01c7c8aa 100644 --- a/app/src/main/java/com/courier/example/ExampleService.kt +++ b/app/src/main/java/com/courier/example/ExampleService.kt @@ -16,7 +16,7 @@ class ExampleService: FirebaseMessagingService() { // Create the PendingIntent that runs when the user taps the notification // This intent targets your Activity and carries the original message payload - val intent = CourierPushNotificationIntent( + val notificationIntent = CourierPushNotificationIntent( context = this, target = MainActivity::class.java, payload = message @@ -25,7 +25,7 @@ class ExampleService: FirebaseMessagingService() { // Show the notification to the user. // Prefer data-only FCM so this service runs even in background/killed state. // Fall back to notification fields if data keys are missing. - intent.presentNotification( + notificationIntent.presentNotification( title = message.data["title"] ?: message.notification?.title, body = message.data["body"] ?: message.notification?.body, ) From e3092cbfd384c64d3fd8a10a90d07220e6af4492 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 8 Sep 2025 10:33:58 -0700 Subject: [PATCH 10/15] Polish --- .../src/main/java/com/courier/android/Courier.kt | 12 +++++++++--- .../courier/android/activity/CourierActivity.kt | 3 +-- .../android/models/CourierTrackingEvent.kt | 3 --- .../com/courier/android/modules/PushModule.kt | 4 ++++ .../CourierPushNotificationIntent.kt | 15 +++++++++------ .../java/com/courier/android/utils/Extensions.kt | 15 ++------------- .../courier/android/utils/NotificationEventBus.kt | 8 ++------ .../main/java/com/courier/example/MainActivity.kt | 3 +-- 8 files changed, 28 insertions(+), 35 deletions(-) diff --git a/android/src/main/java/com/courier/android/Courier.kt b/android/src/main/java/com/courier/android/Courier.kt index d34e1b86..f25b5eb8 100644 --- a/android/src/main/java/com/courier/android/Courier.kt +++ b/android/src/main/java/com/courier/android/Courier.kt @@ -10,6 +10,7 @@ import com.courier.android.client.CourierClient import com.courier.android.models.CourierAgent import com.courier.android.models.CourierAuthenticationListener import com.courier.android.models.CourierException +import com.courier.android.models.CourierPushNotificationEvent import com.courier.android.models.CourierTrackingEvent import com.courier.android.modules.InboxModule import com.courier.android.modules.linkInbox @@ -28,6 +29,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** @@ -63,9 +65,6 @@ class Courier private constructor(val context: Context) : Application.ActivityLi // Inbox private const val SOCKET_CLEANUP_DURATION = 60_000L * 10 // 10 minutes - // Push - internal const val COURIER_PENDING_NOTIFICATION_KEY = "courier_pending_notification_key" - // Eventing val eventBus by lazy { NotificationEventBus() } @@ -170,6 +169,13 @@ class Courier private constructor(val context: Context) : Application.ActivityLi } } + // Returns the last message that was delivered via the event bus + fun onPushNotificationEvent(onEvent: (event: CourierPushNotificationEvent) -> Unit) = coroutineScope.launch(Dispatchers.Main) { + eventBus.events.collectLatest { + onEvent(it) + } + } + } // Inbox diff --git a/android/src/main/java/com/courier/android/activity/CourierActivity.kt b/android/src/main/java/com/courier/android/activity/CourierActivity.kt index 0b833eb9..21103ed3 100644 --- a/android/src/main/java/com/courier/android/activity/CourierActivity.kt +++ b/android/src/main/java/com/courier/android/activity/CourierActivity.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.lifecycleScope import com.courier.android.Courier import com.courier.android.models.CourierTrackingEvent import com.courier.android.utils.getPushNotificationData -import com.courier.android.utils.onPushNotificationEvent import com.courier.android.utils.trackPushNotification import com.courier.android.utils.trackingUrl import kotlinx.coroutines.launch @@ -24,7 +23,7 @@ open class CourierActivity : AppCompatActivity() { checkIntentForPushNotificationClick(intent) // Handle delivered messages on the main thread - onPushNotificationEvent { event -> + Courier.onPushNotificationEvent { event -> when (event.trackingEvent) { CourierTrackingEvent.DELIVERED -> { onPushNotificationDelivered(event.data) diff --git a/android/src/main/java/com/courier/android/models/CourierTrackingEvent.kt b/android/src/main/java/com/courier/android/models/CourierTrackingEvent.kt index c510cb0a..3e2ff5fe 100644 --- a/android/src/main/java/com/courier/android/models/CourierTrackingEvent.kt +++ b/android/src/main/java/com/courier/android/models/CourierTrackingEvent.kt @@ -3,7 +3,4 @@ package com.courier.android.models enum class CourierTrackingEvent(val value: String) { CLICKED("CLICKED"), DELIVERED("DELIVERED"), - OPENED("OPENED"), - READ("READ"), - UNREAD("UNREAD") } \ No newline at end of file diff --git a/android/src/main/java/com/courier/android/modules/PushModule.kt b/android/src/main/java/com/courier/android/modules/PushModule.kt index 96d215b5..01003ce2 100644 --- a/android/src/main/java/com/courier/android/modules/PushModule.kt +++ b/android/src/main/java/com/courier/android/modules/PushModule.kt @@ -254,6 +254,10 @@ fun Courier.setToken(provider: CourierPushProvider, token: String, onSuccess: () } } +/** + * Permission Handlers + */ + fun Courier.requestNotificationPermission(activity: Activity, requestCode: Int = 1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), requestCode) diff --git a/android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt b/android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt index ba972475..6f616843 100644 --- a/android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt +++ b/android/src/main/java/com/courier/android/notifications/CourierPushNotificationIntent.kt @@ -4,7 +4,6 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Parcelable -import com.courier.android.Courier /** * Convenience intent for launching an Activity from a push notification tap. @@ -18,7 +17,7 @@ import com.courier.android.Courier * - Adds `FLAG_ACTIVITY_SINGLE_TOP` so taps route to the existing Activity * instance (and trigger `onNewIntent`) instead of creating a new one. * - Puts the provided `message` into the extras under - * [Courier.COURIER_PENDING_NOTIFICATION_KEY] for later retrieval. + * [CourierPushNotificationIntent.COURIER_PENDING_NOTIFICATION_KEY] for later retrieval. * - Exposes a `pendingIntent` suitable for use in a notification. * * Notes: @@ -47,16 +46,20 @@ import com.courier.android.Courier * * @param context Android context used to create the underlying [Intent] and [PendingIntent]. * @param target The Activity class to open when the user taps the notification. - * @param payload Parcelable payload attached under [Courier.COURIER_PENDING_NOTIFICATION_KEY]. + * @param payload Parcelable payload attached under [CourierPushNotificationIntent.COURIER_PENDING_NOTIFICATION_KEY]. * * @see android.content.Intent.ACTION_MAIN * @see android.content.Intent.CATEGORY_LAUNCHER * @see android.app.PendingIntent */ -class CourierPushNotificationIntent(val context: Context, target: Class<*>?, payload: Parcelable) : Intent(context, target) { +class CourierPushNotificationIntent(val context: Context, private val requestCode: Int = 0, target: Class<*>?, payload: Parcelable) : Intent(context, target) { + + internal companion object { + const val COURIER_PENDING_NOTIFICATION_KEY = "courier_pending_push_notification" + } init { - putExtra(Courier.COURIER_PENDING_NOTIFICATION_KEY, payload) + putExtra(COURIER_PENDING_NOTIFICATION_KEY, payload) addCategory(CATEGORY_LAUNCHER) addFlags(FLAG_ACTIVITY_SINGLE_TOP) action = ACTION_MAIN @@ -65,7 +68,7 @@ class CourierPushNotificationIntent(val context: Context, target: Class<*>?, pay val pendingIntent: PendingIntent get() = PendingIntent.getActivity( context, - 0, + requestCode, this, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE ) diff --git a/android/src/main/java/com/courier/android/utils/Extensions.kt b/android/src/main/java/com/courier/android/utils/Extensions.kt index 0c81eef5..cf8fc886 100644 --- a/android/src/main/java/com/courier/android/utils/Extensions.kt +++ b/android/src/main/java/com/courier/android/utils/Extensions.kt @@ -8,7 +8,6 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Resources import android.graphics.Typeface import android.graphics.drawable.ColorDrawable -import android.net.Uri import android.util.TypedValue import android.view.View import android.widget.Button @@ -21,28 +20,25 @@ import com.courier.android.Courier.Companion.eventBus import com.courier.android.client.CourierClient import com.courier.android.models.CourierException import com.courier.android.models.CourierTrackingEvent -import com.courier.android.models.CourierPushNotificationEvent import com.courier.android.models.SemanticProperties import com.courier.android.models.SemanticProperty import com.courier.android.models.toJson import com.courier.android.ui.CourierStyles import com.courier.android.ui.inbox.BadgeTextView import com.google.firebase.messaging.RemoteMessage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone import androidx.core.net.toUri +import com.courier.android.notifications.CourierPushNotificationIntent internal fun Intent.getPushNotificationData(): Map? { try { // Check to see if we have an intent to work - val key = Courier.COURIER_PENDING_NOTIFICATION_KEY + val key = CourierPushNotificationIntent.COURIER_PENDING_NOTIFICATION_KEY @Suppress("DEPRECATION") (extras?.get(key) as? RemoteMessage)?.let { message -> extras?.remove(key) @@ -82,13 +78,6 @@ internal suspend fun broadcastPushNotification( } } -// Returns the last message that was delivered via the event bus -fun onPushNotificationEvent(onEvent: (event: CourierPushNotificationEvent) -> Unit) = Courier.coroutineScope.launch(Dispatchers.Main) { - eventBus.events.collectLatest { - onEvent(it) - } -} - internal fun String.isoToDate(): Date? { val isoFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) isoFormat.timeZone = TimeZone.getTimeZone("UTC") diff --git a/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt b/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt index 49d7a65a..30d7e967 100644 --- a/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt +++ b/android/src/main/java/com/courier/android/utils/NotificationEventBus.kt @@ -1,6 +1,6 @@ package com.courier.android.utils -import android.util.Log +import com.courier.android.Courier import com.courier.android.models.CourierTrackingEvent import com.courier.android.models.CourierPushNotificationEvent import kotlinx.coroutines.flow.MutableSharedFlow @@ -8,15 +8,11 @@ import kotlinx.coroutines.flow.asSharedFlow class NotificationEventBus { - companion object { - private const val TAG = "EventBus" - } - private val _events = MutableSharedFlow() val events = _events.asSharedFlow() suspend fun onPushNotificationEvent(trackingEvent: CourierTrackingEvent, data: Map) { - Log.d(TAG, "onPushNotificationEvent: $trackingEvent = $data") + Courier.shared.client?.log("onPushNotificationEvent: $trackingEvent = $data") val event = CourierPushNotificationEvent(trackingEvent, data) _events.emit(event) } diff --git a/app/src/main/java/com/courier/example/MainActivity.kt b/app/src/main/java/com/courier/example/MainActivity.kt index 79131bdc..f5fbbb71 100644 --- a/app/src/main/java/com/courier/example/MainActivity.kt +++ b/app/src/main/java/com/courier/example/MainActivity.kt @@ -1,7 +1,6 @@ package com.courier.example import android.os.Bundle -import android.util.Log import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -11,6 +10,7 @@ import com.courier.android.activity.CourierActivity import com.courier.android.client.CourierClient import com.courier.android.models.CourierException import com.courier.android.models.CourierInboxListener +import com.courier.android.models.CourierTrackingEvent import com.courier.android.modules.addInboxListener import com.courier.android.modules.signIn import com.courier.android.modules.tenantId @@ -20,7 +20,6 @@ import com.courier.example.fragments.AuthFragment import com.courier.example.fragments.InboxFragment import com.courier.example.fragments.PreferencesFragment import com.courier.example.fragments.PushFragment -import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.launch From eb27460ea493294f9f35c34bbb573edb7d58d2f4 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 8 Sep 2025 10:42:29 -0700 Subject: [PATCH 11/15] Doc updates --- Docs/PushNotifications.md | 77 ++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/Docs/PushNotifications.md b/Docs/PushNotifications.md index e24e6128..89b90580 100644 --- a/Docs/PushNotifications.md +++ b/Docs/PushNotifications.md @@ -29,7 +29,7 @@ The easiest way to support push notifications in your app. - Manual Token Managment + Manual Token Management @@ -197,36 +197,75 @@ Select which push notification provider you would like Courier to route push not To track new push notifications when they arrive and to automatically sync push notification tokens, create a new file, name it what you'd like and paste the following code in it. (Kotlin example shown below) +### 1. Add the Firebase Messaging Dependency to your `app/build.gradle` + +```gradle +.. +dependencies { + + .. + + // Firebase + implementation platform('com.google.firebase:firebase-bom:XXXX') + implementation "com.google.firebase:firebase-messaging" + +} +``` + +You can find more details in the official Firebase documentation: + +🔗 [Set up Firebase for Android](https://firebase.google.com/docs/android/setup) + +### 2. Create a new Service File to handle push notification delivery and tokens + ```kotlin -import android.annotation.SuppressLint +package your.app.package + +import com.courier.android.Courier +import com.courier.android.notifications.CourierPushNotificationIntent import com.courier.android.notifications.presentNotification -import com.courier.android.service.CourierService +import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -// This is safe. `CourierService` will automatically handle token refreshes. -@SuppressLint("MissingFirebaseInstanceTokenRefresh") -class YourNotificationService: CourierService() { +class CourierPushNotificationService: FirebaseMessagingService() { - override fun showNotification(message: RemoteMessage) { - super.showNotification(message) + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) - // TODO: This is where you will customize the notification that is shown to your users - // `message.presentNotification(...)` is used to get started quickly and not for production use. - // More information on how to customize an Android notification here: - // https://developer.android.com/develop/ui/views/notifications/build-notification + // Notify the Courier SDK that a push was delivered + Courier.onMessageReceived(message.data) - message.presentNotification( + // Create the PendingIntent that runs when the user taps the notification + // This intent targets your Activity and carries the original message payload + val notificationIntent = CourierPushNotificationIntent( context = this, - handlingClass = MainActivity::class.java, - icon = android.R.drawable.ic_dialog_info + target = MainActivity::class.java, + payload = message + ) + + // Show the notification to the user. + // Prefer data-only FCM so this service runs even in background/killed state. + // Fall back to notification fields if data keys are missing. + notificationIntent.presentNotification( + title = message.data["title"] ?: message.notification?.title, + body = message.data["body"] ?: message.notification?.body, ) } + override fun onNewToken(token: String) { + super.onNewToken(token) + + // Register/refresh this device’s FCM token with Courier. + // The SDK caches and updates the token automatically and links it to the current user. + Courier.onNewToken(token) + + } + } ``` -Next, add the `CourierService` entry in your `AndroidManifest.xml` file +### 3. Register the `CourierService` in your `AndroidManifest.xml` file ```xml @@ -238,7 +277,7 @@ Next, add the `CourierService` entry in your `AndroidManifest.xml` file // Add this 👇 @@ -252,7 +291,9 @@ Next, add the `CourierService` entry in your `AndroidManifest.xml` file ``` -Finally, add the `CourierActivity`. This will allow you to handle when users get push notifications delivered and when they click on the push notifications. You will likely want to extend your `MainActivity` but your use case may be slightly different. +### 4. Add the `CourierActivity` + +This will allow you to handle when users get push notifications delivered and when they click on the push notifications. You will likely want to extend your `MainActivity` but your use case may be slightly different. ```kotlin class MainActivity : CourierActivity() { From 402859cab5baff2c9413873d3575b88378d27ab5 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 8 Sep 2025 10:52:30 -0700 Subject: [PATCH 12/15] Dependency cleanup --- android/build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index b5318bfd..83c95367 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -60,11 +60,8 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' - implementation 'androidx.work:work-runtime-ktx:2.9.0' implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.google.code.gson:gson:2.11.0' - - // Internal usage of FCM api implementation 'com.google.firebase:firebase-messaging-ktx:24.1.2' // Exportable APIs From 9404ef7731aa1186cf8bd388c00df68ae70fae8d Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 8 Sep 2025 10:58:28 -0700 Subject: [PATCH 13/15] Service --- Docs/PushNotifications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/PushNotifications.md b/Docs/PushNotifications.md index 89b90580..cd1e32ad 100644 --- a/Docs/PushNotifications.md +++ b/Docs/PushNotifications.md @@ -265,7 +265,7 @@ class CourierPushNotificationService: FirebaseMessagingService() { } ``` -### 3. Register the `CourierService` in your `AndroidManifest.xml` file +### 3. Register the `Service` in your `AndroidManifest.xml` file ```xml From 5f2f98cd187c854823be74bf0eb187320bc7998f Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 8 Sep 2025 11:01:17 -0700 Subject: [PATCH 14/15] Activity docs changes --- Docs/PushNotifications.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Docs/PushNotifications.md b/Docs/PushNotifications.md index cd1e32ad..0dfa357d 100644 --- a/Docs/PushNotifications.md +++ b/Docs/PushNotifications.md @@ -300,12 +300,12 @@ class MainActivity : CourierActivity() { .. - override fun onPushNotificationClicked(message: RemoteMessage) { - Toast.makeText(this, "Message clicked:\n${message.data}", Toast.LENGTH_LONG).show() + override fun onPushNotificationDelivered(pushNotification: Map) { + Toast.makeText(this, "Message delivered:\n${pushNotification}", Toast.LENGTH_LONG).show() } - - override fun onPushNotificationDelivered(message: RemoteMessage) { - Toast.makeText(this, "Message delivered:\n${message.data}", Toast.LENGTH_LONG).show() + + override fun onPushNotificationClicked(pushNotification: Map) { + Toast.makeText(this, "Message clicked:\n${pushNotification}", Toast.LENGTH_LONG).show() } } From 5bba1b366e369fe0b0ca2598afcb70627b1f0bc3 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Mon, 8 Sep 2025 11:02:36 -0700 Subject: [PATCH 15/15] # --- Docs/PushNotifications.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Docs/PushNotifications.md b/Docs/PushNotifications.md index 0dfa357d..b0bd0478 100644 --- a/Docs/PushNotifications.md +++ b/Docs/PushNotifications.md @@ -193,7 +193,7 @@ Select which push notification provider you would like Courier to route push not ## 2. Sync Push Notification Tokens -### Automatic Token Syncing (FCM Only) +## Automatic Token Syncing (FCM Only) To track new push notifications when they arrive and to automatically sync push notification tokens, create a new file, name it what you'd like and paste the following code in it. (Kotlin example shown below) @@ -311,7 +311,7 @@ class MainActivity : CourierActivity() { } ``` -### Manual Token Syncing +## Manual Token Syncing If you do not want to use `CourierService` and `CourierActivity`, you can manually sync push notification tokens with the following code. @@ -340,7 +340,7 @@ lifecycleScope.launch { } ``` -### Manual Notification Tracking +## Manual Notification Tracking This is how you can tell Courier when a notification has been delivered or clicked in your app. If you are using `CourierService` and `CourierActivity`, this is done automatically.