Push: поддержка новых типов и супер-уведомления для звонков
All checks were successful
Android Kernel Build / build (push) Successful in 20m0s
All checks were successful
Android Kernel Build / build (push) Successful in 20m0s
This commit is contained in:
@@ -14,6 +14,9 @@ import com.rosetta.messenger.MainActivity
|
|||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
|
import com.rosetta.messenger.network.CallForegroundService
|
||||||
|
import com.rosetta.messenger.network.CallManager
|
||||||
|
import com.rosetta.messenger.network.CallPhase
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -38,6 +41,12 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
private const val TAG = "RosettaFCM"
|
private const val TAG = "RosettaFCM"
|
||||||
private const val CHANNEL_ID = "rosetta_messages"
|
private const val CHANNEL_ID = "rosetta_messages"
|
||||||
private const val CHANNEL_NAME = "Messages"
|
private const val CHANNEL_NAME = "Messages"
|
||||||
|
private const val CALL_CHANNEL_ID = "rosetta_calls_push"
|
||||||
|
private const val CALL_CHANNEL_NAME = "Calls"
|
||||||
|
private const val PUSH_TYPE_PERSONAL_MESSAGE = "personal_message"
|
||||||
|
private const val PUSH_TYPE_GROUP_MESSAGE = "group_message"
|
||||||
|
private const val PUSH_TYPE_CALL = "call"
|
||||||
|
private const val PUSH_TYPE_READ = "read"
|
||||||
|
|
||||||
// 🔥 Флаг - приложение в foreground (видимо пользователю)
|
// 🔥 Флаг - приложение в foreground (видимо пользователю)
|
||||||
@Volatile var isAppInForeground = false
|
@Volatile var isAppInForeground = false
|
||||||
@@ -114,17 +123,28 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
super.onMessageReceived(remoteMessage)
|
super.onMessageReceived(remoteMessage)
|
||||||
Log.d(TAG, "\u2709\ufe0f onMessageReceived: messageId=${remoteMessage.messageId} from=${remoteMessage.from} data=${remoteMessage.data} notif=${remoteMessage.notification?.body}")
|
Log.d(TAG, "\u2709\ufe0f onMessageReceived: messageId=${remoteMessage.messageId} from=${remoteMessage.from} data=${remoteMessage.data} notif=${remoteMessage.notification?.body}")
|
||||||
|
|
||||||
var handledMessageData = false
|
var handledByData = false
|
||||||
|
|
||||||
// Обрабатываем data payload
|
|
||||||
if (remoteMessage.data.isNotEmpty()) {
|
|
||||||
val data = remoteMessage.data
|
val data = remoteMessage.data
|
||||||
val notificationTitle = remoteMessage.notification?.title?.trim().orEmpty()
|
val notificationTitle = remoteMessage.notification?.title?.trim().orEmpty()
|
||||||
val notificationBody = remoteMessage.notification?.body?.trim().orEmpty()
|
val notificationBody = remoteMessage.notification?.body?.trim().orEmpty()
|
||||||
|
|
||||||
|
// Обрабатываем data payload (новый server формат + legacy fallback)
|
||||||
|
if (data.isNotEmpty()) {
|
||||||
val type =
|
val type =
|
||||||
firstNonBlank(data, "type", "event", "action")
|
firstNonBlank(data, "type", "event", "action")
|
||||||
?.lowercase(Locale.ROOT)
|
?.lowercase(Locale.ROOT)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
val dialogKey =
|
||||||
|
firstNonBlank(
|
||||||
|
data,
|
||||||
|
"dialog",
|
||||||
|
"sender_public_key",
|
||||||
|
"from_public_key",
|
||||||
|
"fromPublicKey",
|
||||||
|
"from",
|
||||||
|
"public_key",
|
||||||
|
"publicKey"
|
||||||
|
)
|
||||||
val senderPublicKey =
|
val senderPublicKey =
|
||||||
firstNonBlank(
|
firstNonBlank(
|
||||||
data,
|
data,
|
||||||
@@ -146,35 +166,57 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
"name"
|
"name"
|
||||||
)
|
)
|
||||||
?: notificationTitle.takeIf { it.isNotBlank() }
|
?: notificationTitle.takeIf { it.isNotBlank() }
|
||||||
|
?: dialogKey?.take(10)
|
||||||
?: senderPublicKey?.take(10)
|
?: senderPublicKey?.take(10)
|
||||||
?: "Rosetta"
|
?: "Rosetta"
|
||||||
val messagePreview =
|
val messagePreview =
|
||||||
firstNonBlank(data, "message_preview", "message", "text", "body")
|
firstNonBlank(data, "message_preview", "message", "text", "body")
|
||||||
?: notificationBody.takeIf { it.isNotBlank() }
|
?: notificationBody.takeIf { it.isNotBlank() }
|
||||||
?: "New message"
|
?: when (type) {
|
||||||
|
PUSH_TYPE_GROUP_MESSAGE -> "New group message"
|
||||||
|
PUSH_TYPE_CALL -> "Incoming call"
|
||||||
|
else -> "New message"
|
||||||
|
}
|
||||||
|
|
||||||
val isReadEvent = type == "message_read" || type == "read"
|
val isReadEvent = type == "message_read" || type == PUSH_TYPE_READ
|
||||||
val isMessageEvent =
|
val isMessageEvent =
|
||||||
type == "new_message" ||
|
type == "new_message" ||
|
||||||
type == "message" ||
|
type == "message" ||
|
||||||
type == "newmessage" ||
|
type == "newmessage" ||
|
||||||
type == "msg_new"
|
type == "msg_new" ||
|
||||||
|
type == PUSH_TYPE_PERSONAL_MESSAGE ||
|
||||||
|
type == PUSH_TYPE_GROUP_MESSAGE
|
||||||
|
|
||||||
when {
|
when {
|
||||||
isMessageEvent -> {
|
|
||||||
showMessageNotification(senderPublicKey, senderName, messagePreview)
|
|
||||||
handledMessageData = true
|
|
||||||
}
|
|
||||||
isReadEvent -> {
|
isReadEvent -> {
|
||||||
handledMessageData = true
|
if (!dialogKey.isNullOrBlank()) {
|
||||||
|
cancelNotificationForChat(applicationContext, dialogKey)
|
||||||
|
}
|
||||||
|
handledByData = true
|
||||||
|
}
|
||||||
|
type == PUSH_TYPE_CALL -> {
|
||||||
|
handleIncomingCallPush(
|
||||||
|
dialogKey = dialogKey ?: senderPublicKey.orEmpty(),
|
||||||
|
title = senderName,
|
||||||
|
body = messagePreview
|
||||||
|
)
|
||||||
|
handledByData = true
|
||||||
|
}
|
||||||
|
isMessageEvent -> {
|
||||||
|
showMessageNotification(dialogKey ?: senderPublicKey, senderName, messagePreview)
|
||||||
|
handledByData = true
|
||||||
}
|
}
|
||||||
// Fallback for servers sending data-only payload without explicit "type".
|
// Fallback for servers sending data-only payload without explicit "type".
|
||||||
senderPublicKey != null || data.containsKey("message_preview") || data.containsKey("message") || data.containsKey("text") -> {
|
dialogKey != null || senderPublicKey != null ||
|
||||||
showMessageNotification(senderPublicKey, senderName, messagePreview)
|
data.containsKey("message_preview") ||
|
||||||
handledMessageData = true
|
data.containsKey("message") ||
|
||||||
|
data.containsKey("text") -> {
|
||||||
|
showMessageNotification(dialogKey ?: senderPublicKey, senderName, messagePreview)
|
||||||
|
handledByData = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!handledByData) {
|
||||||
val looksLikeMessagePayload =
|
val looksLikeMessagePayload =
|
||||||
type.contains("message") ||
|
type.contains("message") ||
|
||||||
data.keys.any { key ->
|
data.keys.any { key ->
|
||||||
@@ -183,20 +225,20 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
lower.contains("text") ||
|
lower.contains("text") ||
|
||||||
lower.contains("body")
|
lower.contains("body")
|
||||||
}
|
}
|
||||||
if (!handledMessageData && !isReadEvent && looksLikeMessagePayload) {
|
if (looksLikeMessagePayload) {
|
||||||
showSimpleNotification(senderName, messagePreview, senderPublicKey)
|
showSimpleNotification(senderName, messagePreview, dialogKey ?: senderPublicKey)
|
||||||
handledMessageData = true
|
handledByData = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обрабатываем notification payload (если есть).
|
// Обрабатываем notification payload (если data-ветка не сработала).
|
||||||
// Для new_message используем data-ветку выше, чтобы не показывать дубликаты
|
|
||||||
// с неуправляемым notification id.
|
|
||||||
remoteMessage.notification?.let {
|
remoteMessage.notification?.let {
|
||||||
if (!handledMessageData) {
|
if (!handledByData) {
|
||||||
val senderPublicKey =
|
val senderPublicKey =
|
||||||
firstNonBlank(
|
firstNonBlank(
|
||||||
remoteMessage.data,
|
data,
|
||||||
|
"dialog",
|
||||||
"sender_public_key",
|
"sender_public_key",
|
||||||
"from_public_key",
|
"from_public_key",
|
||||||
"fromPublicKey",
|
"fromPublicKey",
|
||||||
@@ -329,6 +371,78 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
notificationManager.notify(notifId, notification)
|
notificationManager.notify(notifId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Супер push входящего звонка: пробуждаем протокол и показываем call notification */
|
||||||
|
private fun handleIncomingCallPush(dialogKey: String, title: String, body: String) {
|
||||||
|
wakeProtocolFromPush("call")
|
||||||
|
|
||||||
|
if (isAppInForeground || !areNotificationsEnabled()) return
|
||||||
|
|
||||||
|
val normalizedDialog = dialogKey.trim()
|
||||||
|
if (normalizedDialog.isNotEmpty() && isDialogMuted(normalizedDialog)) return
|
||||||
|
if (CallManager.state.value.phase != CallPhase.IDLE) return
|
||||||
|
|
||||||
|
val dedupKey = "call:${normalizedDialog.ifEmpty { "__no_dialog__" }}"
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val lastTs = lastNotifTimestamps[dedupKey]
|
||||||
|
if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) return
|
||||||
|
lastNotifTimestamps[dedupKey] = now
|
||||||
|
|
||||||
|
createCallNotificationChannel()
|
||||||
|
|
||||||
|
val notifId =
|
||||||
|
if (normalizedDialog.isNotEmpty()) {
|
||||||
|
getNotificationIdForChat(normalizedDialog)
|
||||||
|
} else {
|
||||||
|
("call:$title:$body").hashCode() and 0x7FFFFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
val openIntent =
|
||||||
|
Intent(this, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
putExtra("open_chat", normalizedDialog)
|
||||||
|
putExtra(CallForegroundService.EXTRA_OPEN_CALL_FROM_NOTIFICATION, true)
|
||||||
|
}
|
||||||
|
val pendingIntent =
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
notifId,
|
||||||
|
openIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
val notification =
|
||||||
|
NotificationCompat.Builder(this, CALL_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle(title.ifBlank { "Incoming call" })
|
||||||
|
.setContentText(body.ifBlank { "Incoming call" })
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setFullScreenIntent(pendingIntent, true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.notify(notifId, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Пробуждаем сетевой слой по high-priority push, чтобы быстрее получить сигнальные пакеты */
|
||||||
|
private fun wakeProtocolFromPush(reason: String) {
|
||||||
|
runCatching {
|
||||||
|
val account = AccountManager(applicationContext).getLastLoggedPublicKey().orEmpty()
|
||||||
|
ProtocolManager.initialize(applicationContext)
|
||||||
|
CallManager.initialize(applicationContext)
|
||||||
|
if (account.isNotBlank()) {
|
||||||
|
CallManager.bindAccount(account)
|
||||||
|
}
|
||||||
|
ProtocolManager.reconnectNowIfNeeded("push_$reason")
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.w(TAG, "wakeProtocolFromPush failed: ${error.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Создать notification channel для Android 8+ */
|
/** Создать notification channel для Android 8+ */
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -349,6 +463,26 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Отдельный канал для входящих звонков */
|
||||||
|
private fun createCallNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel =
|
||||||
|
NotificationChannel(
|
||||||
|
CALL_CHANNEL_ID,
|
||||||
|
CALL_CHANNEL_NAME,
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
description = "Incoming call notifications"
|
||||||
|
enableVibration(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Сохранить FCM токен в SharedPreferences */
|
/** Сохранить FCM токен в SharedPreferences */
|
||||||
private fun saveFcmToken(token: String) {
|
private fun saveFcmToken(token: String) {
|
||||||
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
||||||
|
|||||||
Reference in New Issue
Block a user