fix notifications

This commit is contained in:
callebtc
2025-07-10 12:48:04 +02:00
parent 8b5c19ace7
commit 37d605b934
4 changed files with 368 additions and 20 deletions
@@ -1,6 +1,7 @@
package com.bitchat.android package com.bitchat.android
import android.Manifest import android.Manifest
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -44,6 +45,9 @@ class MainActivity : ComponentActivity() {
// Request necessary permissions // Request necessary permissions
requestPermissions() requestPermissions()
// Handle notification intent if app was opened from a notification
handleNotificationIntent(intent)
setContent { setContent {
BitchatTheme { BitchatTheme {
Surface( Surface(
@@ -56,18 +60,49 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// Handle notification intents when app is already running
intent?.let { handleNotificationIntent(it) }
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Notify that app is in foreground for power optimization // Notify that app is in foreground for power optimization and notifications
chatViewModel.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// Notify that app is in background for power optimization // Notify that app is in background for power optimization and notifications
chatViewModel.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true)
} }
/**
* Handle intents from notification clicks - open specific private chat
*/
private fun handleNotificationIntent(intent: Intent) {
val shouldOpenPrivateChat = intent.getBooleanExtra(
com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_PRIVATE_CHAT,
false
)
if (shouldOpenPrivateChat) {
val peerID = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_PEER_ID)
val senderNickname = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_SENDER_NICKNAME)
if (peerID != null) {
android.util.Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
// Open the private chat with this peer
chatViewModel.startPrivateChat(peerID)
// Clear notifications for this sender since user is now viewing the chat
chatViewModel.clearNotificationsForSender(peerID)
}
}
}
private fun requestPermissions() { private fun requestPermissions() {
val permissions = mutableListOf<String>() val permissions = mutableListOf<String>()
@@ -149,11 +149,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
// MARK: - Private Chat Management (delegated) // MARK: - Private Chat Management (delegated)
fun startPrivateChat(peerID: String) { fun startPrivateChat(peerID: String) {
privateChatManager.startPrivateChat(peerID, meshService) val success = privateChatManager.startPrivateChat(peerID, meshService)
if (success) {
// Notify notification manager about current private chat
setCurrentPrivateChatPeer(peerID)
// Clear notifications for this sender since user is now viewing the chat
clearNotificationsForSender(peerID)
}
} }
fun endPrivateChat() { fun endPrivateChat() {
privateChatManager.endPrivateChat() privateChatManager.endPrivateChat()
// Notify notification manager that no private chat is active
setCurrentPrivateChatPeer(null)
} }
// MARK: - Message Sending // MARK: - Message Sending
@@ -266,6 +274,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
fun setAppBackgroundState(inBackground: Boolean) { fun setAppBackgroundState(inBackground: Boolean) {
// Forward to connection manager for power optimization // Forward to connection manager for power optimization
meshService.connectionManager.setAppBackgroundState(inBackground) meshService.connectionManager.setAppBackgroundState(inBackground)
// Forward to notification manager for notification logic
notificationManager.setAppBackgroundState(inBackground)
}
fun setCurrentPrivateChatPeer(peerID: String?) {
// Update notification manager with current private chat peer
notificationManager.setCurrentPrivateChatPeer(peerID)
}
fun clearNotificationsForSender(peerID: String) {
// Clear notifications when user opens a chat
notificationManager.clearNotificationsForSender(peerID)
} }
// MARK: - Command Autocomplete (delegated) // MARK: - Command Autocomplete (delegated)
@@ -46,8 +46,18 @@ class MeshDelegateHandler(
if (message.isPrivate) { if (message.isPrivate) {
// Private message // Private message
privateChatManager.handleIncomingPrivateMessage(message) privateChatManager.handleIncomingPrivateMessage(message)
if (state.getSelectedPrivateChatPeerValue() != message.senderPeerID) {
notificationManager.showPrivateMessageNotification(message.sender, message.content) // Show notification with enhanced information - now includes senderPeerID
message.senderPeerID?.let { senderPeerID ->
if (state.getSelectedPrivateChatPeerValue() != senderPeerID) {
// Use nickname if available, fall back to sender or senderPeerID
val senderNickname = message.sender.takeIf { it != senderPeerID } ?: senderPeerID
notificationManager.showPrivateMessageNotification(
senderPeerID = senderPeerID,
senderNickname = senderNickname,
messageContent = message.content
)
}
} }
} else if (message.channel != null) { } else if (message.channel != null) {
// Channel message // Channel message
@@ -2,15 +2,60 @@ package com.bitchat.android.ui
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.app.NotificationManagerCompat
import com.bitchat.android.MainActivity
import com.bitchat.android.R import com.bitchat.android.R
import java.util.concurrent.ConcurrentHashMap
/**
* Enhanced notification manager for direct messages with production-ready features:
* - Notification grouping per sender
* - Click handling to open specific DM
* - App background state awareness
* - Proper notification management and cleanup
*/
class NotificationManager(private val context: Context) { class NotificationManager(private val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager companion object {
private val channelId = "bitchat_notifications" private const val TAG = "NotificationManager"
private const val CHANNEL_ID = "bitchat_dm_notifications"
private const val GROUP_KEY_DM = "bitchat_dm_group"
private const val NOTIFICATION_REQUEST_CODE = 1000
private const val SUMMARY_NOTIFICATION_ID = 999
// Intent extras for notification handling
const val EXTRA_OPEN_PRIVATE_CHAT = "open_private_chat"
const val EXTRA_PEER_ID = "peer_id"
const val EXTRA_SENDER_NICKNAME = "sender_nickname"
}
private val notificationManager = NotificationManagerCompat.from(context)
private val systemNotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Track pending notifications per sender to enable grouping
private val pendingNotifications = ConcurrentHashMap<String, MutableList<PendingNotification>>()
// Track app background state
@Volatile
private var isAppInBackground = false
// Track current view state
@Volatile
private var currentPrivateChatPeer: String? = null
data class PendingNotification(
val senderPeerID: String,
val senderNickname: String,
val messageContent: String,
val timestamp: Long
)
init { init {
createNotificationChannel() createNotificationChannel()
@@ -18,24 +63,261 @@ class NotificationManager(private val context: Context) {
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "Bitchat Notifications" val name = "Direct Messages"
val descriptionText = "Notifications for new messages and other events" val descriptionText = "Notifications for private messages from other users"
val importance = NotificationManager.IMPORTANCE_DEFAULT val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(channelId, name, importance).apply { val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText description = descriptionText
enableVibration(true)
setShowBadge(true)
} }
notificationManager.createNotificationChannel(channel) systemNotificationManager.createNotificationChannel(channel)
} }
} }
fun showPrivateMessageNotification(sender: String, message: String) { /**
val builder = NotificationCompat.Builder(context, channelId) * Update app background state - notifications should only be shown when app is backgrounded
.setSmallIcon(R.drawable.ic_notification) */
.setContentTitle("New message from $sender") fun setAppBackgroundState(inBackground: Boolean) {
.setContentText(message) isAppInBackground = inBackground
.setPriority(NotificationCompat.PRIORITY_DEFAULT) Log.d(TAG, "App background state changed: $inBackground")
.setAutoCancel(true) }
notificationManager.notify(System.currentTimeMillis().toInt(), builder.build()) /**
* Update current private chat peer - affects notification logic
*/
fun setCurrentPrivateChatPeer(peerID: String?) {
currentPrivateChatPeer = peerID
Log.d(TAG, "Current private chat peer changed: $peerID")
}
/**
* Show a notification for a private message with proper grouping and state awareness
*/
fun showPrivateMessageNotification(senderPeerID: String, senderNickname: String, messageContent: String) {
// Only show notifications if app is in background OR user is not viewing this specific chat
val shouldNotify = isAppInBackground || currentPrivateChatPeer != senderPeerID
if (!shouldNotify) {
Log.d(TAG, "Skipping notification - app in foreground and viewing chat with $senderNickname")
return
}
Log.d(TAG, "Showing notification for message from $senderNickname (peerID: $senderPeerID)")
val notification = PendingNotification(
senderPeerID = senderPeerID,
senderNickname = senderNickname,
messageContent = messageContent,
timestamp = System.currentTimeMillis()
)
// Add to pending notifications for this sender
pendingNotifications.computeIfAbsent(senderPeerID) { mutableListOf() }.add(notification)
// Create or update notification for this sender
showNotificationForSender(senderPeerID)
// Update summary notification if we have multiple senders
if (pendingNotifications.size > 1) {
showSummaryNotification()
}
}
private fun showNotificationForSender(senderPeerID: String) {
val notifications = pendingNotifications[senderPeerID] ?: return
if (notifications.isEmpty()) return
val latestNotification = notifications.last()
val messageCount = notifications.size
// Create intent to open the specific private chat
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_OPEN_PRIVATE_CHAT, true)
putExtra(EXTRA_PEER_ID, senderPeerID)
putExtra(EXTRA_SENDER_NICKNAME, latestNotification.senderNickname)
}
val pendingIntent = PendingIntent.getActivity(
context,
NOTIFICATION_REQUEST_CODE + senderPeerID.hashCode(),
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// Create person for better notification styling
val person = Person.Builder()
.setName(latestNotification.senderNickname)
.setKey(senderPeerID)
.build()
// Build notification content
val contentText = if (messageCount == 1) {
latestNotification.messageContent
} else {
"${latestNotification.messageContent} (+${messageCount - 1} more)"
}
val contentTitle = if (messageCount == 1) {
latestNotification.senderNickname
} else {
"${latestNotification.senderNickname} ($messageCount messages)"
}
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(contentTitle)
.setContentText(contentText)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.addPerson(person)
.setShowWhen(true)
.setWhen(latestNotification.timestamp)
// Add to notification group if we have multiple senders
if (pendingNotifications.size > 1) {
builder.setGroup(GROUP_KEY_DM)
}
// Add style for multiple messages
if (messageCount > 1) {
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(contentTitle)
// Show last few messages in expanded view
notifications.takeLast(5).forEach { notif ->
style.addLine(notif.messageContent)
}
if (messageCount > 5) {
style.setSummaryText("and ${messageCount - 5} more")
}
builder.setStyle(style)
} else {
// Single message - use BigTextStyle for long messages
builder.setStyle(
NotificationCompat.BigTextStyle()
.bigText(latestNotification.messageContent)
)
}
// Use sender peer ID hash as notification ID to group messages from same sender
val notificationId = senderPeerID.hashCode()
notificationManager.notify(notificationId, builder.build())
Log.d(TAG, "Displayed notification for $contentTitle with ID $notificationId")
}
private fun showSummaryNotification() {
if (pendingNotifications.isEmpty()) return
val totalMessages = pendingNotifications.values.sumOf { it.size }
val senderCount = pendingNotifications.size
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
context,
NOTIFICATION_REQUEST_CODE,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("bitchat")
.setContentText("$totalMessages messages from $senderCount people")
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setGroup(GROUP_KEY_DM)
.setGroupSummary(true)
// Add inbox style showing recent senders
val style = NotificationCompat.InboxStyle()
.setBigContentTitle("New Messages")
pendingNotifications.entries.take(5).forEach { (peerID, notifications) ->
val latestNotif = notifications.last()
val count = notifications.size
val line = if (count == 1) {
"${latestNotif.senderNickname}: ${latestNotif.messageContent}"
} else {
"${latestNotif.senderNickname}: $count messages"
}
style.addLine(line)
}
if (pendingNotifications.size > 5) {
style.setSummaryText("and ${pendingNotifications.size - 5} more conversations")
}
builder.setStyle(style)
notificationManager.notify(SUMMARY_NOTIFICATION_ID, builder.build())
Log.d(TAG, "Displayed summary notification for $senderCount senders")
}
/**
* Clear notifications for a specific sender (e.g., when user opens their chat)
*/
fun clearNotificationsForSender(senderPeerID: String) {
pendingNotifications.remove(senderPeerID)
// Cancel the individual notification
val notificationId = senderPeerID.hashCode()
notificationManager.cancel(notificationId)
// Update or remove summary notification
if (pendingNotifications.isEmpty()) {
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
} else if (pendingNotifications.size == 1) {
// Only one sender left, remove group summary
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
} else {
// Update summary notification
showSummaryNotification()
}
Log.d(TAG, "Cleared notifications for sender: $senderPeerID")
}
/**
* Clear all pending notifications
*/
fun clearAllNotifications() {
pendingNotifications.clear()
notificationManager.cancelAll()
Log.d(TAG, "Cleared all notifications")
}
/**
* Get pending notification count for UI badging
*/
fun getPendingNotificationCount(): Int {
return pendingNotifications.values.sumOf { it.size }
}
/**
* Get pending notifications for debugging
*/
fun getDebugInfo(): String {
return buildString {
appendLine("Notification Manager Debug Info:")
appendLine("App in background: $isAppInBackground")
appendLine("Current private chat peer: $currentPrivateChatPeer")
appendLine("Pending notifications: ${pendingNotifications.size} senders")
pendingNotifications.forEach { (peerID, notifications) ->
appendLine(" $peerID: ${notifications.size} messages")
}
}
} }
} }