Update dependency com.google.firebase:firebase-bom to v34.15.0 (#7089)
* Update dependency com.google.firebase:firebase-bom to v34.15.0 * Replace deprecated FCM `token` usages with `installationId` Renamed a few components, changed the behaviour of `FirebaseTokenRotator` so it doesn't save the new token, `FirebaseMessagingService` will do it in its `onRegistered` method * Enable firebase messaging installation_id * Make sure we delete the FID when unregistering the app from Firebase * Add `runFirebaseTask` util to convert `Task<T>` into a `Result<T>` returned from a suspending function Also, rename the use cases and make them use the `invoke` pattern --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín <jorgem@element.io> Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
@@ -84,7 +84,7 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin
|
||||
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
||||
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:34.14.1"
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:34.15.0"
|
||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="firebase_messaging_installation_id_enabled"
|
||||
android:value="true" />
|
||||
|
||||
<!-- Firebase components -->
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
|
||||
+9
-9
@@ -20,26 +20,26 @@ import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.api.toUserList
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("FirebaseNewTokenHandler", LoggerTag.PushLoggerTag)
|
||||
private val loggerTag = LoggerTag("FirebaseNewInstallationIdHandler", LoggerTag.PushLoggerTag)
|
||||
|
||||
/**
|
||||
* Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider.
|
||||
* Handle new installationId received from Firebase. Will update all the sessions which are using Firebase as a push provider.
|
||||
*/
|
||||
interface FirebaseNewTokenHandler {
|
||||
suspend fun handle(firebaseToken: String)
|
||||
interface FirebaseNewInstallationIdHandler {
|
||||
suspend fun handle(installationId: String)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseNewTokenHandler(
|
||||
class DefaultFirebaseNewInstallationIdHandler(
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
private val sessionStore: SessionStore,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val firebaseStore: FirebaseStore,
|
||||
private val firebaseGatewayProvider: FirebaseGatewayProvider,
|
||||
) : FirebaseNewTokenHandler {
|
||||
override suspend fun handle(firebaseToken: String) {
|
||||
firebaseStore.storeFcmToken(firebaseToken)
|
||||
) : FirebaseNewInstallationIdHandler {
|
||||
override suspend fun handle(installationId: String) {
|
||||
firebaseStore.storeInstallationId(installationId)
|
||||
// Register the pusher for all the sessions
|
||||
sessionStore.getAllSessions().toUserList()
|
||||
.map { SessionId(it) }
|
||||
@@ -55,7 +55,7 @@ class DefaultFirebaseNewTokenHandler(
|
||||
pusherSubscriber
|
||||
.registerPusher(
|
||||
matrixClient = client,
|
||||
pushKey = firebaseToken,
|
||||
pushKey = installationId,
|
||||
gateway = firebaseGatewayProvider.getFirebaseGateway(),
|
||||
)
|
||||
.onFailure {
|
||||
+5
-5
@@ -26,7 +26,7 @@ class FirebasePushProvider(
|
||||
private val firebaseStore: FirebaseStore,
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
private val isPlayServiceAvailable: IsPlayServiceAvailable,
|
||||
private val firebaseTokenRotator: FirebaseTokenRotator,
|
||||
private val rotateFirebaseSession: RotateFirebaseSession,
|
||||
private val firebaseGatewayProvider: FirebaseGatewayProvider,
|
||||
) : PushProvider {
|
||||
override val index = FirebaseConfig.INDEX
|
||||
@@ -40,7 +40,7 @@ class FirebasePushProvider(
|
||||
}
|
||||
|
||||
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
|
||||
val pushKey = firebaseStore.getFcmToken() ?: return Result.failure<Unit>(
|
||||
val pushKey = firebaseStore.getInstallationId() ?: return Result.failure<Unit>(
|
||||
IllegalStateException(
|
||||
"Unable to register pusher, Firebase token is not known."
|
||||
)
|
||||
@@ -59,7 +59,7 @@ class FirebasePushProvider(
|
||||
override suspend fun getCurrentDistributor(sessionId: SessionId) = firebaseDistributor
|
||||
|
||||
override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
|
||||
val pushKey = firebaseStore.getFcmToken()
|
||||
val pushKey = firebaseStore.getInstallationId()
|
||||
return if (pushKey == null) {
|
||||
Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.")
|
||||
Result.success(Unit)
|
||||
@@ -74,7 +74,7 @@ class FirebasePushProvider(
|
||||
override suspend fun onSessionDeleted(sessionId: SessionId) = Unit
|
||||
|
||||
override suspend fun getPushConfig(sessionId: SessionId): Config? {
|
||||
return firebaseStore.getFcmToken()?.let { fcmToken ->
|
||||
return firebaseStore.getInstallationId()?.let { fcmToken ->
|
||||
Config(
|
||||
url = firebaseGatewayProvider.getFirebaseGateway(),
|
||||
pushKey = fcmToken
|
||||
@@ -85,7 +85,7 @@ class FirebasePushProvider(
|
||||
override fun canRotateToken(): Boolean = true
|
||||
|
||||
override suspend fun rotateToken(): Result<Unit> {
|
||||
return firebaseTokenRotator.rotate()
|
||||
return rotateFirebaseSession()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+13
-13
@@ -18,28 +18,28 @@ import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
/**
|
||||
* This class store the Firebase token in SharedPrefs.
|
||||
* This class stores the Firebase installationId in SharedPrefs.
|
||||
*/
|
||||
interface FirebaseStore {
|
||||
fun getFcmToken(): String?
|
||||
fun fcmTokenFlow(): Flow<String?>
|
||||
fun storeFcmToken(token: String?)
|
||||
fun getInstallationId(): String?
|
||||
fun fcmInstallationIdFlow(): Flow<String?>
|
||||
fun storeInstallationId(installationId: String?)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class SharedPreferencesFirebaseStore(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) : FirebaseStore {
|
||||
override fun getFcmToken(): String? {
|
||||
return sharedPreferences.getString(PREFS_KEY_FCM_TOKEN, null)
|
||||
override fun getInstallationId(): String? {
|
||||
return sharedPreferences.getString(PREFS_KEY_FCM_INSTALLATION_ID, null)
|
||||
}
|
||||
|
||||
override fun fcmTokenFlow(): Flow<String?> {
|
||||
val flow = MutableStateFlow(getFcmToken())
|
||||
override fun fcmInstallationIdFlow(): Flow<String?> {
|
||||
val flow = MutableStateFlow(getInstallationId())
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, k ->
|
||||
if (k == PREFS_KEY_FCM_TOKEN) {
|
||||
if (k == PREFS_KEY_FCM_INSTALLATION_ID) {
|
||||
try {
|
||||
flow.value = getFcmToken()
|
||||
flow.value = getInstallationId()
|
||||
} catch (_: Exception) {
|
||||
flow.value = null
|
||||
}
|
||||
@@ -50,13 +50,13 @@ class SharedPreferencesFirebaseStore(
|
||||
.onCompletion { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}
|
||||
|
||||
override fun storeFcmToken(token: String?) {
|
||||
override fun storeInstallationId(installationId: String?) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_KEY_FCM_TOKEN, token)
|
||||
putString(PREFS_KEY_FCM_INSTALLATION_ID, installationId)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
|
||||
private const val PREFS_KEY_FCM_INSTALLATION_ID = "FCM_TOKEN"
|
||||
}
|
||||
}
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface FirebaseTokenDeleter {
|
||||
/**
|
||||
* Deletes the current Firebase token.
|
||||
*/
|
||||
suspend fun delete()
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseTokenDeleter(
|
||||
private val isPlayServiceAvailable: IsPlayServiceAvailable,
|
||||
) : FirebaseTokenDeleter {
|
||||
override suspend fun delete() {
|
||||
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||
isPlayServiceAvailable.checkAvailableOrThrow()
|
||||
suspendCoroutine { continuation ->
|
||||
try {
|
||||
FirebaseMessaging.getInstance().deleteToken()
|
||||
.addOnSuccessListener {
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Timber.e(e, "## deleteFirebaseToken() : failed")
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## deleteFirebaseToken() : failed")
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface FirebaseTokenGetter {
|
||||
/**
|
||||
* Read the current Firebase token from FirebaseMessaging.
|
||||
* If the token does not exist, it will be generated.
|
||||
*/
|
||||
suspend fun get(): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseTokenGetter(
|
||||
private val isPlayServiceAvailable: IsPlayServiceAvailable,
|
||||
) : FirebaseTokenGetter {
|
||||
override suspend fun get(): String {
|
||||
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||
isPlayServiceAvailable.checkAvailableOrThrow()
|
||||
return suspendCoroutine { continuation ->
|
||||
try {
|
||||
FirebaseMessaging.getInstance().token
|
||||
.addOnSuccessListener { token ->
|
||||
continuation.resume(token)
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Timber.e(e, "## retrievedFirebaseToken() : failed")
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## retrievedFirebaseToken() : failed")
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
|
||||
interface FirebaseTokenRotator {
|
||||
suspend fun rotate(): Result<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
* This class delete the Firebase token and generate a new one.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseTokenRotator(
|
||||
private val firebaseTokenDeleter: FirebaseTokenDeleter,
|
||||
private val firebaseTokenGetter: FirebaseTokenGetter,
|
||||
) : FirebaseTokenRotator {
|
||||
override suspend fun rotate(): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
firebaseTokenDeleter.delete()
|
||||
firebaseTokenGetter.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-7
@@ -10,7 +10,6 @@ package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
|
||||
interface FirebaseTroubleshooter {
|
||||
suspend fun troubleshoot(): Result<Unit>
|
||||
@@ -21,13 +20,9 @@ interface FirebaseTroubleshooter {
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseTroubleshooter(
|
||||
private val newTokenHandler: FirebaseNewTokenHandler,
|
||||
private val firebaseTokenGetter: FirebaseTokenGetter,
|
||||
private val rotateFirebaseSession: RotateFirebaseSession,
|
||||
) : FirebaseTroubleshooter {
|
||||
override suspend fun troubleshoot(): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
val token = firebaseTokenGetter.get()
|
||||
newTokenHandler.handle(token)
|
||||
}
|
||||
return rotateFirebaseSession()
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.pushproviders.firebase.util.runFirebaseTask
|
||||
import timber.log.Timber
|
||||
|
||||
fun interface RegisterFirebaseSession {
|
||||
/**
|
||||
* Register the device to Firebase Messaging.
|
||||
*/
|
||||
suspend operator fun invoke(): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRegisterFirebaseSession(
|
||||
private val isPlayServiceAvailable: IsPlayServiceAvailable,
|
||||
) : RegisterFirebaseSession {
|
||||
override suspend operator fun invoke(): Result<Unit> {
|
||||
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||
isPlayServiceAvailable.checkAvailableOrThrow()
|
||||
return runFirebaseTask { FirebaseMessaging.getInstance().register() }
|
||||
.onFailure { Timber.e(it, "## registerFirebaseMessaging() : failed") }
|
||||
// Change return type from Void! to Unit
|
||||
.map {}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
|
||||
fun interface RotateFirebaseSession {
|
||||
suspend operator fun invoke(): Result<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
* This class deletes the Firebase installation id and generates a new one.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRotateFirebaseSession(
|
||||
private val registerFirebaseSession: RegisterFirebaseSession,
|
||||
private val unregisterFirebaseSession: UnregisterFirebaseSession,
|
||||
) : RotateFirebaseSession {
|
||||
override suspend operator fun invoke(): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
// Stop the current session, which will also delete the existing installation id from Firebase
|
||||
unregisterFirebaseSession().getOrThrow()
|
||||
// Register again to get a new installation id
|
||||
registerFirebaseSession().getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import com.google.firebase.installations.FirebaseInstallations
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.pushproviders.firebase.util.runFirebaseTask
|
||||
import timber.log.Timber
|
||||
|
||||
fun interface UnregisterFirebaseSession {
|
||||
/**
|
||||
* Deletes the current Firebase token.
|
||||
*/
|
||||
suspend operator fun invoke(): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnregisterFirebaseSession(
|
||||
private val isPlayServiceAvailable: IsPlayServiceAvailable,
|
||||
) : UnregisterFirebaseSession {
|
||||
override suspend operator fun invoke(): Result<Unit> {
|
||||
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
|
||||
isPlayServiceAvailable.checkAvailableOrThrow()
|
||||
return runFirebaseTask {
|
||||
// Unregister the device from Firebase Messaging
|
||||
FirebaseMessaging.getInstance().unregister()
|
||||
// Also delete the existing installation id from Firebase
|
||||
.continueWithTask { FirebaseInstallations.getInstance().delete() }
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it, "## unregisterFirebaseMessaging() : failed")
|
||||
}
|
||||
// Change return type from Void! to Unit
|
||||
.map {}
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -24,7 +24,7 @@ import timber.log.Timber
|
||||
private val loggerTag = LoggerTag("VectorFirebaseMessagingService", LoggerTag.PushLoggerTag)
|
||||
|
||||
class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
|
||||
@Inject lateinit var firebaseNewInstallationIdHandler: FirebaseNewInstallationIdHandler
|
||||
@Inject lateinit var pushParser: FirebasePushParser
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
@Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager
|
||||
@@ -36,10 +36,12 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
bindings<VectorFirebaseMessagingServiceBindings>().inject(this)
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
Timber.tag(loggerTag.value).w("New Firebase token")
|
||||
override fun onRegistered(installationId: String) {
|
||||
super.onRegistered(installationId)
|
||||
|
||||
Timber.tag(loggerTag.value).w("New Firebase installation id")
|
||||
coroutineScope.launch {
|
||||
firebaseNewTokenHandler.handle(token)
|
||||
firebaseNewInstallationIdHandler.handle(installationId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ class FirebaseTokenTest(
|
||||
override suspend fun run(coroutineScope: CoroutineScope) {
|
||||
currentJob?.cancel()
|
||||
delegate.start()
|
||||
currentJob = firebaseStore.fcmTokenFlow()
|
||||
currentJob = firebaseStore.fcmInstallationIdFlow()
|
||||
.onEach { token ->
|
||||
if (token != null) {
|
||||
delegate.updateState(
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase.util
|
||||
|
||||
import com.google.android.gms.tasks.Task
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
suspend fun <T> runFirebaseTaskWithResult(closure: () -> Task<T>): Result<T> {
|
||||
return runCatchingExceptions {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
try {
|
||||
closure().addOnSuccessListener {
|
||||
continuation.resume(it)
|
||||
}.addOnFailureListener { e ->
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for `Void!` return type, which is not a valid Kotlin type. We convert it to `Unit` instead.
|
||||
suspend fun runFirebaseTask(closure: () -> Task<Void?>): Result<Unit> {
|
||||
return runFirebaseTaskWithResult<Unit> { closure().continueWith {} }
|
||||
}
|
||||
+5
-5
@@ -30,16 +30,16 @@ import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultFirebaseNewTokenHandlerTest {
|
||||
class DefaultFirebaseNewInstallationIdHandlerTest {
|
||||
@Test
|
||||
fun `when a new token is received it is stored in the firebase store`() = runTest {
|
||||
val firebaseStore = InMemoryFirebaseStore()
|
||||
assertThat(firebaseStore.getFcmToken()).isNull()
|
||||
assertThat(firebaseStore.getInstallationId()).isNull()
|
||||
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
|
||||
firebaseStore = firebaseStore,
|
||||
)
|
||||
firebaseNewTokenHandler.handle("aToken")
|
||||
assertThat(firebaseStore.getFcmToken()).isEqualTo("aToken")
|
||||
assertThat(firebaseStore.getInstallationId()).isEqualTo("aToken")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,8 +142,8 @@ class DefaultFirebaseNewTokenHandlerTest {
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
firebaseStore: FirebaseStore = InMemoryFirebaseStore(),
|
||||
firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider(),
|
||||
): FirebaseNewTokenHandler {
|
||||
return DefaultFirebaseNewTokenHandler(
|
||||
): FirebaseNewInstallationIdHandler {
|
||||
return DefaultFirebaseNewInstallationIdHandler(
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
sessionStore = sessionStore,
|
||||
userPushStoreFactory = userPushStoreFactory,
|
||||
+4
-4
@@ -10,10 +10,10 @@ package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeFirebaseNewTokenHandler(
|
||||
class FakeFirebaseNewInstallationIdHandler(
|
||||
private val handleResult: (String) -> Unit = { lambdaError() }
|
||||
) : FirebaseNewTokenHandler {
|
||||
override suspend fun handle(firebaseToken: String) {
|
||||
handleResult(firebaseToken)
|
||||
) : FirebaseNewInstallationIdHandler {
|
||||
override suspend fun handle(installationId: String) {
|
||||
handleResult(installationId)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -10,10 +10,10 @@ package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeFirebaseTokenRotator(
|
||||
class FakeRotateFirebaseSession(
|
||||
private val rotateWithResult: () -> Result<Unit> = { lambdaError() }
|
||||
) : FirebaseTokenRotator {
|
||||
override suspend fun rotate(): Result<Unit> {
|
||||
) : RotateFirebaseSession {
|
||||
override suspend fun invoke(): Result<Unit> {
|
||||
return rotateWithResult()
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -61,7 +61,7 @@ class FirebasePushProviderTest {
|
||||
val registerPusherResultLambda = lambdaRecorder<MatrixClient, String, String, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
installationId = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
registerPusherResult = registerPusherResultLambda
|
||||
@@ -78,7 +78,7 @@ class FirebasePushProviderTest {
|
||||
fun `register ko no token`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = null
|
||||
installationId = null
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
registerPusherResult = { _, _, _ -> Result.success(Unit) }
|
||||
@@ -92,7 +92,7 @@ class FirebasePushProviderTest {
|
||||
fun `register ko error`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
installationId = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
registerPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
@@ -108,7 +108,7 @@ class FirebasePushProviderTest {
|
||||
val unregisterPusherResultLambda = lambdaRecorder<MatrixClient, String, String, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
installationId = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
unregisterPusherResult = unregisterPusherResultLambda
|
||||
@@ -125,7 +125,7 @@ class FirebasePushProviderTest {
|
||||
fun `unregister no token - in this case, the error is ignored`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = null
|
||||
installationId = null
|
||||
),
|
||||
)
|
||||
val result = firebasePushProvider.unregister(FakeMatrixClient())
|
||||
@@ -136,7 +136,7 @@ class FirebasePushProviderTest {
|
||||
fun `unregister ko error`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
installationId = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
unregisterPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
@@ -150,7 +150,7 @@ class FirebasePushProviderTest {
|
||||
fun `getCurrentUserPushConfig no push ket`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = null
|
||||
installationId = null
|
||||
)
|
||||
)
|
||||
val result = firebasePushProvider.getPushConfig(A_SESSION_ID)
|
||||
@@ -161,7 +161,7 @@ class FirebasePushProviderTest {
|
||||
fun `getCurrentUserPushConfig ok`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
installationId = "aToken"
|
||||
),
|
||||
)
|
||||
val result = firebasePushProvider.getPushConfig(A_SESSION_ID)
|
||||
@@ -169,10 +169,10 @@ class FirebasePushProviderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotateToken invokes the FirebaseTokenRotator`() = runTest {
|
||||
fun `rotateToken invokes the FirebaseMessagingSessionRotator`() = runTest {
|
||||
val lambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseTokenRotator = FakeFirebaseTokenRotator(lambda),
|
||||
rotateFirebaseSession = FakeRotateFirebaseSession(lambda),
|
||||
)
|
||||
firebasePushProvider.rotateToken()
|
||||
lambda.assertions().isCalledOnce()
|
||||
@@ -194,14 +194,14 @@ class FirebasePushProviderTest {
|
||||
firebaseStore: FirebaseStore = InMemoryFirebaseStore(),
|
||||
pusherSubscriber: PusherSubscriber = FakePusherSubscriber(),
|
||||
isPlayServiceAvailable: IsPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
|
||||
firebaseTokenRotator: FirebaseTokenRotator = FakeFirebaseTokenRotator(),
|
||||
rotateFirebaseSession: RotateFirebaseSession = FakeRotateFirebaseSession(),
|
||||
firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider()
|
||||
): FirebasePushProvider {
|
||||
return FirebasePushProvider(
|
||||
firebaseStore = firebaseStore,
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
isPlayServiceAvailable = isPlayServiceAvailable,
|
||||
firebaseTokenRotator = firebaseTokenRotator,
|
||||
rotateFirebaseSession = rotateFirebaseSession,
|
||||
firebaseGatewayProvider = firebaseGatewayProvider,
|
||||
)
|
||||
}
|
||||
|
||||
+5
-5
@@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class InMemoryFirebaseStore(
|
||||
private var token: String? = null
|
||||
private var installationId: String? = null
|
||||
) : FirebaseStore {
|
||||
override fun getFcmToken(): String? = token
|
||||
override fun getInstallationId(): String? = installationId
|
||||
|
||||
override fun fcmTokenFlow(): Flow<String?> = flowOf(token)
|
||||
override fun fcmInstallationIdFlow(): Flow<String?> = flowOf(installationId)
|
||||
|
||||
override fun storeFcmToken(token: String?) {
|
||||
this.token = token
|
||||
override fun storeInstallationId(installationId: String?) {
|
||||
this.installationId = installationId
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -171,22 +171,22 @@ class VectorFirebaseMessagingServiceTest : RobolectricTest() {
|
||||
fun `test new token is forwarded to the handler`() = runTest {
|
||||
val lambda = lambdaRecorder<String, Unit> { }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
firebaseNewTokenHandler = FakeFirebaseNewTokenHandler(handleResult = lambda)
|
||||
firebaseNewInstallationIdHandler = FakeFirebaseNewInstallationIdHandler(handleResult = lambda)
|
||||
)
|
||||
vectorFirebaseMessagingService.onNewToken("aToken")
|
||||
vectorFirebaseMessagingService.onRegistered("installationId")
|
||||
advanceUntilIdle()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value("aToken"))
|
||||
.with(value("installationId"))
|
||||
}
|
||||
|
||||
private fun TestScope.createVectorFirebaseMessagingService(
|
||||
firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(),
|
||||
firebaseNewInstallationIdHandler: FirebaseNewInstallationIdHandler = FakeFirebaseNewInstallationIdHandler(),
|
||||
pushHandler: PushHandler = FakePushHandler(),
|
||||
pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
|
||||
): VectorFirebaseMessagingService {
|
||||
return VectorFirebaseMessagingService().apply {
|
||||
this.firebaseNewTokenHandler = firebaseNewTokenHandler
|
||||
this.firebaseNewInstallationIdHandler = firebaseNewInstallationIdHandler
|
||||
this.pushParser = FirebasePushParser()
|
||||
this.pushHandler = pushHandler
|
||||
this.coroutineScope = this@createVectorFirebaseMessagingService
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ class FirebaseTokenTestTest {
|
||||
firebaseStore = firebaseStore,
|
||||
firebaseTroubleshooter = FakeFirebaseTroubleshooter(
|
||||
troubleShootResult = {
|
||||
firebaseStore.storeFcmToken(FAKE_TOKEN)
|
||||
firebaseStore.storeInstallationId(FAKE_TOKEN)
|
||||
Result.success(Unit)
|
||||
}
|
||||
),
|
||||
@@ -70,7 +70,7 @@ class FirebaseTokenTestTest {
|
||||
firebaseStore = firebaseStore,
|
||||
firebaseTroubleshooter = FakeFirebaseTroubleshooter(
|
||||
troubleShootResult = {
|
||||
firebaseStore.storeFcmToken(FAKE_TOKEN)
|
||||
firebaseStore.storeInstallationId(FAKE_TOKEN)
|
||||
Result.success(Unit)
|
||||
}
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user