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 com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import ru.daemonlord.messenger.core.notifications.NotificationChannels
|
import ru.daemonlord.messenger.core.notifications.NotificationChannels
|
||||||
|
import ru.daemonlord.messenger.push.PushTokenSyncManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class MessengerApplication : Application(), ImageLoaderFactory {
|
class MessengerApplication : Application(), ImageLoaderFactory {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var pushTokenSyncManager: PushTokenSyncManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
@@ -20,6 +26,7 @@ class MessengerApplication : Application(), ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
|
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
|
||||||
NotificationChannels.ensureCreated(this)
|
NotificationChannels.ensureCreated(this)
|
||||||
|
pushTokenSyncManager.triggerBestEffortSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
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.auth.repository.AuthRepository
|
||||||
import ru.daemonlord.messenger.domain.common.AppError
|
import ru.daemonlord.messenger.domain.common.AppError
|
||||||
import ru.daemonlord.messenger.domain.common.AppResult
|
import ru.daemonlord.messenger.domain.common.AppResult
|
||||||
|
import ru.daemonlord.messenger.push.PushTokenSyncManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ import javax.inject.Singleton
|
|||||||
class NetworkAuthRepository @Inject constructor(
|
class NetworkAuthRepository @Inject constructor(
|
||||||
private val authApiService: AuthApiService,
|
private val authApiService: AuthApiService,
|
||||||
private val tokenRepository: TokenRepository,
|
private val tokenRepository: TokenRepository,
|
||||||
|
private val pushTokenSyncManager: PushTokenSyncManager,
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : AuthRepository {
|
) : AuthRepository {
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
savedAtMillis = System.currentTimeMillis(),
|
savedAtMillis = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
pushTokenSyncManager.triggerBestEffortSync()
|
||||||
getMe()
|
getMe()
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
AppResult.Error(error.toAppError(mode = ApiErrorMode.LOGIN))
|
AppResult.Error(error.toAppError(mode = ApiErrorMode.LOGIN))
|
||||||
@@ -62,6 +65,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
savedAtMillis = System.currentTimeMillis(),
|
savedAtMillis = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
pushTokenSyncManager.triggerBestEffortSync()
|
||||||
AppResult.Success(Unit)
|
AppResult.Success(Unit)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
tokenRepository.clearTokens()
|
tokenRepository.clearTokens()
|
||||||
@@ -125,6 +129,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout() {
|
override suspend fun logout() {
|
||||||
|
pushTokenSyncManager.unregisterCurrentTokenOnLogout()
|
||||||
tokenRepository.clearTokens()
|
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.chat.api.ChatApiService
|
||||||
import ru.daemonlord.messenger.data.media.api.MediaApiService
|
import ru.daemonlord.messenger.data.media.api.MediaApiService
|
||||||
import ru.daemonlord.messenger.data.message.api.MessageApiService
|
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.search.api.SearchApiService
|
||||||
import ru.daemonlord.messenger.data.user.api.UserApiService
|
import ru.daemonlord.messenger.data.user.api.UserApiService
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
@@ -154,4 +155,10 @@ object NetworkModule {
|
|||||||
fun provideSearchApiService(retrofit: Retrofit): SearchApiService {
|
fun provideSearchApiService(retrofit: Retrofit): SearchApiService {
|
||||||
return retrofit.create(SearchApiService::class.java)
|
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
|
@Inject
|
||||||
lateinit var notificationDispatcher: NotificationDispatcher
|
lateinit var notificationDispatcher: NotificationDispatcher
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var pushTokenSyncManager: PushTokenSyncManager
|
||||||
|
|
||||||
override fun onMessageReceived(message: RemoteMessage) {
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
val payload = PushPayloadParser.parse(message) ?: return
|
val payload = PushPayloadParser.parse(message) ?: return
|
||||||
notificationDispatcher.showChatMessage(payload)
|
notificationDispatcher.showChatMessage(payload)
|
||||||
@@ -20,5 +23,6 @@ class MessengerFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
Log.i("MessengerPush", "FCM token refreshed (${token.take(10)}...)")
|
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