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:
parent
8c4b4b992b
commit
f3b0056a90
5 changed files with 194 additions and 138 deletions
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 that’ll 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 that’ll 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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue