1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3628 Notifications | Refactoring (#2436)

This commit is contained in:
Konstantin Ivanov 2025-05-22 14:45:42 +02:00 committed by GitHub
parent 8c4b4b992b
commit f3b0056a90
Signed by: github
GPG key ID: B5690EEEBB952194
5 changed files with 194 additions and 138 deletions

View file

@ -1,22 +1,7 @@
package com.anytypeio.anytype.device
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.Base64
import androidx.core.app.NotificationCompat
import com.anytypeio.anytype.R
import com.anytypeio.anytype.app.AndroidApplication
import com.anytypeio.anytype.core_models.DecryptedPushContent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_ui.views.Relations1
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService
import com.anytypeio.anytype.ui.main.MainActivity
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import javax.inject.Inject
@ -28,11 +13,7 @@ class AnytypePushService : FirebaseMessagingService() {
lateinit var deviceTokenSavingService: DeviceTokenStoringService
@Inject
lateinit var decryptionService: DecryptionPushContentService
private val notificationManager by lazy {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
lateinit var processor: PushMessageProcessor
init {
Timber.d("AnytypePushService initialized")
@ -41,7 +22,7 @@ class AnytypePushService : FirebaseMessagingService() {
override fun onCreate() {
super.onCreate()
(application as AndroidApplication).componentManager.pushContentComponent.get().inject(this)
createNotificationChannel()
Timber.d("AnytypePushService initialized")
}
override fun onNewToken(token: String) {
@ -52,109 +33,11 @@ class AnytypePushService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
Timber.d("Received message: ${message}")
try {
// Extract encrypted data and keyId from the message
val encryptedData = message.data[PAYLOAD_KEY]?.let { Base64.decode(it, Base64.DEFAULT) }
val keyId = message.data[KEY_ID_KEY]
if (encryptedData == null || keyId == null) {
Timber.w("Missing required data in push message: encryptedData is null =${encryptedData == null}, keyId=$keyId")
return
}
// Decrypt the message
val decryptedContent = decryptionService.decrypt(encryptedData, keyId)
if (decryptedContent == null) {
Timber.w("Failed to decrypt push message")
return
}
// Handle the decrypted content
handleDecryptedContent(decryptedContent)
} catch (e: Exception) {
Timber.w(e, "Error processing push message")
}
}
private fun handleDecryptedContent(content: DecryptedPushContent) {
Timber.d("Decrypted content: $content")
when (content.type) {
1 -> handleNewMessage(
message = content.newMessage,
space = content.spaceId
)
else -> Timber.w("Unknown message type: ${content.type}")
}
}
private fun handleNewMessage(
message: DecryptedPushContent.Message,
space: Id
) {
Timber.d("New message received: $message")
// Create an intent to open the app when notification is tapped
val intent = Intent(this, MainActivity::class.java).apply {
action = ACTION_OPEN_CHAT
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(Relations.CHAT_ID, message.chatId)
putExtra(Relations.SPACE_ID, space)
}
val pendingIntent = PendingIntent.getActivity(
this,
NOTIFICATION_REQUEST_CODE,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
// Build the notification
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_app_notification)
.setContentTitle(message.spaceName.trim())
.setSubText(message.senderName.trim())
.setContentText(message.text.trim())
.setStyle(NotificationCompat.BigTextStyle().bigText(message.text.trim()))
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setFullScreenIntent(pendingIntent, true)
.setLights(0xFF0000FF.toInt(), 300, 1000)
.setVibrate(longArrayOf(0, 500, 200, 500))
.build()
// Show the notification
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "New messages notifications"
enableLights(true)
enableVibration(true)
setShowBadge(true)
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
}
notificationManager.createNotificationChannel(channel)
}
Timber.d("Received message: $message")
processor.process(message.data)
}
companion object {
private const val CHANNEL_ID = "messages_channel"
private const val PAYLOAD_KEY = "x-any-payload"
private const val KEY_ID_KEY = "x-any-key-id"
private const val CHANNEL_NAME = "Chat Messages"
private const val NOTIFICATION_REQUEST_CODE = 100
const val ACTION_OPEN_CHAT = "com.anytype.ACTION_OPEN_CHAT"
}
}

View file

@ -0,0 +1,95 @@
package com.anytypeio.anytype.device
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 androidx.core.app.NotificationCompat
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.DecryptedPushContent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.ui.main.MainActivity
class NotificationBuilder(
private val context: Context,
private val notificationManager: NotificationManager
) {
fun buildAndNotify(message: DecryptedPushContent.Message, spaceId: Id) {
// 1) Build the intent thatll open your MainActivity in the right chat
val pending = createChatPendingIntent(
context = context,
chatId = message.chatId,
spaceId = spaceId
)
val notif = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_app_notification)
.setContentTitle(message.spaceName.trim())
.setSubText(message.senderName.trim())
.setContentText(message.text.trim())
.setStyle(NotificationCompat.BigTextStyle().bigText(message.text.trim()))
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pending)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setFullScreenIntent(pending, true)
.setLights(0xFF0000FF.toInt(), 300, 1000)
.setVibrate(longArrayOf(0, 500, 200, 500))
.build()
notificationManager.notify(System.currentTimeMillis().toInt(), notif)
}
fun createNotificationChannelIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH
).apply {
description = "New messages notifications"
enableLights(true)
enableVibration(true)
setShowBadge(true)
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
}
notificationManager.createNotificationChannel(channel)
}
}
/**
* Creates the tap-action intent and wraps it in a PendingIntent for notifications.
*/
fun createChatPendingIntent(
context: Context,
chatId: String,
spaceId: Id
): PendingIntent {
// 1) Build the intent thatll open your MainActivity in the right chat
val intent = Intent(context, MainActivity::class.java).apply {
action = AnytypePushService.ACTION_OPEN_CHAT
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(Relations.CHAT_ID, chatId)
putExtra(Relations.SPACE_ID, spaceId)
}
// 2) Wrap it in a one-shot immutable PendingIntent
return PendingIntent.getActivity(
context,
NOTIFICATION_REQUEST_CODE,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
}
companion object {
private const val NOTIFICATION_REQUEST_CODE = 100
private const val CHANNEL_ID = "messages_channel"
private const val CHANNEL_NAME = "Chat Messages"
}
}

View file

@ -0,0 +1,38 @@
package com.anytypeio.anytype.device
import android.util.Base64
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService
interface PushMessageProcessor {
/**
* Returns `true` if the message was handled (e.g. decrypted & showed a notification),
* or `false` if it should be ignored (e.g. missing payload/key or decryption failed).
*/
fun process(messageData: Map<String, String>): Boolean
}
class DefaultPushMessageProcessor(
private val decryptionService: DecryptionPushContentService,
private val notificationBuilder: NotificationBuilder
) : PushMessageProcessor {
override fun process(messageData: Map<String, String>): Boolean {
val base64 = messageData[PAYLOAD_KEY] ?: return false
val keyId = messageData[KEY_ID_KEY] ?: return false
val encrypted = Base64.decode(base64, Base64.DEFAULT)
val content = decryptionService.decrypt(encrypted, keyId) ?: return false
notificationBuilder.buildAndNotify(
message = content.newMessage,
spaceId = content.spaceId
)
return true
}
companion object {
private const val PAYLOAD_KEY = "x-any-payload"
private const val KEY_ID_KEY = "x-any-key-id"
}
}

View file

@ -1,11 +1,21 @@
package com.anytypeio.anytype.di.feature.notifications
import android.app.NotificationManager
import android.content.Context
import com.anytypeio.anytype.device.AnytypePushService
import com.anytypeio.anytype.device.DefaultPushMessageProcessor
import com.anytypeio.anytype.device.NotificationBuilder
import com.anytypeio.anytype.device.PushMessageProcessor
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.anytypeio.anytype.presentation.notifications.CryptoService
import com.anytypeio.anytype.presentation.notifications.CryptoServiceImpl
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentServiceImpl
import com.anytypeio.anytype.presentation.notifications.PushKeyProvider
import dagger.Component
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Singleton
@ -24,10 +34,56 @@ interface PushContentComponent {
@Module
object PushContentModule {
@JvmStatic
@Provides
@Singleton
fun provideNotificationManager(
context: Context
): NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@JvmStatic
@Provides
@Singleton
fun provideNotificationBuilder(
context: Context,
notificationManager: NotificationManager
): NotificationBuilder = NotificationBuilder(
context = context,
notificationManager = notificationManager
).apply {
createNotificationChannelIfNeeded()
}
@JvmStatic
@Provides
@Singleton
fun providePushMessageProcessor(
decryptionService: DecryptionPushContentService,
notificationBuilder: NotificationBuilder
): PushMessageProcessor = DefaultPushMessageProcessor(
decryptionService = decryptionService,
notificationBuilder = notificationBuilder
)
@JvmStatic
@Provides
@Singleton
fun provideCryptoService(): CryptoService = CryptoServiceImpl()
@JvmStatic
@Provides
@Singleton
fun provideDecryptionPushContentService(
pushKeyProvider: PushKeyProvider,
cryptoService: CryptoService,
): DecryptionPushContentService = DecryptionPushContentServiceImpl(
pushKeyProvider = pushKeyProvider,
cryptoService = cryptoService,
)
}
interface PushContentDependencies : ComponentDependencies {
fun deviceTokenSavingService(): DeviceTokenStoringService
fun decryptionService(): DecryptionPushContentService
fun pushKeyProvider(): PushKeyProvider
fun context(): Context
}

View file

@ -116,22 +116,6 @@ object NotificationsModule {
channel = channel
)
@JvmStatic
@Provides
@Singleton
fun provideCryptoService(): CryptoService = CryptoServiceImpl()
@JvmStatic
@Provides
@Singleton
fun provideDecryptionPushContentService(
pushKeyProvider: PushKeyProvider,
cryptoService: CryptoService,
): DecryptionPushContentService = DecryptionPushContentServiceImpl(
pushKeyProvider = pushKeyProvider,
cryptoService = cryptoService,
)
@Provides
@Singleton
@JvmStatic