diff --git a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt index d8b9633..194af02 100644 --- a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt +++ b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt @@ -206,7 +206,12 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { keysToClear.forEach { key -> cancelNotificationForChat(applicationContext, key) } - Log.d(TAG, "READ push cleared notifications for keys=$keysToClear") + val titleHints = collectReadTitleHints(data, keysToClear) + cancelMatchingActiveNotifications(keysToClear, titleHints) + Log.d( + TAG, + "READ push cleared notifications for keys=$keysToClear titles=$titleHints" + ) } handledByData = true } @@ -588,6 +593,118 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { return candidates } + private fun collectReadTitleHints( + data: Map, + dialogKeys: Set + ): Set { + val hints = linkedSetOf() + + fun add(raw: String?) { + val value = raw?.trim().orEmpty() + if (value.isNotBlank()) hints.add(value) + } + + add(firstNonBlank(data, "title", "sender_name", "sender_title", "from_title", "name")) + dialogKeys.forEach { key -> + add(resolveNameForKey(key)) + } + + return hints + } + + /** + * Fallback for system-shown notifications with unknown IDs (FCM notification payload path). + * Matches active notifications by: + * 1) deterministic dialog hash id + * 2) dialog key/group key in notification extras/text + * 3) known dialog title hints + */ + private fun cancelMatchingActiveNotifications( + dialogKeys: Set, + titleHints: Set + ) { + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager ?: return + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return + + val normalizedDialogKeys = + dialogKeys + .flatMap { key -> + buildDialogKeyVariants(key).toList() + key.trim() + } + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() + val normalizedHints = titleHints.map { it.trim() }.filter { it.isNotEmpty() }.toSet() + if (normalizedDialogKeys.isEmpty() && normalizedHints.isEmpty()) return + + runCatching { + manager.activeNotifications.forEach { sbn -> + val notification = sbn.notification ?: return@forEach + if (notification.channelId == "rosetta_calls") return@forEach + val title = + notification.extras + ?.getCharSequence(android.app.Notification.EXTRA_TITLE) + ?.toString() + ?.trim() + .orEmpty() + val text = + notification.extras + ?.getCharSequence(android.app.Notification.EXTRA_TEXT) + ?.toString() + ?.trim() + .orEmpty() + val bigText = + notification.extras + ?.getCharSequence(android.app.Notification.EXTRA_BIG_TEXT) + ?.toString() + ?.trim() + .orEmpty() + + val bag = mutableSetOf() + if (title.isNotEmpty()) bag.add(title) + if (text.isNotEmpty()) bag.add(text) + if (bigText.isNotEmpty()) bag.add(bigText) + notification.extras?.keySet()?.forEach { extraKey -> + bag.add(extraKey) + val value = notification.extras?.get(extraKey) + when (value) { + is CharSequence -> bag.add(value.toString()) + is String -> bag.add(value) + is Array<*> -> value.filterIsInstance().forEach { bag.add(it.toString()) } + } + } + val bagLower = bag.map { it.lowercase(Locale.ROOT) } + val matchesDialogKey = + normalizedDialogKeys.any { key -> + val lowerKey = key.lowercase(Locale.ROOT) + bagLower.any { it.contains(lowerKey) } + } + val matchesHint = + normalizedHints.any { hint -> + title.equals(hint, ignoreCase = true) || + text.contains(hint, ignoreCase = true) || + bigText.contains(hint, ignoreCase = true) + } + val matchesDeterministicId = + normalizedDialogKeys.any { key -> + getNotificationIdForChat(key) == sbn.id + } + + if (matchesDeterministicId || matchesDialogKey || matchesHint) { + manager.cancel(sbn.tag, sbn.id) + Log.d( + TAG, + "READ push fallback cancel id=${sbn.id} tag=${sbn.tag} " + + "channel=${notification.channelId} title='$title' " + + "matchId=$matchesDeterministicId matchKey=$matchesDialogKey matchHint=$matchesHint" + ) + } + } + }.onFailure { error -> + Log.w(TAG, "cancelMatchingActiveNotifications failed: ${error.message}") + } + } + private fun isAvatarInNotificationsEnabled(): Boolean { return runCatching { runBlocking(Dispatchers.IO) {