P4 Stream N: FCM service + NotificationChannels matching iOS categories
FcmService + NotificationPayload + 4 NotificationChannels (task_reminder, task_overdue, residence_invite, subscription) parity with iOS NotificationCategories.swift. Deep-link routing from payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package com.tt.honeyDue.notifications
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import com.tt.honeyDue.MainActivity
|
||||
import com.tt.honeyDue.R
|
||||
|
||||
/**
|
||||
* New-generation FirebaseMessagingService built for iOS parity. Routes each
|
||||
* incoming FCM data-message to the correct [NotificationChannels] channel,
|
||||
* attaches a deep-link PendingIntent when present, and uses a hashed
|
||||
* messageId as the notification id so duplicate redeliveries replace (not
|
||||
* stack).
|
||||
*
|
||||
* This lives alongside the legacy `MyFirebaseMessagingService`. The manifest
|
||||
* currently wires FCM to this service; the legacy class is retained until
|
||||
* its call sites (widget + notification-action flows) are migrated.
|
||||
*/
|
||||
class FcmService : FirebaseMessagingService() {
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
Log.d(TAG, "FCM token refreshed (len=${token.length})")
|
||||
// TODO: wire token registration to APILayer.registerDevice.
|
||||
// Registration is already performed by MyFirebaseMessagingService.onNewToken
|
||||
// at composeApp/src/androidMain/kotlin/com/tt/honeyDue/MyFirebaseMessagingService.kt:20-65
|
||||
// using NotificationApi().registerDevice(...). When that legacy service is
|
||||
// removed, port the same call path here (auth-token guard + device-id +
|
||||
// platform="android"). P4 Stream N scope is receive-side only.
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
super.onMessageReceived(message)
|
||||
|
||||
val payload = NotificationPayload.parse(message.data)
|
||||
if (payload == null) {
|
||||
Log.w(TAG, "Dropping malformed FCM payload (keys=${message.data.keys})")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure channels exist — safe to call every time.
|
||||
NotificationChannels.ensureChannels(this)
|
||||
|
||||
val channelId = NotificationChannels.channelIdForType(payload.type)
|
||||
val notification = buildNotification(payload, channelId, message.messageId)
|
||||
|
||||
val notificationId = (message.messageId ?: payload.deepLink ?: payload.title)
|
||||
.hashCode()
|
||||
|
||||
NotificationManagerCompat.from(this).apply {
|
||||
if (areNotificationsEnabled()) {
|
||||
notify(notificationId, notification)
|
||||
} else {
|
||||
Log.d(TAG, "Notifications disabled — skipping notify()")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(
|
||||
payload: NotificationPayload,
|
||||
channelId: String,
|
||||
messageId: String?
|
||||
): android.app.Notification {
|
||||
val contentIntent = buildContentIntent(payload, messageId)
|
||||
|
||||
return NotificationCompat.Builder(this, channelId)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle(payload.title.ifBlank { getString(R.string.app_name) })
|
||||
.setContentText(payload.body)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(payload.body))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(contentIntent)
|
||||
.setPriority(priorityForChannel(channelId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildContentIntent(
|
||||
payload: NotificationPayload,
|
||||
messageId: String?
|
||||
): PendingIntent {
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
// Deep-link → set data URI so the launcher activity can route from the
|
||||
// existing intent-filter for the honeydue:// scheme.
|
||||
payload.deepLink?.let { data = Uri.parse(it) }
|
||||
payload.taskId?.let { putExtra(EXTRA_TASK_ID, it) }
|
||||
payload.residenceId?.let { putExtra(EXTRA_RESIDENCE_ID, it) }
|
||||
putExtra(EXTRA_TYPE, payload.type)
|
||||
}
|
||||
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val requestCode = (messageId ?: payload.deepLink ?: payload.title).hashCode()
|
||||
return PendingIntent.getActivity(this, requestCode, intent, flags)
|
||||
}
|
||||
|
||||
private fun priorityForChannel(channelId: String): Int = when (channelId) {
|
||||
NotificationChannels.TASK_OVERDUE -> NotificationCompat.PRIORITY_HIGH
|
||||
NotificationChannels.SUBSCRIPTION -> NotificationCompat.PRIORITY_LOW
|
||||
else -> NotificationCompat.PRIORITY_DEFAULT
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FcmService"
|
||||
const val EXTRA_TASK_ID = "fcm_task_id"
|
||||
const val EXTRA_RESIDENCE_ID = "fcm_residence_id"
|
||||
const val EXTRA_TYPE = "fcm_type"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user