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

DROID-3636 Notifications | Push display (#2389)

This commit is contained in:
Konstantin Ivanov 2025-05-10 10:59:23 +02:00 committed by GitHub
parent c5bb4faea6
commit 4a4646929d
Signed by: github
GPG key ID: B5690EEEBB952194
3 changed files with 148 additions and 1 deletions

View file

@ -1,7 +1,19 @@
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.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
@ -12,13 +24,21 @@ class AnytypePushService : FirebaseMessagingService() {
@Inject
lateinit var deviceTokenSavingService: DeviceTokenStoringService
@Inject
lateinit var decryptionService: DecryptionPushContentService
private val notificationManager by lazy {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
init {
Timber.d("AnytypePushService initialized")
}
override fun onCreate() {
(application as AndroidApplication).componentManager.pushContentComponent.get().inject(this)
super.onCreate()
(application as AndroidApplication).componentManager.pushContentComponent.get().inject(this)
createNotificationChannel()
}
override fun onNewToken(token: String) {
@ -29,5 +49,104 @@ 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(content.newMessage)
else -> Timber.w("Unknown message type: ${content.type}")
}
}
private fun handleNewMessage(message: DecryptedPushContent.Message) {
Timber.d("New message received: $message")
// Create an intent to open the app when notification is tapped
//todo extra task on Navigation
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_CHAT_ID, message.chatId)
putExtra(EXTRA_SPACE_ID, message.spaceName)
}
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.senderName)
.setContentText(message.text)
.setStyle(NotificationCompat.BigTextStyle().bigText(message.text))
.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)
}
}
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"
val EXTRA_CHAT_ID = "chatId"
val EXTRA_SPACE_ID = "spaceId"
private const val NOTIFICATION_REQUEST_CODE = 100
}
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.di.feature.notifications
import com.anytypeio.anytype.device.AnytypePushService
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.anytypeio.anytype.presentation.notifications.DecryptionPushContentService
import dagger.Component
import dagger.Module
import javax.inject.Singleton
@ -28,4 +29,5 @@ object PushContentModule {
interface PushContentDependencies : ComponentDependencies {
fun deviceTokenSavingService(): DeviceTokenStoringService
fun decryptionService(): DecryptionPushContentService
}

View file

@ -8,6 +8,7 @@ import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
@ -128,6 +129,31 @@ class DecryptionPushContentServiceImplTest {
assertNull(result)
}
@Test
fun `decrypt should successfully decrypt actual RemoteMessage data`() {
// Given
val actualKeyId = "626166797265696376626f79757979696a6c66636235677461336665736c6f716132656f646b707377766133326b6d6c6b76336870637366756971e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
val actualEncryptedPayload = "OllD4bCyF0VbI0VrEsz0aYFuj+X8cnsRvm1wDYJC6aCzIyBu99NhHJi3xbIX565cUvIB6tlCdFzRUDc1WJqV8/0dbaB5PZozwLwbv9Pk+Ozxgsu6AspYT8MAR67exZ2ekD3dSo3hoeqlD50bJYQQnWvTgRUns5WzOzDanwwMMXJncxERlB2BdiqC7S2LmU47dgxoMytwBaJXemw9wHiU7dPnICSDAbnNlJU6DAGTn0Rqc38GpMbDg8+u2ksa1gb7P+P8XwTn9AFRPFz4Ay/mM/5jxcignyRGm3PObxBfUCP8NDwl7jH+55Q2VUgC2SX7vVEBLb5mlNJu3DwkhJvB7iRssulypiQ8I1w+mJ+Xh3TG2RYbgjb4l48mNoecblL/hvaRh560T3OTqlWlVNh0c5wRd/eo5YH5zoXrQydk2JXO6vReEWaJQt+bPU2y6N6IUbpLlw2q7OQu9jRIF5T35R3XO8GU8CmyKmhlJK4xAvhOiKIc8X47BGfApY6hl3TSPea9dSEnb0+EB0YsC7DyRc7y3NL588+Yc0sfHLA5Mp2oWs9a"
val actualEncryptedData = Base64.decode(actualEncryptedPayload, Base64.DEFAULT)
// Use the actual push key from the logs
val actualKeyValue = "RT1gb7DyUW5tc5qCF92Jc3IlEQVOgxxBo6x2BP5T5mU="
whenever(pushKeyProvider.getPushKey()).thenReturn(
mapOf(actualKeyId to PushKey(id = actualKeyId, value = actualKeyValue))
)
// When
val result = decryptionService.decrypt(actualEncryptedData, actualKeyId)
// Then
assertNotNull(result)
assertEquals(1, result?.type)
assertNotNull(result?.newMessage)
assertEquals("Спейсдля пушей", result?.newMessage?.spaceName)
assertEquals("Test not", result?.newMessage?.senderName)
assertEquals("ooo", result?.newMessage?.text)
}
private fun createTestContent(): DecryptedPushContent {
return DecryptedPushContent(
spaceId = testSpaceId,