android: sync FCM token with backend notifications API
This commit is contained in:
@@ -8,11 +8,17 @@ import coil.memory.MemoryCache
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import ru.daemonlord.messenger.core.notifications.NotificationChannels
|
||||
import ru.daemonlord.messenger.push.PushTokenSyncManager
|
||||
import java.io.File
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class MessengerApplication : Application(), ImageLoaderFactory {
|
||||
|
||||
@Inject
|
||||
lateinit var pushTokenSyncManager: PushTokenSyncManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) {
|
||||
@@ -20,6 +26,7 @@ class MessengerApplication : Application(), ImageLoaderFactory {
|
||||
}
|
||||
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
|
||||
NotificationChannels.ensureCreated(this)
|
||||
pushTokenSyncManager.triggerBestEffortSync()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
|
||||
@@ -17,6 +17,7 @@ import ru.daemonlord.messenger.domain.auth.model.AuthSession
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import ru.daemonlord.messenger.push.PushTokenSyncManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -24,6 +25,7 @@ import javax.inject.Singleton
|
||||
class NetworkAuthRepository @Inject constructor(
|
||||
private val authApiService: AuthApiService,
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val pushTokenSyncManager: PushTokenSyncManager,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : AuthRepository {
|
||||
|
||||
@@ -42,6 +44,7 @@ class NetworkAuthRepository @Inject constructor(
|
||||
savedAtMillis = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
pushTokenSyncManager.triggerBestEffortSync()
|
||||
getMe()
|
||||
} catch (error: Throwable) {
|
||||
AppResult.Error(error.toAppError(mode = ApiErrorMode.LOGIN))
|
||||
@@ -62,6 +65,7 @@ class NetworkAuthRepository @Inject constructor(
|
||||
savedAtMillis = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
pushTokenSyncManager.triggerBestEffortSync()
|
||||
AppResult.Success(Unit)
|
||||
} catch (error: Throwable) {
|
||||
tokenRepository.clearTokens()
|
||||
@@ -125,6 +129,7 @@ class NetworkAuthRepository @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
pushTokenSyncManager.unregisterCurrentTokenOnLogout()
|
||||
tokenRepository.clearTokens()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.daemonlord.messenger.data.notifications.api
|
||||
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.POST
|
||||
import ru.daemonlord.messenger.data.notifications.dto.PushTokenDeleteRequestDto
|
||||
import ru.daemonlord.messenger.data.notifications.dto.PushTokenUpsertRequestDto
|
||||
|
||||
interface PushTokenApiService {
|
||||
@POST("/api/v1/notifications/push-token")
|
||||
suspend fun upsert(@Body request: PushTokenUpsertRequestDto)
|
||||
|
||||
@HTTP(method = "DELETE", path = "/api/v1/notifications/push-token", hasBody = true)
|
||||
suspend fun delete(@Body request: PushTokenDeleteRequestDto)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.daemonlord.messenger.data.notifications.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PushTokenUpsertRequestDto(
|
||||
@SerialName("platform")
|
||||
val platform: String,
|
||||
@SerialName("token")
|
||||
val token: String,
|
||||
@SerialName("device_id")
|
||||
val deviceId: String? = null,
|
||||
@SerialName("app_version")
|
||||
val appVersion: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushTokenDeleteRequestDto(
|
||||
@SerialName("platform")
|
||||
val platform: String,
|
||||
@SerialName("token")
|
||||
val token: String,
|
||||
)
|
||||
@@ -19,6 +19,7 @@ import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
||||
import ru.daemonlord.messenger.data.chat.api.ChatApiService
|
||||
import ru.daemonlord.messenger.data.media.api.MediaApiService
|
||||
import ru.daemonlord.messenger.data.message.api.MessageApiService
|
||||
import ru.daemonlord.messenger.data.notifications.api.PushTokenApiService
|
||||
import ru.daemonlord.messenger.data.search.api.SearchApiService
|
||||
import ru.daemonlord.messenger.data.user.api.UserApiService
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
@@ -154,4 +155,10 @@ object NetworkModule {
|
||||
fun provideSearchApiService(retrofit: Retrofit): SearchApiService {
|
||||
return retrofit.create(SearchApiService::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePushTokenApiService(retrofit: Retrofit): PushTokenApiService {
|
||||
return retrofit.create(PushTokenApiService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ class MessengerFirebaseMessagingService : FirebaseMessagingService() {
|
||||
@Inject
|
||||
lateinit var notificationDispatcher: NotificationDispatcher
|
||||
|
||||
@Inject
|
||||
lateinit var pushTokenSyncManager: PushTokenSyncManager
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
val payload = PushPayloadParser.parse(message) ?: return
|
||||
notificationDispatcher.showChatMessage(payload)
|
||||
@@ -20,5 +23,6 @@ class MessengerFirebaseMessagingService : FirebaseMessagingService() {
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
Log.i("MessengerPush", "FCM token refreshed (${token.take(10)}...)")
|
||||
pushTokenSyncManager.onNewToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package ru.daemonlord.messenger.push
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.daemonlord.messenger.BuildConfig
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.data.notifications.api.PushTokenApiService
|
||||
import ru.daemonlord.messenger.data.notifications.dto.PushTokenDeleteRequestDto
|
||||
import ru.daemonlord.messenger.data.notifications.dto.PushTokenUpsertRequestDto
|
||||
import ru.daemonlord.messenger.di.IoDispatcher
|
||||
import ru.daemonlord.messenger.di.TokenPrefs
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PushTokenSyncManager @Inject constructor(
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val pushTokenApiService: PushTokenApiService,
|
||||
@TokenPrefs private val securePrefs: SharedPreferences,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
||||
|
||||
fun onNewToken(token: String) {
|
||||
val cleaned = token.trim()
|
||||
if (cleaned.isBlank()) {
|
||||
return
|
||||
}
|
||||
securePrefs.edit().putString(KEY_LAST_FCM_TOKEN, cleaned).apply()
|
||||
scope.launch {
|
||||
registerTokenIfPossible(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerBestEffortSync() {
|
||||
val cached = securePrefs.getString(KEY_LAST_FCM_TOKEN, null)?.trim().orEmpty()
|
||||
if (cached.isNotBlank()) {
|
||||
scope.launch {
|
||||
registerTokenIfPossible(cached)
|
||||
}
|
||||
}
|
||||
FirebaseMessaging.getInstance().token
|
||||
.addOnSuccessListener { token -> onNewToken(token) }
|
||||
.addOnFailureListener { error ->
|
||||
Timber.w(error, "Failed to fetch FCM token for sync")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unregisterCurrentTokenOnLogout() {
|
||||
val token = securePrefs.getString(KEY_LAST_FCM_TOKEN, null)?.trim().orEmpty()
|
||||
if (token.isBlank()) {
|
||||
return
|
||||
}
|
||||
val hasTokens = tokenRepository.getTokens() != null
|
||||
if (!hasTokens) {
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
pushTokenApiService.delete(
|
||||
request = PushTokenDeleteRequestDto(
|
||||
platform = PLATFORM_ANDROID,
|
||||
token = token,
|
||||
)
|
||||
)
|
||||
}.onFailure { error ->
|
||||
Timber.w(error, "Failed to unregister push token on logout")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun registerTokenIfPossible(token: String) {
|
||||
val hasTokens = tokenRepository.getTokens() != null
|
||||
if (!hasTokens) {
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
pushTokenApiService.upsert(
|
||||
request = PushTokenUpsertRequestDto(
|
||||
platform = PLATFORM_ANDROID,
|
||||
token = token,
|
||||
deviceId = null,
|
||||
appVersion = BuildConfig.VERSION_NAME,
|
||||
)
|
||||
)
|
||||
}.onFailure { error ->
|
||||
Timber.w(error, "Failed to sync push token")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_LAST_FCM_TOKEN = "last_fcm_token"
|
||||
const val PLATFORM_ANDROID = "android"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user