mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
fix notifications
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.bitchat.android
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -44,6 +45,9 @@ class MainActivity : ComponentActivity() {
|
||||
// Request necessary permissions
|
||||
requestPermissions()
|
||||
|
||||
// Handle notification intent if app was opened from a notification
|
||||
handleNotificationIntent(intent)
|
||||
|
||||
setContent {
|
||||
BitchatTheme {
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
val permissions = mutableListOf<String>()
|
||||
|
||||
|
||||
@@ -149,11 +149,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
// MARK: - Private Chat Management (delegated)
|
||||
|
||||
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() {
|
||||
privateChatManager.endPrivateChat()
|
||||
// Notify notification manager that no private chat is active
|
||||
setCurrentPrivateChatPeer(null)
|
||||
}
|
||||
|
||||
// MARK: - Message Sending
|
||||
@@ -266,6 +274,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
fun setAppBackgroundState(inBackground: Boolean) {
|
||||
// Forward to connection manager for power optimization
|
||||
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)
|
||||
|
||||
@@ -46,8 +46,18 @@ class MeshDelegateHandler(
|
||||
if (message.isPrivate) {
|
||||
// Private 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) {
|
||||
// Channel message
|
||||
|
||||
@@ -2,15 +2,60 @@ package com.bitchat.android.ui
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
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 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) {
|
||||
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
private val channelId = "bitchat_notifications"
|
||||
companion object {
|
||||
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 {
|
||||
createNotificationChannel()
|
||||
@@ -18,24 +63,261 @@ class NotificationManager(private val context: Context) {
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name = "Bitchat Notifications"
|
||||
val descriptionText = "Notifications for new messages and other events"
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel = NotificationChannel(channelId, name, importance).apply {
|
||||
val name = "Direct Messages"
|
||||
val descriptionText = "Notifications for private messages from other users"
|
||||
val importance = NotificationManager.IMPORTANCE_HIGH
|
||||
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
|
||||
description = descriptionText
|
||||
enableVibration(true)
|
||||
setShowBadge(true)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
systemNotificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
fun showPrivateMessageNotification(sender: String, message: String) {
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle("New message from $sender")
|
||||
.setContentText(message)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
/**
|
||||
* Update app background state - notifications should only be shown when app is backgrounded
|
||||
*/
|
||||
fun setAppBackgroundState(inBackground: Boolean) {
|
||||
isAppInBackground = inBackground
|
||||
Log.d(TAG, "App background state changed: $inBackground")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user