From f3b0056a908a11f43fd1785e2e37e1c24fde9b45 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Thu, 22 May 2025 14:45:42 +0200 Subject: [PATCH] DROID-3628 Notifications | Refactoring (#2436) --- .../anytype/device/AnytypePushService.kt | 125 +----------------- .../anytype/device/NotificationBuilder.kt | 95 +++++++++++++ .../anytype/device/PushMessageProcessor.kt | 38 ++++++ .../di/feature/notifications/PushDI.kt | 58 +++++++- .../anytype/di/main/NotificationsModule.kt | 16 --- 5 files changed, 194 insertions(+), 138 deletions(-) create mode 100644 app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt diff --git a/app/src/main/java/com/anytypeio/anytype/device/AnytypePushService.kt b/app/src/main/java/com/anytypeio/anytype/device/AnytypePushService.kt index 61eeda1068..80aa688cf4 100644 --- a/app/src/main/java/com/anytypeio/anytype/device/AnytypePushService.kt +++ b/app/src/main/java/com/anytypeio/anytype/device/AnytypePushService.kt @@ -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" } } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt b/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt new file mode 100644 index 0000000000..7c55c49045 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/device/NotificationBuilder.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt b/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt new file mode 100644 index 0000000000..e688dec518 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/device/PushMessageProcessor.kt @@ -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): Boolean +} + +class DefaultPushMessageProcessor( + private val decryptionService: DecryptionPushContentService, + private val notificationBuilder: NotificationBuilder +) : PushMessageProcessor { + + override fun process(messageData: Map): 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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt index 74e483ae42..5354c0791f 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/notifications/PushDI.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt index 859977804a..b963f5c73f 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/NotificationsModule.kt @@ -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