1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

DROID-3636 Notifications | Firebase messaging service implementation (#2383)

This commit is contained in:
Konstantin Ivanov 2025-05-07 18:02:03 +02:00 committed by GitHub
parent 0e9a997998
commit 01638ff2f7
Signed by: github
GPG key ID: B5690EEEBB952194
26 changed files with 273 additions and 10 deletions

View file

@ -230,6 +230,9 @@ dependencies {
implementation libs.room
implementation platform(libs.firebaseBom)
implementation libs.firebaseMessaging
implementation libs.exoPlayerCore
implementation libs.exoPlayerUi

View file

@ -160,6 +160,13 @@
</intent-filter>
<meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>
<service
android:name="com.anytypeio.anytype.device.AnytypePushService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View file

@ -0,0 +1,33 @@
package com.anytypeio.anytype.device
import com.anytypeio.anytype.app.AndroidApplication
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import javax.inject.Inject
import timber.log.Timber
class AnytypePushService : FirebaseMessagingService() {
@Inject
lateinit var deviceTokenSavingService: DeviceTokenStoringService
init {
Timber.d("AnytypePushService initialized")
}
override fun onCreate() {
(application as AndroidApplication).componentManager.pushContentComponent.get().inject(this)
super.onCreate()
}
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New token received: $token")
deviceTokenSavingService.saveToken(token)
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
}
}

View file

@ -65,6 +65,7 @@ import com.anytypeio.anytype.di.feature.multiplayer.DaggerRequestJoinSpaceCompon
import com.anytypeio.anytype.di.feature.multiplayer.DaggerShareSpaceComponent
import com.anytypeio.anytype.di.feature.multiplayer.DaggerSpaceJoinRequestComponent
import com.anytypeio.anytype.di.feature.notifications.DaggerNotificationComponent
import com.anytypeio.anytype.di.feature.notifications.DaggerPushContentComponent
import com.anytypeio.anytype.di.feature.objects.DaggerSelectObjectTypeComponent
import com.anytypeio.anytype.di.feature.onboarding.DaggerOnboardingComponent
import com.anytypeio.anytype.di.feature.onboarding.DaggerOnboardingStartComponent
@ -1137,6 +1138,12 @@ class ComponentManager(
.create(params, findComponentDependencies())
}
val pushContentComponent = Component {
DaggerPushContentComponent
.factory()
.create(findComponentDependencies())
}
class Component<T>(private val builder: () -> T) {
private var instance: T? = null

View file

@ -0,0 +1,31 @@
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 dagger.Component
import dagger.Module
import javax.inject.Singleton
@Singleton
@Component(
dependencies = [PushContentDependencies::class],
modules = [PushContentModule::class],
)
interface PushContentComponent {
@Component.Factory
interface Factory {
fun create(dependency: PushContentDependencies): PushContentComponent
}
fun inject(service: AnytypePushService)
}
@Module
object PushContentModule {
}
interface PushContentDependencies : ComponentDependencies {
fun deviceTokenSavingService(): DeviceTokenStoringService
}

View file

@ -8,6 +8,7 @@ import com.anytypeio.anytype.data.auth.event.EventDataChannel
import com.anytypeio.anytype.data.auth.event.EventRemoteChannel
import com.anytypeio.anytype.data.auth.event.FileLimitsDataChannel
import com.anytypeio.anytype.data.auth.event.FileLimitsRemoteChannel
import com.anytypeio.anytype.data.auth.event.PushKeyRemoteChannel
import com.anytypeio.anytype.data.auth.event.SubscriptionDataChannel
import com.anytypeio.anytype.data.auth.event.SubscriptionEventRemoteChannel
import com.anytypeio.anytype.data.auth.status.SyncAndP2PStatusEventsStore
@ -128,12 +129,14 @@ object EventModule {
logger: MiddlewareProtobufLogger,
@Named(DEFAULT_APP_COROUTINE_SCOPE) scope: CoroutineScope,
channel: EventHandlerChannel,
syncP2PStore: SyncAndP2PStatusEventsStore
syncP2PStore: SyncAndP2PStatusEventsStore,
pushKeyRemoteChannel: PushKeyRemoteChannel
): EventProxy = EventHandler(
scope = scope,
logger = logger,
channel = channel,
syncP2PStore = syncP2PStore
syncP2PStore = syncP2PStore,
pushKeyRemoteChannel = pushKeyRemoteChannel
)
@JvmStatic

View file

@ -36,6 +36,7 @@ import com.anytypeio.anytype.di.feature.multiplayer.RequestJoinSpaceDependencies
import com.anytypeio.anytype.di.feature.multiplayer.ShareSpaceDependencies
import com.anytypeio.anytype.di.feature.multiplayer.SpaceJoinRequestDependencies
import com.anytypeio.anytype.di.feature.notifications.NotificationDependencies
import com.anytypeio.anytype.di.feature.notifications.PushContentDependencies
import com.anytypeio.anytype.di.feature.objects.SelectObjectTypeDependencies
import com.anytypeio.anytype.di.feature.onboarding.OnboardingDependencies
import com.anytypeio.anytype.di.feature.onboarding.OnboardingStartDependencies
@ -143,7 +144,8 @@ interface MainComponent :
DebugDependencies,
CreateObjectTypeDependencies,
SpaceTypesDependencies,
SpacePropertiesDependencies
SpacePropertiesDependencies,
PushContentDependencies
{
fun inject(app: AndroidApplication)
@ -414,4 +416,9 @@ abstract class ComponentDependenciesModule {
@IntoMap
@ComponentDependenciesKey(SpacePropertiesDependencies::class)
abstract fun provideSpacePropertiesDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(PushContentDependencies::class)
abstract fun providePushContentDependencies(component: MainComponent): ComponentDependencies
}

View file

@ -1,6 +1,8 @@
package com.anytypeio.anytype.di.main
import android.content.SharedPreferences
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.device.DeviceTokenStoringServiceImpl
import com.anytypeio.anytype.di.main.ConfigModule.DEFAULT_APP_COROUTINE_SCOPE
import com.anytypeio.anytype.domain.account.AwaitAccountStartManager
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
@ -11,12 +13,14 @@ import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.debugging.DebugAccountSelectTrace
import com.anytypeio.anytype.domain.debugging.Logger
import com.anytypeio.anytype.domain.device.NetworkConnectionStatus
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.DefaultUserPermissionProvider
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.notifications.RegisterDeviceToken
import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
@ -223,14 +227,16 @@ object SubscriptionsModule {
permissions: UserPermissionProvider,
isSpaceDeleted: SpaceDeletedStatusWatcher,
profileSubscriptionManager: ProfileSubscriptionManager,
networkConnectionStatus: NetworkConnectionStatus
networkConnectionStatus: NetworkConnectionStatus,
deviceTokenStoringService: DeviceTokenStoringService
): GlobalSubscriptionManager = GlobalSubscriptionManager.Default(
types = types,
relations = relations,
permissions = permissions,
isSpaceDeleted = isSpaceDeleted,
profile = profileSubscriptionManager,
networkConnectionStatus = networkConnectionStatus
networkConnectionStatus = networkConnectionStatus,
deviceTokenStoringService = deviceTokenStoringService
)
@JvmStatic
@ -259,4 +265,19 @@ object SubscriptionsModule {
repo = repo,
dispatchers = dispatchers
)
@JvmStatic
@Provides
@Singleton
fun deviceTokenStoreService(
@Named("encrypted") sharedPreferences: SharedPreferences,
registerDeviceToken: RegisterDeviceToken,
dispatchers: AppCoroutineDispatchers,
@Named(DEFAULT_APP_COROUTINE_SCOPE) scope: CoroutineScope
): DeviceTokenStoringService = DeviceTokenStoringServiceImpl(
sharedPreferences = sharedPreferences,
registerDeviceToken = registerDeviceToken,
dispatchers = dispatchers,
scope = scope
)
}

View file

@ -710,4 +710,8 @@ sealed class Command {
val blockId: Id,
val properties: List<Key>
) : Command()
data class RegisterDeviceToken(
val token: String
) : Command()
}

View file

@ -105,4 +105,8 @@ class AuthCacheDataStore(private val cache: AuthCache) : AuthDataStore {
override suspend fun cancelAccountMigration(account: Id) {
throw UnsupportedOperationException()
}
override suspend fun registerDeviceToken(request: Command.RegisterDeviceToken) {
throw UnsupportedOperationException()
}
}

View file

@ -121,4 +121,8 @@ class AuthDataRepository(
override suspend fun debugExportLogs(dir: String): String {
return factory.remote.debugExportLogs(dir)
}
override suspend fun registerDeviceToken(command: Command.RegisterDeviceToken) {
factory.remote.registerDeviceToken(command)
}
}

View file

@ -47,4 +47,6 @@ interface AuthDataStore {
suspend fun getNetworkMode(): NetworkModeConfig
suspend fun setNetworkMode(modeConfig: NetworkModeConfig)
suspend fun debugExportLogs(dir: String): String
suspend fun registerDeviceToken(request: Command.RegisterDeviceToken)
}

View file

@ -28,4 +28,6 @@ interface AuthRemote {
suspend fun getVersion(): String
suspend fun setInitialParams(command: Command.SetInitialParams)
suspend fun debugExportLogs(dir: String): String
suspend fun registerDeviceToken(command: Command.RegisterDeviceToken)
}

View file

@ -110,4 +110,8 @@ class AuthRemoteDataStore(
override suspend fun debugExportLogs(dir: String): String {
return authRemote.debugExportLogs(dir)
}
override suspend fun registerDeviceToken(command: Command.RegisterDeviceToken) {
authRemote.registerDeviceToken(command)
}
}

View file

@ -0,0 +1,54 @@
package com.anytypeio.anytype.device
import android.content.SharedPreferences
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.anytypeio.anytype.domain.notifications.RegisterDeviceToken
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
class DeviceTokenStoringServiceImpl @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val registerDeviceToken: RegisterDeviceToken,
private val dispatchers: AppCoroutineDispatchers,
private val scope: CoroutineScope
) : DeviceTokenStoringService {
override fun saveToken(token: String) {
scope.launch(dispatchers.io) {
Timber.d("Saving token: $token")
sharedPreferences.edit().apply {
putString(PREF_KEY, token)
apply()
}
}
}
override fun start() {
val token = sharedPreferences.getString(PREF_KEY, null)
if (!token.isNullOrEmpty()) {
scope.launch(dispatchers.io) {
val params = RegisterDeviceToken.Params(token = token)
registerDeviceToken.async(params).fold(
onSuccess = {
Timber.d("Successfully registered token: $token")
},
onFailure = { error ->
Timber.w("Failed to register token: $token, error: $error")
}
)
}
}
}
override fun stop() {
// Nothing to do here
}
companion object {
private const val PREF_KEY = "prefs.device_token"
}
}

View file

@ -64,4 +64,6 @@ interface AuthRepository {
suspend fun getNetworkMode(): NetworkModeConfig
suspend fun setNetworkMode(modeConfig: NetworkModeConfig)
suspend fun debugExportLogs(dir: String): String
suspend fun registerDeviceToken(command: Command.RegisterDeviceToken)
}

View file

@ -0,0 +1,7 @@
package com.anytypeio.anytype.domain.device
interface DeviceTokenStoringService {
fun saveToken(token: String)
fun start()
fun stop()
}

View file

@ -0,0 +1,24 @@
package com.anytypeio.anytype.domain.notifications
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import javax.inject.Inject
class RegisterDeviceToken @Inject constructor(
private val repository: AuthRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<RegisterDeviceToken.Params, Unit>(dispatchers.io) {
override suspend fun doWork(params: Params) {
val command = Command.RegisterDeviceToken(
token = params.token,
)
repository.registerDeviceToken(command = command)
}
data class Params(
val token: String
)
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.domain.subscriptions
import com.anytypeio.anytype.domain.device.DeviceTokenStoringService
import com.anytypeio.anytype.domain.device.NetworkConnectionStatus
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionManager
@ -19,7 +20,8 @@ interface GlobalSubscriptionManager {
private val permissions: UserPermissionProvider,
private val isSpaceDeleted: SpaceDeletedStatusWatcher,
private val profile: ProfileSubscriptionManager,
private val networkConnectionStatus: NetworkConnectionStatus
private val networkConnectionStatus: NetworkConnectionStatus,
private val deviceTokenStoringService: DeviceTokenStoringService
) : GlobalSubscriptionManager {
override fun onStart() {
@ -29,6 +31,7 @@ interface GlobalSubscriptionManager {
isSpaceDeleted.onStart()
profile.onStart()
networkConnectionStatus.start()
deviceTokenStoringService.start()
}
override fun onStop() {
@ -38,6 +41,7 @@ interface GlobalSubscriptionManager {
isSpaceDeleted.onStop()
profile.onStop()
networkConnectionStatus.stop()
deviceTokenStoringService.stop()
}
}

View file

@ -62,6 +62,7 @@ dataStoreVersion = '1.1.4'
amplitudeVersion = '3.35.1'
coilComposeVersion = '3.1.0'
sentryVersion = '7.13.0'
firebaseBomVersion = "33.13.0"
composeQrCodeVersion = '1.0.1'
fragmentComposeVersion = "1.8.6"
@ -153,12 +154,14 @@ navigationCompose = { module = "androidx.navigation:navigation-compose", version
composeQrCode = { module = "com.lightspark:compose-qr-code", version.ref = "composeQrCodeVersion" }
playBilling = { module = "com.android.billingclient:billing", version = "7.1.1" }
fragmentCompose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "fragmentComposeVersion" }
firebaseBom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBomVersion" }
firebaseMessaging = { module = "com.google.firebase:firebase-messaging"}
[bundles]
[plugins]
application = { id = "com.android.application", version = "8.8.2" }
library = { id = "com.android.library", version = "8.8.2" }
application = { id = "com.android.application", version = "8.9.1" }
library = { id = "com.android.library", version = "8.9.1" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinVersion" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlinVersion" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaVersion" }

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View file

@ -96,4 +96,8 @@ class AuthMiddleware(
override suspend fun debugExportLogs(dir: String): String {
return middleware.debugExportLogs(dir)
}
override suspend fun registerDeviceToken(command: Command.RegisterDeviceToken) {
middleware.registerDeviceToken(command = command)
}
}

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.middleware.interactor
import anytype.Event
import com.anytypeio.anytype.data.auth.event.PushKeyRemoteChannel
import com.anytypeio.anytype.data.auth.status.SyncAndP2PStatusEventsStore
import com.anytypeio.anytype.middleware.EventProxy
import java.io.IOException
@ -17,10 +18,14 @@ class EventHandler @Inject constructor(
private val logger: MiddlewareProtobufLogger,
private val scope: CoroutineScope,
private val channel: EventHandlerChannel,
private val syncP2PStore: SyncAndP2PStatusEventsStore
private val syncP2PStore: SyncAndP2PStatusEventsStore,
private val pushKeyRemoteChannel: PushKeyRemoteChannel
) : EventProxy {
init {
scope.launch {
pushKeyRemoteChannel.start()
}
scope.launch {
syncP2PStore.start()
}

View file

@ -2,6 +2,7 @@ package com.anytypeio.anytype.middleware.interactor
import anytype.Rpc
import anytype.Rpc.Chat.ReadMessages.ReadType
import anytype.Rpc.PushNotification.RegisterToken.Platform
import anytype.model.Block
import anytype.model.ParticipantPermissionChange
import anytype.model.Range
@ -2978,6 +2979,17 @@ class Middleware @Inject constructor(
return response.event.toPayload()
}
@Throws(Exception::class)
fun registerDeviceToken(command: Command.RegisterDeviceToken) {
val request = Rpc.PushNotification.RegisterToken.Request(
token = command.token,
platform = Platform.Android
)
logRequestIfDebug(request)
val (response, time) = measureTimedValue { service.pushNotificationRegisterToken(request) }
logResponseIfDebug(response, time)
}
private fun logRequestIfDebug(request: Any) {
if (BuildConfig.DEBUG) {
logger.logRequest(request).also {

View file

@ -634,4 +634,7 @@ interface MiddlewareService {
@Throws(Exception::class)
fun blockDataViewRelationSet(request: Rpc.BlockDataview.Relation.Set.Request): Rpc.BlockDataview.Relation.Set.Response
@Throws(Exception::class)
fun pushNotificationRegisterToken(request: Rpc.PushNotification.RegisterToken.Request): Rpc.PushNotification.RegisterToken.Response
}

View file

@ -2573,4 +2573,17 @@ class MiddlewareServiceImplementation @Inject constructor(
return response
}
}
override fun pushNotificationRegisterToken(request: Rpc.PushNotification.RegisterToken.Request): Rpc.PushNotification.RegisterToken.Response {
val encoded = Service.pushNotificationRegisterToken(
Rpc.PushNotification.RegisterToken.Request.ADAPTER.encode(request)
)
val response = Rpc.PushNotification.RegisterToken.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.PushNotification.RegisterToken.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
}