forked from dsutanto/bChot-android
First Commit
This commit is contained in:
19
libraries/pushproviders/api/build.gradle.kts
Normal file
19
libraries/pushproviders/api/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.pushproviders.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
data class Config(
|
||||
val url: String,
|
||||
val pushKey: String,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
/**
|
||||
* Firebase does not have the concept of distributor. So for Firebase, there will be one distributor:
|
||||
* Distributor("Firebase", "Firebase").
|
||||
*
|
||||
* For UnifiedPush, for instance, the Distributor can be:
|
||||
* Distributor("io.heckel.ntfy", "ntfy").
|
||||
* But other values are possible.
|
||||
*/
|
||||
data class Distributor(
|
||||
val value: String,
|
||||
val name: String,
|
||||
) {
|
||||
val fullName = "$name ($value)"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
/**
|
||||
* Represent parsed data that the app has received from a Push content.
|
||||
*
|
||||
* @property eventId The Event Id.
|
||||
* @property roomId The Room Id.
|
||||
* @property unread Number of unread message.
|
||||
* @property clientSecret data used when the pusher was configured, to be able to determine the session.
|
||||
*/
|
||||
data class PushData(
|
||||
val eventId: EventId,
|
||||
val roomId: RoomId,
|
||||
val unread: Int?,
|
||||
val clientSecret: String,
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
interface PushHandler {
|
||||
suspend fun handle(
|
||||
pushData: PushData,
|
||||
providerInfo: String,
|
||||
)
|
||||
|
||||
suspend fun handleInvalid(
|
||||
providerInfo: String,
|
||||
data: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
/**
|
||||
* This is the main API for this module.
|
||||
*/
|
||||
interface PushProvider {
|
||||
/**
|
||||
* Allow to sort providers, from lower index to higher index.
|
||||
*/
|
||||
val index: Int
|
||||
|
||||
/**
|
||||
* User friendly name.
|
||||
*/
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* true if the Push provider supports multiple distributors.
|
||||
*/
|
||||
val supportMultipleDistributors: Boolean
|
||||
|
||||
/**
|
||||
* Return the list of available distributors.
|
||||
*/
|
||||
fun getDistributors(): List<Distributor>
|
||||
|
||||
/**
|
||||
* Register the pusher to the homeserver.
|
||||
*/
|
||||
suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit>
|
||||
|
||||
/**
|
||||
* Return the current distributor, or null if none.
|
||||
*/
|
||||
suspend fun getCurrentDistributorValue(sessionId: SessionId): String?
|
||||
|
||||
/**
|
||||
* Return the current distributor, or null if none.
|
||||
*/
|
||||
suspend fun getCurrentDistributor(sessionId: SessionId): Distributor?
|
||||
|
||||
/**
|
||||
* Unregister the pusher.
|
||||
*/
|
||||
suspend fun unregister(matrixClient: MatrixClient): Result<Unit>
|
||||
|
||||
/**
|
||||
* To invoke when the session is deleted.
|
||||
*/
|
||||
suspend fun onSessionDeleted(sessionId: SessionId)
|
||||
|
||||
suspend fun getPushConfig(sessionId: SessionId): Config?
|
||||
|
||||
fun canRotateToken(): Boolean
|
||||
|
||||
suspend fun rotateToken(): Result<Unit> {
|
||||
error("rotateToken() not implemented, you need to override this method in your implementation")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
|
||||
interface PusherSubscriber {
|
||||
/**
|
||||
* Register a pusher. Note that failure will be a [RegistrationFailure].
|
||||
*/
|
||||
suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit>
|
||||
|
||||
/**
|
||||
* Unregister a pusher.
|
||||
*/
|
||||
suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit>
|
||||
}
|
||||
|
||||
class RegistrationFailure(
|
||||
val clientException: ClientException,
|
||||
val isRegisteringAgain: Boolean
|
||||
) : Exception(clientException)
|
||||
7
libraries/pushproviders/firebase/README.md
Normal file
7
libraries/pushproviders/firebase/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Firebase
|
||||
|
||||
## Configuration
|
||||
|
||||
In order to make this module only know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module.
|
||||
|
||||
To be able to change the values set to `google_app_id` in the file `build.gradle.kts` of this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app/src/main`, build the project, and check the generated file `app/build/generated/res/google-services/<buildtype>/values/values.xml` to import the generated values into the `build.gradle.kts` files.
|
||||
82
libraries/pushproviders/firebase/build.gradle.kts
Normal file
82
libraries/pushproviders/firebase/build.gradle.kts
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import config.BuildTimeConfig
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.pushproviders.firebase"
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
consumerProguardFiles("consumer-proguard-rules.pro")
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "google_app_id",
|
||||
value = BuildTimeConfig.GOOGLE_APP_ID_RELEASE,
|
||||
)
|
||||
}
|
||||
getByName("debug") {
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "google_app_id",
|
||||
value = BuildTimeConfig.GOOGLE_APP_ID_DEBUG,
|
||||
)
|
||||
}
|
||||
register("nightly") {
|
||||
consumerProguardFiles("consumer-proguard-rules.pro")
|
||||
matchingFallbacks += listOf("release")
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "google_app_id",
|
||||
value = BuildTimeConfig.GOOGLE_APP_ID_NIGHTLY,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(projects.features.enterprise.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.troubleshoot.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
implementation(projects.libraries.pushstore.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
|
||||
api(platform(libs.google.firebase.bom))
|
||||
api("com.google.firebase:firebase-messaging") {
|
||||
exclude(group = "com.google.firebase", module = "firebase-core")
|
||||
exclude(group = "com.google.firebase", module = "firebase-analytics")
|
||||
exclude(group = "com.google.firebase", module = "firebase-measurement-connector")
|
||||
}
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.kotlinx.collections.immutable)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.troubleshoot.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Fix this error:
|
||||
# ERROR: Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in /Users/bmarty/workspaces/element-x-android/app/build/outputs/mapping/nightly/missing_rules.txt.
|
||||
# ERROR: R8: Missing class com.google.firebase.analytics.connector.AnalyticsConnector (referenced from: void com.google.firebase.messaging.MessagingAnalytics.logToScion(java.lang.String, android.os.Bundle) and 1 other context)
|
||||
-dontwarn com.google.firebase.analytics.connector.AnalyticsConnector
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2025 Element Creations Ltd.
|
||||
~ Copyright 2023 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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<!-- Firebase components -->
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
<service
|
||||
android:name="io.element.android.libraries.pushproviders.firebase.VectorFirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
object FirebaseConfig {
|
||||
/**
|
||||
* It is the push gateway for firebase.
|
||||
* Note: pusher_http_url should have path '/_matrix/push/v1/notify' -->
|
||||
*/
|
||||
const val PUSHER_HTTP_URL: String = "https://matrix.org/_matrix/push/v1/notify"
|
||||
|
||||
const val INDEX = 0
|
||||
const val NAME = "Firebase"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 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.features.enterprise.api.EnterpriseService
|
||||
|
||||
interface FirebaseGatewayProvider {
|
||||
fun getFirebaseGateway(): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseGatewayProvider(
|
||||
private val enterpriseService: EnterpriseService,
|
||||
) : FirebaseGatewayProvider {
|
||||
override fun getFirebaseGateway(): String {
|
||||
return enterpriseService.firebasePushGateway() ?: FirebaseConfig.PUSHER_HTTP_URL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
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)
|
||||
|
||||
/**
|
||||
* Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider.
|
||||
*/
|
||||
interface FirebaseNewTokenHandler {
|
||||
suspend fun handle(firebaseToken: String)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseNewTokenHandler(
|
||||
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)
|
||||
// Register the pusher for all the sessions
|
||||
sessionStore.getAllSessions().toUserList()
|
||||
.map { SessionId(it) }
|
||||
.forEach { sessionId ->
|
||||
val userDataStore = userPushStoreFactory.getOrCreate(sessionId)
|
||||
if (userDataStore.getPushProviderName() == FirebaseConfig.NAME) {
|
||||
matrixClientProvider
|
||||
.getOrRestore(sessionId)
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).e(it, "Failed to restore session $sessionId")
|
||||
}
|
||||
.flatMap { client ->
|
||||
pusherSubscriber
|
||||
.registerPusher(
|
||||
matrixClient = client,
|
||||
pushKey = firebaseToken,
|
||||
gateway = firebaseGatewayProvider.getFirebaseGateway(),
|
||||
)
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("This session is not using Firebase pusher")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
||||
@Inject
|
||||
class FirebasePushParser {
|
||||
fun parse(message: Map<String, String?>): PushData? {
|
||||
val pushDataFirebase = PushDataFirebase(
|
||||
eventId = message["event_id"],
|
||||
roomId = message["room_id"],
|
||||
unread = message["unread"]?.toIntOrNull(),
|
||||
clientSecret = message["cs"],
|
||||
)
|
||||
return pushDataFirebase.toPushData()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("FirebasePushProvider", LoggerTag.PushLoggerTag)
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class FirebasePushProvider(
|
||||
private val firebaseStore: FirebaseStore,
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
private val isPlayServiceAvailable: IsPlayServiceAvailable,
|
||||
private val firebaseTokenRotator: FirebaseTokenRotator,
|
||||
private val firebaseGatewayProvider: FirebaseGatewayProvider,
|
||||
) : PushProvider {
|
||||
override val index = FirebaseConfig.INDEX
|
||||
override val name = FirebaseConfig.NAME
|
||||
override val supportMultipleDistributors = false
|
||||
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
return listOfNotNull(
|
||||
firebaseDistributor.takeIf { isPlayServiceAvailable.isAvailable() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
|
||||
val pushKey = firebaseStore.getFcmToken() ?: return Result.failure<Unit>(
|
||||
IllegalStateException(
|
||||
"Unable to register pusher, Firebase token is not known."
|
||||
)
|
||||
).also {
|
||||
Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.")
|
||||
}
|
||||
return pusherSubscriber.registerPusher(
|
||||
matrixClient = matrixClient,
|
||||
pushKey = pushKey,
|
||||
gateway = firebaseGatewayProvider.getFirebaseGateway(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCurrentDistributorValue(sessionId: SessionId): String = firebaseDistributor.value
|
||||
|
||||
override suspend fun getCurrentDistributor(sessionId: SessionId) = firebaseDistributor
|
||||
|
||||
override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
|
||||
val pushKey = firebaseStore.getFcmToken()
|
||||
return if (pushKey == null) {
|
||||
Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.")
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
pusherSubscriber.unregisterPusher(matrixClient, pushKey, firebaseGatewayProvider.getFirebaseGateway())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nothing to clean up here.
|
||||
*/
|
||||
override suspend fun onSessionDeleted(sessionId: SessionId) = Unit
|
||||
|
||||
override suspend fun getPushConfig(sessionId: SessionId): Config? {
|
||||
return firebaseStore.getFcmToken()?.let { fcmToken ->
|
||||
Config(
|
||||
url = firebaseGatewayProvider.getFirebaseGateway(),
|
||||
pushKey = fcmToken
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun canRotateToken(): Boolean = true
|
||||
|
||||
override suspend fun rotateToken(): Result<Unit> {
|
||||
return firebaseTokenRotator.rotate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val firebaseDistributor = Distributor("Firebase", "Firebase")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
/**
|
||||
* This class store the Firebase token in SharedPrefs.
|
||||
*/
|
||||
interface FirebaseStore {
|
||||
fun getFcmToken(): String?
|
||||
fun fcmTokenFlow(): Flow<String?>
|
||||
fun storeFcmToken(token: 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 fcmTokenFlow(): Flow<String?> {
|
||||
val flow = MutableStateFlow(getFcmToken())
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, k ->
|
||||
if (k == PREFS_KEY_FCM_TOKEN) {
|
||||
try {
|
||||
flow.value = getFcmToken()
|
||||
} catch (e: Exception) {
|
||||
flow.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
return flow
|
||||
.onStart { sharedPreferences.registerOnSharedPreferenceChangeListener(listener) }
|
||||
.onCompletion { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}
|
||||
|
||||
override fun storeFcmToken(token: String?) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_KEY_FCM_TOKEN, token)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
|
||||
interface FirebaseTroubleshooter {
|
||||
suspend fun troubleshoot(): Result<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
* This class force retrieving and storage of the Firebase token.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFirebaseTroubleshooter(
|
||||
private val newTokenHandler: FirebaseNewTokenHandler,
|
||||
private val firebaseTokenGetter: FirebaseTokenGetter,
|
||||
) : FirebaseTroubleshooter {
|
||||
override suspend fun troubleshoot(): Result<Unit> {
|
||||
return runCatchingExceptions {
|
||||
val token = firebaseTokenGetter.get()
|
||||
newTokenHandler.handle(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 android.content.Context
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailabilityLight
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import timber.log.Timber
|
||||
|
||||
interface IsPlayServiceAvailable {
|
||||
fun isAvailable(): Boolean
|
||||
}
|
||||
|
||||
fun IsPlayServiceAvailable.checkAvailableOrThrow() {
|
||||
if (!isAvailable()) {
|
||||
throw Exception("No valid Google Play Services found. Cannot use FCM.").also(Timber::e)
|
||||
}
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultIsPlayServiceAvailable(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : IsPlayServiceAvailable {
|
||||
override fun isAvailable(): Boolean {
|
||||
val apiAvailability = GoogleApiAvailabilityLight.getInstance()
|
||||
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
|
||||
return if (resultCode == ConnectionResult.SUCCESS) {
|
||||
Timber.d("Google Play Services is available")
|
||||
true
|
||||
} else {
|
||||
Timber.w("Google Play Services is not available")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
||||
/**
|
||||
* In this case, the format is:
|
||||
* <pre>
|
||||
* {
|
||||
* "event_id":"$anEventId",
|
||||
* "room_id":"!aRoomId",
|
||||
* "unread":"1",
|
||||
* "prio":"high",
|
||||
* "cs":"<client_secret>"
|
||||
* }
|
||||
* </pre>
|
||||
* .
|
||||
*/
|
||||
data class PushDataFirebase(
|
||||
val eventId: String?,
|
||||
val roomId: String?,
|
||||
val unread: Int?,
|
||||
val clientSecret: String?
|
||||
)
|
||||
|
||||
fun PushDataFirebase.toPushData(): PushData? {
|
||||
val safeEventId = eventId?.let(::EventId) ?: return null
|
||||
val safeRoomId = roomId?.let(::RoomId) ?: return null
|
||||
val safeClientSecret = clientSecret ?: return null
|
||||
return PushData(
|
||||
eventId = safeEventId,
|
||||
roomId = safeRoomId,
|
||||
unread = unread,
|
||||
clientSecret = safeClientSecret,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("VectorFirebaseMessagingService", LoggerTag.PushLoggerTag)
|
||||
|
||||
class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
|
||||
@Inject lateinit var pushParser: FirebasePushParser
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
@AppCoroutineScope
|
||||
@Inject lateinit var coroutineScope: CoroutineScope
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
bindings<VectorFirebaseMessagingServiceBindings>().inject(this)
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
Timber.tag(loggerTag.value).w("New Firebase token")
|
||||
coroutineScope.launch {
|
||||
firebaseNewTokenHandler.handle(token)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}")
|
||||
coroutineScope.launch {
|
||||
val pushData = pushParser.parse(message.data)
|
||||
if (pushData == null) {
|
||||
Timber.tag(loggerTag.value).w("Invalid data received from Firebase")
|
||||
pushHandler.handleInvalid(
|
||||
providerInfo = FirebaseConfig.NAME,
|
||||
data = message.data.keys.joinToString("\n") {
|
||||
"$it: ${message.data[it]}"
|
||||
},
|
||||
)
|
||||
} else {
|
||||
pushHandler.handle(
|
||||
pushData = pushData,
|
||||
providerInfo = FirebaseConfig.NAME,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface VectorFirebaseMessagingServiceBindings {
|
||||
fun inject(service: VectorFirebaseMessagingService)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.troubleshoot
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
|
||||
import io.element.android.libraries.pushproviders.firebase.IsPlayServiceAvailable
|
||||
import io.element.android.libraries.pushproviders.firebase.R
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class FirebaseAvailabilityTest(
|
||||
private val isPlayServiceAvailable: IsPlayServiceAvailable,
|
||||
private val stringProvider: StringProvider,
|
||||
) : NotificationTroubleshootTest {
|
||||
override val order = 300
|
||||
private val delegate = NotificationTroubleshootTestDelegate(
|
||||
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_title),
|
||||
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_description),
|
||||
visibleWhenIdle = false,
|
||||
fakeDelay = NotificationTroubleshootTestDelegate.LONG_DELAY,
|
||||
)
|
||||
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
|
||||
|
||||
override fun isRelevant(data: TestFilterData): Boolean {
|
||||
return data.currentPushProviderName == FirebaseConfig.NAME
|
||||
}
|
||||
|
||||
override suspend fun run(coroutineScope: CoroutineScope) {
|
||||
delegate.start()
|
||||
val result = isPlayServiceAvailable.isAvailable()
|
||||
if (result) {
|
||||
delegate.updateState(
|
||||
description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_success),
|
||||
status = NotificationTroubleshootTestState.Status.Success
|
||||
)
|
||||
} else {
|
||||
delegate.updateState(
|
||||
description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_failure),
|
||||
status = NotificationTroubleshootTestState.Status.Failure()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() = delegate.reset()
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.troubleshoot
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
|
||||
import io.element.android.libraries.pushproviders.firebase.FirebaseStore
|
||||
import io.element.android.libraries.pushproviders.firebase.FirebaseTroubleshooter
|
||||
import io.element.android.libraries.pushproviders.firebase.R
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class FirebaseTokenTest(
|
||||
private val firebaseStore: FirebaseStore,
|
||||
private val firebaseTroubleshooter: FirebaseTroubleshooter,
|
||||
private val stringProvider: StringProvider,
|
||||
) : NotificationTroubleshootTest {
|
||||
override val order = 310
|
||||
private val delegate = NotificationTroubleshootTestDelegate(
|
||||
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_title),
|
||||
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_description),
|
||||
visibleWhenIdle = false,
|
||||
fakeDelay = NotificationTroubleshootTestDelegate.LONG_DELAY,
|
||||
)
|
||||
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
|
||||
|
||||
override fun isRelevant(data: TestFilterData): Boolean {
|
||||
return data.currentPushProviderName == FirebaseConfig.NAME
|
||||
}
|
||||
|
||||
private var currentJob: Job? = null
|
||||
override suspend fun run(coroutineScope: CoroutineScope) {
|
||||
currentJob?.cancel()
|
||||
delegate.start()
|
||||
currentJob = firebaseStore.fcmTokenFlow()
|
||||
.onEach { token ->
|
||||
if (token != null) {
|
||||
delegate.updateState(
|
||||
description = stringProvider.getString(
|
||||
R.string.troubleshoot_notifications_test_firebase_token_success,
|
||||
"*****${token.takeLast(8)}"
|
||||
),
|
||||
status = NotificationTroubleshootTestState.Status.Success
|
||||
)
|
||||
} else {
|
||||
delegate.updateState(
|
||||
description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure),
|
||||
status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun reset() = delegate.reset()
|
||||
|
||||
override suspend fun quickFix(
|
||||
coroutineScope: CoroutineScope,
|
||||
navigator: NotificationTroubleshootNavigator,
|
||||
) {
|
||||
delegate.start()
|
||||
firebaseTroubleshooter.troubleshoot()
|
||||
run(coroutineScope)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Пераканайцеся, што Firebase даступны."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase недаступны."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase даступны."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Праверыць Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Пераканайцеся, што маркер Firebase даступны."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Маркер Firebase невядомы."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Маркер Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Праверце маркер Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Уверете се, че Firebase е наличен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase не е наличен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase е наличен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Проверка на Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Ujistěte se, že je k dispozici Firebase."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase není k dispozici."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase je k dispozici."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Zkontrolovat Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Ujistěte se, že je k dispozici Firebase token."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase token není znám."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Zkontrolovat Firebase token"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Gwnewch yn siwr fod Firebase ar gael."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Nid yw Firebase ar gael."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Mae Firebase ar gael."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Gwiriwch Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Gwnewch yn siŵr fod tocyn Firebase ar gael."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Nid yw tocyn Firebase yn hysbys."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Tocyn Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Gwiriwch tocyn Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Sørg for, at Firebase er tilgængelig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase er ikke tilgængelig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase er tilgængelig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Tjek Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Sørg for, at Firebase-tokenet er tilgængeligt."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase-tokenet er ikke kendt."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase-token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Tjek Firebase-token"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Stelle sicher, dass Firebase verfügbar ist."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase ist nicht verfügbar."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase ist verfügbar."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Überprüfe Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Stelle sicher, dass der Firebase Token verfügbar ist."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase Token ist nicht bekannt."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase Token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Prüfe Firebase Token"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Βεβαιώσου ότι το Firebase είναι διαθέσιμο."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Το Firebase δεν είναι διαθέσιμο."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Το Firebase είναι διαθέσιμο."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Έλεγχος Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Βεβαιώσου ότι το διακριτικό του Firebase είναι διαθέσιμο."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Το διακριτικό Firebase δεν είναι γνωστό."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Διακριτικό Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Έλεγξε το διακριτικό του Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Asegurarse de que Firebase esté disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase no está disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase está disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Verificar Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Asegurarse de que el token de Firebase esté disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Se desconoce el token de Firebase."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token de Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Verificar token de Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Palun veendu, et Firebase oleks saadaval."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase pole saadaval."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase on saadaval."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Kontrolli Firebase\'i"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Palun veendu, et Firebase\'i pääsuluba oleks saadaval."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase\'i pääsuluba pole teada."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase\'i pääsuluba: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Kontrolli Firebase\'i pääsuluba"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Ziurtatu Firebase erabilgarri dagoela."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase ez dago erabilgarri."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase eskuragarri dago."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Egiaztatu Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Ziurtatu Firebaseren tokena erabilgarri dagoela."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Ez da Firebaseren tokena ezagutzen."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebaseren tokena: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Egiaztatu Firebaseren tokena"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Varmistaa, että Firebase on käytettävissä."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase ei ole saatavilla."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase on saatavilla."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebasen tarkistus"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Varmistaa, että Firebase token on käytettävissä."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase token ei ole tiedossa."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Firebase tokenin tarkistus"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Vérification que Firebase est disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase n’est pas disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase est disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Vérification de Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Vérifier que le jeton Firebase est disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Le jeton Firebase n’est pas connu."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Jeton Firebase :%1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Vérifier le jeton Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Győződjön meg arról, hogy a Firebase elérhető-e."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"A Firebase nem érhető el."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"A Firebase elérhető."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Ellenőrizze a Firebase-t"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Győződjön meg arról, hogy a Firebase-token elérhető."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"A Firebase-token nem ismert."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase-token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Ellenőrizze a Firebase-tokent"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Pastikan bahwa Firebase tersedia."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase tidak tersedia."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase tersedia."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Periksa Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Pastikan token Firebase tersedia."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Token Firebase tidak diketahui."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Periksa token Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Assicurati che Firebase sia disponibile."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase non è disponibile."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase è disponibile."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Controlla Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Assicurati che il token di Firebase sia disponibile."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Il token di Firebase non è noto."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Verifica il token di Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"დარწმუნდით რომ Firebase ხელმისაწვდომია."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase არაა ხელმისაწვდომი."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase ხელმისაწვდომია."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebase-ის შემოწმება"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"დარწმუნდით რომ Firebase ტოკენი ხელმისაწვდომია."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase ტოკენი უცნობია."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase ტოკენი: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"შეამოწმეთ Firebase ტოკენი"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Firebase를 사용할 수 있는지 확인하세요."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase는 사용할 수 없습니다."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase를 사용할 수 있습니다."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebase 확인"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Firebase 토큰을 사용할 수 있는지 확인하세요."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase 토큰이 인식되지 않았습니다."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase 토큰: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Firebase 토큰 확인"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Sørg for at Firebase er tilgjengelig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase er ikke tilgjengelig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase er tilgjengelig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Sjekk Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Sørg for at Firebase-token er tilgjengelig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase-token er ikke kjent."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase-token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Sjekk Firebase-token"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Zorg ervoor dat Firebase beschikbaar is."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase is niet beschikbaar."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase is beschikbaar."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebase controleren"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Zorg ervoor dat de Firebase-token beschikbaar is."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase-token is niet bekend."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase-token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Firebase-token controleren"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Upewnij się, że Firebase jest dostępny."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Baza Firebase jest niedostępna."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Baza Firebase jest dostępna."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Sprawdź Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Upewnij się, że token Firebase jest dostępny."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Token Firebase nie jest znany."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Sprawdź token Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Certifique-se de que o Firebase esteja disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"O Firebase não está disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"O Firebase está disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Verificar o Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Certifique-se de que o token do Firebase esteja disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"O token do Firebase não é conhecido."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token do Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Verificar o token do Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Certifica que a Firebase está disponível"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase indisponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Verificar a Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Certifica que o \"token\" da Firebase está disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"\"Token\" da Firebase desconhecido."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"\"Token\" da Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Verificar \"token\" da Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Asigurați-vă că Firebase este disponibil."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase nu este disponibil."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase este disponibil."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Verificați Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Asigurați-vă că tokenul Firebase este disponibil."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Tokenul Firebase nu este cunoscut."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Verificați token-ul Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Убедитесь, что Firebase доступен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase недоступен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase доступен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Проверить Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Убедитесь, что токен Firebase доступен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Токен Firebase неизвестен."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Токен Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Проверить токен Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Uistite sa, že Firebase je k dispozícii."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase nie je k dispozícii."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase je k dispozícii."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Skontrolovať Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Uistite sa, že je k dispozícii token Firebase."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Token Firebase nie je známy."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Skontrolovať token Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Se till att Firebase är tillgängligt."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase är inte tillgängligt."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase är tillgänglig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Kontrollera Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Se till att Firebase-token är tillgänglig."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase-token är inte känd."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase-token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Kontrollera Firebase-token"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Firebase\'in kullanılabilir olduğundan emin olun."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase kullanılamıyor."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase kullanılabilir."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebase\'i kontrol et"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Firebase belirtecinin mevcut olduğundan emin olun."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase belirteci bilinmiyor."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase belirteci: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Firebase jetonunu kontrol edin"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Переконується, що Firebase доступний."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase недоступний."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase доступний."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Перевірка Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Переконується, що токен Firebase доступний."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Токен Firebase невідомий."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Токен Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Перевірка токена Firebase"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"یقینی بنائیں کہ Firebase دستیاب ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase دستیاب نہیں ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase دستیاب ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebase کی پڑتال کریں"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"یقینی بنائیں کہ Firebase رمزِ ممیز دستیاب ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase رمزِ ممیز معلوم نہیں ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase رمزِ ممیز:%1$s۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Firebase رمزِ ممیز کی پڑتال کریں"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Firebase mavjudligiga ishonch hosil qiling."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase mavjud emas."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase mavjud."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebase-ni tekshiring"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Firebase tokeni mavjudligiga ishonch hosil qiling."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase mavjudligiga ishonch hosil qiling."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase tokeni: %1$s ."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Firebase tokenini tekshiring"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"確保 Firebase 可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase 不可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase 可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"檢查 Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"確保 Firebase 權杖可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase 權杖未知。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase 權杖:%1$s。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"檢查 Firebase 權杖"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"确保 Firebase 可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase 不可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase 可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"检查 Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"确保 Firebase 令牌可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase 令牌未知。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase 令牌:%1$s 。"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"检查 Firebase 令牌"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="default_web_client_id" translatable="false">912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com</string>
|
||||
<string name="firebase_database_url" translatable="false">https://vector-alpha.firebaseio.com</string>
|
||||
<string name="gcm_defaultSenderId" translatable="false">912726360885</string>
|
||||
<string name="google_api_key" translatable="false">AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c</string>
|
||||
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c</string>
|
||||
<string name="google_storage_bucket" translatable="false">vector-alpha.appspot.com</string>
|
||||
<string name="project_id" translatable="false" tools:ignore="UnusedResources">vector-alpha</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Ensure that Firebase is available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase is not available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase is available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Check Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Ensure that Firebase token is available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase token is not known."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Check Firebase token"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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 com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.push.test.FakePusherSubscriber
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultFirebaseNewTokenHandlerTest {
|
||||
@Test
|
||||
fun `when a new token is received it is stored in the firebase store`() = runTest {
|
||||
val firebaseStore = InMemoryFirebaseStore()
|
||||
assertThat(firebaseStore.getFcmToken()).isNull()
|
||||
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
|
||||
firebaseStore = firebaseStore,
|
||||
)
|
||||
firebaseNewTokenHandler.handle("aToken")
|
||||
assertThat(firebaseStore.getFcmToken()).isEqualTo("aToken")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a new token is received, the handler registers the pusher again to all sessions using Firebase`() = runTest {
|
||||
val aMatrixClient1 = FakeMatrixClient(A_USER_ID)
|
||||
val aMatrixClient2 = FakeMatrixClient(A_USER_ID_2)
|
||||
val aMatrixClient3 = FakeMatrixClient(A_USER_ID_3)
|
||||
val registerPusherResult = lambdaRecorder<MatrixClient, String, String, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult)
|
||||
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(A_USER_ID.value),
|
||||
aSessionData(A_USER_ID_2.value),
|
||||
aSessionData(A_USER_ID_3.value),
|
||||
)
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider { sessionId ->
|
||||
when (sessionId) {
|
||||
A_USER_ID -> Result.success(aMatrixClient1)
|
||||
A_USER_ID_2 -> Result.success(aMatrixClient2)
|
||||
A_USER_ID_3 -> Result.success(aMatrixClient3)
|
||||
else -> Result.failure(IllegalStateException())
|
||||
}
|
||||
},
|
||||
userPushStoreFactory = FakeUserPushStoreFactory(
|
||||
userPushStore = { sessionId ->
|
||||
when (sessionId) {
|
||||
A_USER_ID -> FakeUserPushStore(pushProviderName = FirebaseConfig.NAME)
|
||||
A_USER_ID_2 -> FakeUserPushStore(pushProviderName = "Other")
|
||||
A_USER_ID_3 -> FakeUserPushStore(pushProviderName = FirebaseConfig.NAME)
|
||||
else -> error("Unexpected sessionId: $sessionId")
|
||||
}
|
||||
}
|
||||
),
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
)
|
||||
firebaseNewTokenHandler.handle("aToken")
|
||||
registerPusherResult.assertions()
|
||||
.isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(aMatrixClient1), value("aToken"), value(A_FIREBASE_GATEWAY)),
|
||||
listOf(value(aMatrixClient3), value("aToken"), value(A_FIREBASE_GATEWAY)),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a new token is received, if the session cannot be restore, nothing happen`() = runTest {
|
||||
val registerPusherResult = lambdaRecorder<MatrixClient, String, String, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult)
|
||||
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData(A_USER_ID.value))
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider {
|
||||
Result.failure(IllegalStateException())
|
||||
},
|
||||
userPushStoreFactory = FakeUserPushStoreFactory(
|
||||
userPushStore = { _ ->
|
||||
FakeUserPushStore(pushProviderName = FirebaseConfig.NAME)
|
||||
}
|
||||
),
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
)
|
||||
firebaseNewTokenHandler.handle("aToken")
|
||||
registerPusherResult.assertions()
|
||||
.isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a new token is received, error when registering the pusher is ignored`() = runTest {
|
||||
val aMatrixClient1 = FakeMatrixClient(A_USER_ID)
|
||||
val registerPusherResult = lambdaRecorder<MatrixClient, String, String, Result<Unit>> { _, _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult)
|
||||
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData(A_USER_ID.value))
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider {
|
||||
Result.success(aMatrixClient1)
|
||||
},
|
||||
userPushStoreFactory = FakeUserPushStoreFactory(
|
||||
userPushStore = { _ ->
|
||||
FakeUserPushStore(pushProviderName = FirebaseConfig.NAME)
|
||||
}
|
||||
),
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
)
|
||||
firebaseNewTokenHandler.handle("aToken")
|
||||
registerPusherResult.assertions()
|
||||
registerPusherResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(aMatrixClient1), value("aToken"), value(A_FIREBASE_GATEWAY))
|
||||
}
|
||||
|
||||
private fun createDefaultFirebaseNewTokenHandler(
|
||||
pusherSubscriber: PusherSubscriber = FakePusherSubscriber(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(),
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
firebaseStore: FirebaseStore = InMemoryFirebaseStore(),
|
||||
firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider(),
|
||||
): FirebaseNewTokenHandler {
|
||||
return DefaultFirebaseNewTokenHandler(
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
sessionStore = sessionStore,
|
||||
userPushStoreFactory = userPushStoreFactory,
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
firebaseStore = firebaseStore,
|
||||
firebaseGatewayProvider = firebaseGatewayProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 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
|
||||
|
||||
const val A_FIREBASE_GATEWAY = "aGateway"
|
||||
|
||||
class FakeFirebaseGatewayProvider(
|
||||
private val firebaseGatewayResult: () -> String = { A_FIREBASE_GATEWAY }
|
||||
) : FirebaseGatewayProvider {
|
||||
override fun getFirebaseGateway() = firebaseGatewayResult()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeFirebaseNewTokenHandler(
|
||||
private val handleResult: (String) -> Unit = { lambdaError() }
|
||||
) : FirebaseNewTokenHandler {
|
||||
override suspend fun handle(firebaseToken: String) {
|
||||
handleResult(firebaseToken)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeFirebaseTokenRotator(
|
||||
private val rotateWithResult: () -> Result<Unit> = { lambdaError() }
|
||||
) : FirebaseTokenRotator {
|
||||
override suspend fun rotate(): Result<Unit> {
|
||||
return rotateWithResult()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeFirebaseTroubleshooter(
|
||||
private val troubleShootResult: () -> Result<Unit> = { Result.success(Unit) }
|
||||
) : FirebaseTroubleshooter {
|
||||
override suspend fun troubleshoot(): Result<Unit> = simulateLongTask {
|
||||
troubleShootResult()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
class FakeIsPlayServiceAvailable(
|
||||
private val isAvailable: Boolean,
|
||||
) : IsPlayServiceAvailable {
|
||||
override fun isAvailable() = isAvailable
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import io.element.android.tests.testutils.assertThrowsInDebug
|
||||
import org.junit.Test
|
||||
|
||||
class FirebasePushParserTest {
|
||||
private val validData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 1,
|
||||
clientSecret = "a-secret"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test edge cases Firebase`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
// Empty Json
|
||||
assertThat(pushParser.parse(emptyMap())).isNull()
|
||||
// Bad Json
|
||||
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("unread", "str"))).isEqualTo(validData.copy(unread = null))
|
||||
// Extra data
|
||||
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("extra", "5"))).isEqualTo(validData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test Firebase format`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
assertThat(pushParser.parse(FIREBASE_PUSH_DATA)).isEqualTo(validData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test empty roomId`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull()
|
||||
assertThrowsInDebug { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test invalid roomId`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
assertThrowsInDebug { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test empty eventId`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull()
|
||||
assertThrowsInDebug { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test empty client secret`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("cs", null))).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test invalid eventId`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
assertThrowsInDebug { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FIREBASE_PUSH_DATA = mapOf(
|
||||
"event_id" to AN_EVENT_ID.value,
|
||||
"room_id" to A_ROOM_ID.value,
|
||||
"unread" to "1",
|
||||
"prio" to "high",
|
||||
"cs" to "a-secret",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<String, String?>.mutate(key: String, value: String?): Map<String, String?> {
|
||||
return toMutableMap().apply { put(key, value) }
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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 com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.push.test.FakePusherSubscriber
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class FirebasePushProviderTest {
|
||||
@Test
|
||||
fun `test index and name`() {
|
||||
val firebasePushProvider = createFirebasePushProvider()
|
||||
assertThat(firebasePushProvider.name).isEqualTo(FirebaseConfig.NAME)
|
||||
assertThat(firebasePushProvider.index).isEqualTo(FirebaseConfig.INDEX)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDistributors return the unique distributor if available`() {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = true)
|
||||
)
|
||||
val result = firebasePushProvider.getDistributors()
|
||||
assertThat(result).containsExactly(Distributor("Firebase", "Firebase"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDistributors return empty list if service is not available`() {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = false)
|
||||
)
|
||||
val result = firebasePushProvider.getDistributors()
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentDistributor always returns the unique distributor`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider()
|
||||
val result = firebasePushProvider.getCurrentDistributor(A_SESSION_ID)
|
||||
assertThat(result).isEqualTo(Distributor("Firebase", "Firebase"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register ok`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val registerPusherResultLambda = lambdaRecorder<MatrixClient, String, String, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
registerPusherResult = registerPusherResultLambda
|
||||
)
|
||||
)
|
||||
val result = firebasePushProvider.registerWith(matrixClient, Distributor("value", "Name"))
|
||||
assertThat(result).isEqualTo(Result.success(Unit))
|
||||
registerPusherResultLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(matrixClient), value("aToken"), value(A_FIREBASE_GATEWAY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register ko no token`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = null
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
registerPusherResult = { _, _, _ -> Result.success(Unit) }
|
||||
)
|
||||
)
|
||||
val result = firebasePushProvider.registerWith(FakeMatrixClient(), Distributor("value", "Name"))
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register ko error`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
registerPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
)
|
||||
)
|
||||
val result = firebasePushProvider.registerWith(FakeMatrixClient(), Distributor("value", "Name"))
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unregister ok`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val unregisterPusherResultLambda = lambdaRecorder<MatrixClient, String, String, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
unregisterPusherResult = unregisterPusherResultLambda
|
||||
)
|
||||
)
|
||||
val result = firebasePushProvider.unregister(matrixClient)
|
||||
assertThat(result).isEqualTo(Result.success(Unit))
|
||||
unregisterPusherResultLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(matrixClient), value("aToken"), value(A_FIREBASE_GATEWAY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unregister no token - in this case, the error is ignored`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = null
|
||||
),
|
||||
)
|
||||
val result = firebasePushProvider.unregister(FakeMatrixClient())
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unregister ko error`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
),
|
||||
pusherSubscriber = FakePusherSubscriber(
|
||||
unregisterPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
)
|
||||
)
|
||||
val result = firebasePushProvider.unregister(FakeMatrixClient())
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentUserPushConfig no push ket`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = null
|
||||
)
|
||||
)
|
||||
val result = firebasePushProvider.getPushConfig(A_SESSION_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentUserPushConfig ok`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseStore = InMemoryFirebaseStore(
|
||||
token = "aToken"
|
||||
),
|
||||
)
|
||||
val result = firebasePushProvider.getPushConfig(A_SESSION_ID)
|
||||
assertThat(result).isEqualTo(Config(A_FIREBASE_GATEWAY, "aToken"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotateToken invokes the FirebaseTokenRotator`() = runTest {
|
||||
val lambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
firebaseTokenRotator = FakeFirebaseTokenRotator(lambda),
|
||||
)
|
||||
firebasePushProvider.rotateToken()
|
||||
lambda.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canRotateToken should return true`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider()
|
||||
assertThat(firebasePushProvider.canRotateToken()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onSessionDeleted should be noop`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider()
|
||||
firebasePushProvider.onSessionDeleted(A_SESSION_ID)
|
||||
}
|
||||
|
||||
private fun createFirebasePushProvider(
|
||||
firebaseStore: FirebaseStore = InMemoryFirebaseStore(),
|
||||
pusherSubscriber: PusherSubscriber = FakePusherSubscriber(),
|
||||
isPlayServiceAvailable: IsPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
|
||||
firebaseTokenRotator: FirebaseTokenRotator = FakeFirebaseTokenRotator(),
|
||||
firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider()
|
||||
): FirebasePushProvider {
|
||||
return FirebasePushProvider(
|
||||
firebaseStore = firebaseStore,
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
isPlayServiceAvailable = isPlayServiceAvailable,
|
||||
firebaseTokenRotator = firebaseTokenRotator,
|
||||
firebaseGatewayProvider = firebaseGatewayProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class InMemoryFirebaseStore(
|
||||
private var token: String? = null
|
||||
) : FirebaseStore {
|
||||
override fun getFcmToken(): String? = token
|
||||
|
||||
override fun fcmTokenFlow(): Flow<String?> = flowOf(token)
|
||||
|
||||
override fun storeFcmToken(token: String?) {
|
||||
this.token = token
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SECRET
|
||||
import io.element.android.libraries.push.test.test.FakePushHandler
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class VectorFirebaseMessagingServiceTest {
|
||||
@Test
|
||||
fun `test receiving invalid data`() = runTest {
|
||||
val lambda = lambdaRecorder<String, String, Unit> { _, _ -> }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
pushHandler = FakePushHandler(handleInvalidResult = lambda)
|
||||
)
|
||||
vectorFirebaseMessagingService.onMessageReceived(
|
||||
message = RemoteMessage(
|
||||
Bundle().apply {
|
||||
putString("a", "A")
|
||||
putString("b", "B")
|
||||
}
|
||||
)
|
||||
)
|
||||
runCurrent()
|
||||
lambda.assertions().isCalledOnce()
|
||||
.with(
|
||||
value(FirebaseConfig.NAME),
|
||||
value("a: A\nb: B"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test receiving valid data`() = runTest {
|
||||
val lambda = lambdaRecorder<PushData, String, Unit> { _, _ -> }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
pushHandler = FakePushHandler(handleResult = lambda)
|
||||
)
|
||||
vectorFirebaseMessagingService.onMessageReceived(
|
||||
message = RemoteMessage(
|
||||
Bundle().apply {
|
||||
putString("event_id", AN_EVENT_ID.value)
|
||||
putString("room_id", A_ROOM_ID.value)
|
||||
putString("cs", A_SECRET)
|
||||
},
|
||||
)
|
||||
)
|
||||
advanceUntilIdle()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET)),
|
||||
value(FirebaseConfig.NAME)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test new token is forwarded to the handler`() = runTest {
|
||||
val lambda = lambdaRecorder<String, Unit> { }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
firebaseNewTokenHandler = FakeFirebaseNewTokenHandler(handleResult = lambda)
|
||||
)
|
||||
vectorFirebaseMessagingService.onNewToken("aToken")
|
||||
advanceUntilIdle()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value("aToken"))
|
||||
}
|
||||
|
||||
private fun TestScope.createVectorFirebaseMessagingService(
|
||||
firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(),
|
||||
pushHandler: PushHandler = FakePushHandler(),
|
||||
): VectorFirebaseMessagingService {
|
||||
return VectorFirebaseMessagingService().apply {
|
||||
this.firebaseNewTokenHandler = firebaseNewTokenHandler
|
||||
this.pushParser = FirebasePushParser()
|
||||
this.pushHandler = pushHandler
|
||||
this.coroutineScope = this@createVectorFirebaseMessagingService
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.troubleshoot
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.pushproviders.firebase.FakeIsPlayServiceAvailable
|
||||
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
|
||||
import io.element.android.libraries.troubleshoot.test.runAndTestState
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class FirebaseAvailabilityTestTest {
|
||||
@Test
|
||||
fun `test FirebaseAvailabilityTest success`() = runTest {
|
||||
val sut = FirebaseAvailabilityTest(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(true),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test FirebaseAvailabilityTest failure`() = runTest {
|
||||
val sut = FirebaseAvailabilityTest(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
|
||||
sut.reset()
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test FirebaseAvailabilityTest isRelevant`() {
|
||||
val sut = FirebaseAvailabilityTest(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "unknown"))).isFalse()
|
||||
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = FirebaseConfig.NAME))).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.troubleshoot
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter
|
||||
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
|
||||
import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
|
||||
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
|
||||
import io.element.android.libraries.troubleshoot.test.runAndTestState
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class FirebaseTokenTestTest {
|
||||
@Test
|
||||
fun `test FirebaseTokenTest success`() = runTest {
|
||||
val sut = FirebaseTokenTest(
|
||||
firebaseStore = InMemoryFirebaseStore(FAKE_TOKEN),
|
||||
firebaseTroubleshooter = FakeFirebaseTroubleshooter(),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
|
||||
assertThat(lastItem.description).contains(FAKE_TOKEN.takeLast(8))
|
||||
assertThat(lastItem.description).doesNotContain(FAKE_TOKEN)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test FirebaseTokenTest error`() = runTest {
|
||||
val firebaseStore = InMemoryFirebaseStore(null)
|
||||
val sut = FirebaseTokenTest(
|
||||
firebaseStore = firebaseStore,
|
||||
firebaseTroubleshooter = FakeFirebaseTroubleshooter(
|
||||
troubleShootResult = {
|
||||
firebaseStore.storeFcmToken(FAKE_TOKEN)
|
||||
Result.success(Unit)
|
||||
}
|
||||
),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
|
||||
// Quick fix
|
||||
sut.quickFix(this, FakeNotificationTroubleshootNavigator())
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test FirebaseTokenTest error and reset`() = runTest {
|
||||
val firebaseStore = InMemoryFirebaseStore(null)
|
||||
val sut = FirebaseTokenTest(
|
||||
firebaseStore = firebaseStore,
|
||||
firebaseTroubleshooter = FakeFirebaseTroubleshooter(
|
||||
troubleShootResult = {
|
||||
firebaseStore.storeFcmToken(FAKE_TOKEN)
|
||||
Result.success(Unit)
|
||||
}
|
||||
),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
|
||||
sut.reset()
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test FirebaseTokenTest isRelevant`() {
|
||||
val sut = FirebaseTokenTest(
|
||||
firebaseStore = InMemoryFirebaseStore(null),
|
||||
firebaseTroubleshooter = FakeFirebaseTroubleshooter(),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "unknown"))).isFalse()
|
||||
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = FirebaseConfig.NAME))).isTrue()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FAKE_TOKEN = "abcdefghijk"
|
||||
}
|
||||
}
|
||||
20
libraries/pushproviders/test/build.gradle.kts
Normal file
20
libraries/pushproviders/test/build.gradle.kts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.pushproviders.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakePushProvider(
|
||||
override val index: Int = 0,
|
||||
override val name: String = "aFakePushProvider",
|
||||
override val supportMultipleDistributors: Boolean = false,
|
||||
private val distributors: List<Distributor> = listOf(Distributor("aDistributorValue", "aDistributorName")),
|
||||
private val currentDistributorValue: () -> String? = { lambdaError() },
|
||||
private val currentDistributor: () -> Distributor? = { distributors.firstOrNull() },
|
||||
private val config: Config? = null,
|
||||
private val registerWithResult: (MatrixClient, Distributor) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val unregisterWithResult: (MatrixClient) -> Result<Unit> = { lambdaError() },
|
||||
private val onSessionDeletedLambda: (SessionId) -> Unit = { lambdaError() },
|
||||
private val canRotateTokenResult: () -> Boolean = { lambdaError() },
|
||||
private val rotateTokenLambda: () -> Result<Unit> = { lambdaError() },
|
||||
) : PushProvider {
|
||||
override fun getDistributors(): List<Distributor> = distributors
|
||||
|
||||
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
|
||||
return registerWithResult(matrixClient, distributor)
|
||||
}
|
||||
|
||||
override suspend fun getCurrentDistributorValue(sessionId: SessionId): String? {
|
||||
return currentDistributorValue()
|
||||
}
|
||||
|
||||
override suspend fun getCurrentDistributor(sessionId: SessionId): Distributor? {
|
||||
return currentDistributor()
|
||||
}
|
||||
|
||||
override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
|
||||
return unregisterWithResult(matrixClient)
|
||||
}
|
||||
|
||||
override suspend fun onSessionDeleted(sessionId: SessionId) {
|
||||
onSessionDeletedLambda(sessionId)
|
||||
}
|
||||
|
||||
override suspend fun getPushConfig(sessionId: SessionId): Config? {
|
||||
return config
|
||||
}
|
||||
|
||||
override fun canRotateToken(): Boolean {
|
||||
return canRotateTokenResult()
|
||||
}
|
||||
|
||||
override suspend fun rotateToken(): Result<Unit> {
|
||||
return rotateTokenLambda()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.test
|
||||
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
|
||||
fun aSessionPushConfig(
|
||||
url: String = "aUrl",
|
||||
pushKey: String = "aPushKey",
|
||||
) = Config(
|
||||
url = url,
|
||||
pushKey = pushKey,
|
||||
)
|
||||
57
libraries/pushproviders/unifiedpush/build.gradle.kts
Normal file
57
libraries/pushproviders/unifiedpush/build.gradle.kts
Normal file
@@ -0,0 +1,57 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023, 2024 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.pushproviders.unifiedpush"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.enterprise.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.troubleshoot.api)
|
||||
|
||||
implementation(projects.libraries.pushstore.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
implementation(projects.libraries.network)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation(libs.network.okhttp.okhttp)
|
||||
implementation(platform(libs.network.retrofit.bom))
|
||||
implementation(libs.network.retrofit)
|
||||
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
// UnifiedPush library
|
||||
api(libs.unifiedpush)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.kotlinx.collections.immutable)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.libraries.troubleshoot.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2025 Element Creations Ltd.
|
||||
~ Copyright 2023 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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application>
|
||||
<receiver
|
||||
android:name=".VectorUnifiedPushMessagingReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
|
||||
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
|
||||
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".KeepInternalDistributor"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<!--
|
||||
This action is checked to track installed and uninstalled distributors.
|
||||
We declare it to keep the background sync as an internal
|
||||
unifiedpush distributor.
|
||||
-->
|
||||
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
|
||||
interface DefaultPushGatewayHttpUrlProvider {
|
||||
fun provide(): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDefaultPushGatewayHttpUrlProvider(
|
||||
private val enterpriseService: EnterpriseService,
|
||||
) : DefaultPushGatewayHttpUrlProvider {
|
||||
override fun provide(): String {
|
||||
return enterpriseService.unifiedPushDefaultPushGateway() ?: UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
|
||||
interface GuardServiceStarter {
|
||||
fun start() {}
|
||||
fun stop() {}
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class NoopGuardServiceStarter : GuardServiceStarter
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* UnifiedPush lib tracks an action to check installed and uninstalled distributors.
|
||||
* We declare it to keep the background sync as an internal unifiedpush distributor.
|
||||
* This class is used to declare this action.
|
||||
*/
|
||||
class KeepInternalDistributor : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* In this case, the format is:
|
||||
* <pre>
|
||||
* {
|
||||
* "notification":{
|
||||
* "event_id":"$anEventId",
|
||||
* "room_id":"!aRoomId",
|
||||
* "counts":{
|
||||
* "unread":1
|
||||
* },
|
||||
* "prio":"high"
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* .
|
||||
*/
|
||||
@Serializable
|
||||
data class PushDataUnifiedPush(
|
||||
val notification: PushDataUnifiedPushNotification? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushDataUnifiedPushNotification(
|
||||
@SerialName("event_id") val eventId: String? = null,
|
||||
@SerialName("room_id") val roomId: String? = null,
|
||||
@SerialName("counts") val counts: PushDataUnifiedPushCounts? = null,
|
||||
@SerialName("prio") val prio: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushDataUnifiedPushCounts(
|
||||
@SerialName("unread") val unread: Int? = null
|
||||
)
|
||||
|
||||
fun PushDataUnifiedPush.toPushData(clientSecret: String): PushData? {
|
||||
val safeEventId = notification?.eventId?.let(::EventId) ?: return null
|
||||
val safeRoomId = notification.roomId?.let(::RoomId) ?: return null
|
||||
return PushData(
|
||||
eventId = safeEventId,
|
||||
roomId = safeRoomId,
|
||||
unread = notification.counts?.unread,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
interface RegisterUnifiedPushUseCase {
|
||||
suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRegisterUnifiedPushUseCase(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val endpointRegistrationHandler: EndpointRegistrationHandler,
|
||||
) : RegisterUnifiedPushUseCase {
|
||||
override suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit> {
|
||||
UnifiedPush.saveDistributor(context, distributor.value)
|
||||
// This will trigger the callback
|
||||
// VectorUnifiedPushMessagingReceiver.onNewEndpoint
|
||||
UnifiedPush.register(context = context, instance = clientSecret)
|
||||
// Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed
|
||||
@Suppress("RunCatchingNotAllowed")
|
||||
return runCatching {
|
||||
withTimeout(30.seconds) {
|
||||
val result = endpointRegistrationHandler.state
|
||||
.filter { it.clientSecret == clientSecret }
|
||||
.first()
|
||||
.result
|
||||
result.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.network.RetrofitFactory
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi
|
||||
|
||||
interface UnifiedPushApiFactory {
|
||||
fun create(baseUrl: String): UnifiedPushApi
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushApiFactory(
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
) : UnifiedPushApiFactory {
|
||||
override fun create(baseUrl: String): UnifiedPushApi {
|
||||
return retrofitFactory.create(baseUrl)
|
||||
.create(UnifiedPushApi::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
object UnifiedPushConfig {
|
||||
/**
|
||||
* It is the push gateway for UnifiedPush.
|
||||
* Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify'
|
||||
*/
|
||||
const val DEFAULT_PUSH_GATEWAY_HTTP_URL: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
|
||||
|
||||
const val UNIFIED_PUSH_DISTRIBUTORS_URL = "https://unifiedpush.org/users/distributors/"
|
||||
|
||||
const val INDEX = 1
|
||||
const val NAME = "UnifiedPush"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.system.getApplicationLabel
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
|
||||
interface UnifiedPushDistributorProvider {
|
||||
fun getDistributors(): List<Distributor>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushDistributorProvider(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : UnifiedPushDistributorProvider {
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
val distributors = UnifiedPush.getDistributors(context)
|
||||
return distributors.mapNotNull {
|
||||
if (it == context.packageName) {
|
||||
// Exclude self
|
||||
null
|
||||
} else {
|
||||
Distributor(it, context.getApplicationLabel(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
sealed interface UnifiedPushGatewayResolverResult {
|
||||
data class Success(val gateway: String) : UnifiedPushGatewayResolverResult
|
||||
data class Error(val gateway: String) : UnifiedPushGatewayResolverResult
|
||||
data object NoMatrixGateway : UnifiedPushGatewayResolverResult
|
||||
data object ErrorInvalidUrl : UnifiedPushGatewayResolverResult
|
||||
}
|
||||
|
||||
interface UnifiedPushGatewayResolver {
|
||||
suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult
|
||||
}
|
||||
|
||||
private val loggerTag = LoggerTag("DefaultUnifiedPushGatewayResolver")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushGatewayResolver(
|
||||
private val unifiedPushApiFactory: UnifiedPushApiFactory,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : UnifiedPushGatewayResolver {
|
||||
override suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult {
|
||||
val url = tryOrNull(
|
||||
onException = { Timber.tag(loggerTag.value).d(it, "Cannot parse endpoint as an URL") }
|
||||
) {
|
||||
URL(endpoint)
|
||||
}
|
||||
return if (url == null) {
|
||||
Timber.tag(loggerTag.value).d("ErrorInvalidUrl")
|
||||
UnifiedPushGatewayResolverResult.ErrorInvalidUrl
|
||||
} else {
|
||||
val port = if (url.port != -1) ":${url.port}" else ""
|
||||
val customBase = "${url.protocol}://${url.host}$port"
|
||||
val customUrl = "$customBase/_matrix/push/v1/notify"
|
||||
Timber.tag(loggerTag.value).i("Testing $customUrl")
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
val api = unifiedPushApiFactory.create(customBase)
|
||||
try {
|
||||
val discoveryResponse = api.discover()
|
||||
if (discoveryResponse.unifiedpush.gateway == "matrix") {
|
||||
Timber.tag(loggerTag.value).d("The endpoint seems to be a valid UnifiedPush gateway")
|
||||
UnifiedPushGatewayResolverResult.Success(customUrl)
|
||||
} else {
|
||||
// The endpoint returned a 200 OK but didn't promote an actual matrix gateway, which means it doesn't have any
|
||||
Timber.tag(loggerTag.value).w("The endpoint does not seem to be a valid UnifiedPush gateway, using fallback")
|
||||
UnifiedPushGatewayResolverResult.NoMatrixGateway
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
val code = (throwable as? HttpException)?.code()
|
||||
if (code in NoMatrixGatewayResp) {
|
||||
Timber.tag(loggerTag.value).i("Checking for UnifiedPush endpoint yielded $code, using fallback")
|
||||
UnifiedPushGatewayResolverResult.NoMatrixGateway
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).e(throwable, "Error checking for UnifiedPush endpoint")
|
||||
UnifiedPushGatewayResolverResult.Error(customUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NoMatrixGatewayResp = listOf<Int>(
|
||||
HttpURLConnection.HTTP_UNAUTHORIZED,
|
||||
HttpURLConnection.HTTP_FORBIDDEN,
|
||||
HttpURLConnection.HTTP_NOT_FOUND,
|
||||
HttpURLConnection.HTTP_BAD_METHOD,
|
||||
HttpURLConnection.HTTP_NOT_ACCEPTABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
|
||||
interface UnifiedPushGatewayUrlResolver {
|
||||
fun resolve(
|
||||
gatewayResult: UnifiedPushGatewayResolverResult,
|
||||
instance: String,
|
||||
): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushGatewayUrlResolver(
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val defaultPushGatewayHttpUrlProvider: DefaultPushGatewayHttpUrlProvider,
|
||||
) : UnifiedPushGatewayUrlResolver {
|
||||
override fun resolve(
|
||||
gatewayResult: UnifiedPushGatewayResolverResult,
|
||||
instance: String,
|
||||
): String {
|
||||
return when (gatewayResult) {
|
||||
is UnifiedPushGatewayResolverResult.Error -> {
|
||||
// Use previous gateway if any, or the provided one
|
||||
unifiedPushStore.getPushGateway(instance) ?: gatewayResult.gateway
|
||||
}
|
||||
UnifiedPushGatewayResolverResult.ErrorInvalidUrl,
|
||||
UnifiedPushGatewayResolverResult.NoMatrixGateway -> {
|
||||
defaultPushGatewayHttpUrlProvider.provide()
|
||||
}
|
||||
is UnifiedPushGatewayResolverResult.Success -> {
|
||||
gatewayResult.gateway
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("DefaultUnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag)
|
||||
|
||||
/**
|
||||
* Handle new endpoint received from UnifiedPush. Will update the session matching the client secret.
|
||||
*/
|
||||
interface UnifiedPushNewGatewayHandler {
|
||||
suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushNewGatewayHandler(
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : UnifiedPushNewGatewayHandler {
|
||||
override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> {
|
||||
// Register the pusher for the session with this client secret, if is it using UnifiedPush.
|
||||
val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>(
|
||||
IllegalStateException("Unable to retrieve session")
|
||||
).also {
|
||||
Timber.tag(loggerTag.value).w("Unable to retrieve session")
|
||||
}
|
||||
val userDataStore = userPushStoreFactory.getOrCreate(userId)
|
||||
return if (userDataStore.getPushProviderName() == UnifiedPushConfig.NAME) {
|
||||
matrixClientProvider
|
||||
.getOrRestore(userId)
|
||||
.flatMap { client ->
|
||||
pusherSubscriber.registerPusher(client, endpoint, pushGateway)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to register pusher")
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher")
|
||||
Result.failure(
|
||||
IllegalStateException("This session is not using UnifiedPush pusher")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
||||
@Inject
|
||||
class UnifiedPushParser(
|
||||
private val json: JsonProvider,
|
||||
) {
|
||||
fun parse(message: ByteArray, clientSecret: String): PushData? {
|
||||
return tryOrNull { json().decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class UnifiedPushProvider(
|
||||
private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider,
|
||||
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
|
||||
private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val unifiedPushSessionPushConfigProvider: UnifiedPushSessionPushConfigProvider,
|
||||
) : PushProvider {
|
||||
override val index = UnifiedPushConfig.INDEX
|
||||
override val name = UnifiedPushConfig.NAME
|
||||
override val supportMultipleDistributors = true
|
||||
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
return unifiedPushDistributorProvider.getDistributors()
|
||||
}
|
||||
|
||||
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
|
||||
return registerUnifiedPushUseCase.execute(distributor, clientSecret)
|
||||
.onSuccess {
|
||||
unifiedPushStore.setDistributorValue(matrixClient.sessionId, distributor.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCurrentDistributorValue(sessionId: SessionId): String? {
|
||||
return unifiedPushStore.getDistributorValue(sessionId)
|
||||
}
|
||||
|
||||
override suspend fun getCurrentDistributor(sessionId: SessionId): Distributor? {
|
||||
val distributorValue = unifiedPushStore.getDistributorValue(sessionId)
|
||||
return getDistributors().find { it.value == distributorValue }
|
||||
}
|
||||
|
||||
override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
|
||||
return unRegisterUnifiedPushUseCase.unregister(matrixClient, clientSecret)
|
||||
}
|
||||
|
||||
override suspend fun onSessionDeleted(sessionId: SessionId) {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(sessionId)
|
||||
unRegisterUnifiedPushUseCase.cleanup(clientSecret)
|
||||
}
|
||||
|
||||
override suspend fun getPushConfig(sessionId: SessionId): Config? {
|
||||
return unifiedPushSessionPushConfigProvider.provide(sessionId)
|
||||
}
|
||||
|
||||
override fun canRotateToken(): Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("UnifiedPushRemovedGatewayHandler", LoggerTag.PushLoggerTag)
|
||||
|
||||
/**
|
||||
* Handle endpoint removal received from UnifiedPush. Will try to register again.
|
||||
*/
|
||||
fun interface UnifiedPushRemovedGatewayHandler {
|
||||
suspend fun handle(clientSecret: String): Result<Unit>
|
||||
}
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class UnifiedPushRemovedGatewayThrottler(
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) {
|
||||
private val firstThrottler = FirstThrottler(
|
||||
minimumInterval = 60_000,
|
||||
coroutineScope = appCoroutineScope,
|
||||
)
|
||||
|
||||
fun canRegisterAgain(): Boolean {
|
||||
return firstThrottler.canHandle()
|
||||
}
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushRemovedGatewayHandler(
|
||||
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val pushService: PushService,
|
||||
private val unifiedPushRemovedGatewayThrottler: UnifiedPushRemovedGatewayThrottler,
|
||||
) : UnifiedPushRemovedGatewayHandler {
|
||||
/**
|
||||
* The application has been informed by the UnifiedPush distributor that the topic has been deleted.
|
||||
* So this code aim to unregister the pusher from the homeserver, register a new topic on the
|
||||
* UnifiedPush application then register a new pusher to the homeserver.
|
||||
* No registration will happen if the topic deletion has already occurred in the last minute.
|
||||
*/
|
||||
override suspend fun handle(clientSecret: String): Result<Unit> {
|
||||
val sessionId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>(
|
||||
IllegalStateException("Unable to retrieve session")
|
||||
).also {
|
||||
Timber.tag(loggerTag.value).w("Unable to retrieve session")
|
||||
}
|
||||
return matrixClientProvider
|
||||
.getOrRestore(sessionId)
|
||||
.onFailure {
|
||||
// Silently ignore this error (do not invoke onServiceUnregistered)
|
||||
Timber.tag(loggerTag.value).w(it, "Fails to restore client")
|
||||
}
|
||||
.flatMap { client ->
|
||||
client.rotateRegistration(clientSecret = clientSecret)
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Issue during pusher unregistration / re registration")
|
||||
// Let the user know
|
||||
pushService.onServiceUnregistered(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the pusher for the session. Then register again if possible.
|
||||
*/
|
||||
private suspend fun MatrixClient.rotateRegistration(clientSecret: String): Result<Unit> {
|
||||
val unregisterResult = unregisterUnifiedPushUseCase.unregister(
|
||||
matrixClient = this,
|
||||
clientSecret = clientSecret,
|
||||
unregisterUnifiedPush = false,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to unregister pusher")
|
||||
}
|
||||
return unregisterResult.flatMap {
|
||||
registerAgain()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to register again, if possible i.e. the current configuration is known and the
|
||||
* deletion of data in the UnifiedPush application has not already occurred in the last minute.
|
||||
*/
|
||||
private suspend fun MatrixClient.registerAgain(): Result<Unit> {
|
||||
return if (unifiedPushRemovedGatewayThrottler.canRegisterAgain()) {
|
||||
val pushProvider = pushService.getCurrentPushProvider(sessionId)
|
||||
val distributor = pushProvider?.getCurrentDistributor(sessionId)
|
||||
if (pushProvider != null && distributor != null) {
|
||||
pushService.registerWith(
|
||||
matrixClient = this,
|
||||
pushProvider = pushProvider,
|
||||
distributor = distributor,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to register with current data")
|
||||
}
|
||||
} else {
|
||||
Result.failure(IllegalStateException("Unable to register again"))
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).w("Second removal in less than 1 minute, do not register again")
|
||||
Result.failure(IllegalStateException("Too many requests to register again"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
|
||||
interface UnifiedPushSessionPushConfigProvider {
|
||||
suspend fun provide(sessionId: SessionId): Config?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushPushConfigProvider(
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
) : UnifiedPushSessionPushConfigProvider {
|
||||
override suspend fun provide(sessionId: SessionId): Config? {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(sessionId)
|
||||
val url = unifiedPushStore.getPushGateway(clientSecret) ?: return null
|
||||
val pushKey = unifiedPushStore.getEndpoint(clientSecret) ?: return null
|
||||
return Config(
|
||||
url = url,
|
||||
pushKey = pushKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
interface UnifiedPushStore {
|
||||
fun getEndpoint(clientSecret: String): String?
|
||||
fun storeUpEndpoint(clientSecret: String, endpoint: String?)
|
||||
fun getPushGateway(clientSecret: String): String?
|
||||
fun storePushGateway(clientSecret: String, gateway: String?)
|
||||
fun getDistributorValue(userId: UserId): String?
|
||||
fun setDistributorValue(userId: UserId, value: String)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class SharedPreferencesUnifiedPushStore(
|
||||
@ApplicationContext val context: Context,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) : UnifiedPushStore {
|
||||
/**
|
||||
* Retrieves the UnifiedPush Endpoint.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @return the UnifiedPush Endpoint or null if not received
|
||||
*/
|
||||
override fun getEndpoint(clientSecret: String): String? {
|
||||
return sharedPreferences.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store UnifiedPush Endpoint to the SharedPrefs.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @param endpoint the endpoint to store
|
||||
*/
|
||||
override fun storeUpEndpoint(clientSecret: String, endpoint: String?) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Push Gateway.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @return the Push Gateway or null if not defined
|
||||
*/
|
||||
override fun getPushGateway(clientSecret: String): String? {
|
||||
return sharedPreferences.getString(PREFS_PUSH_GATEWAY + clientSecret, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store Push Gateway to the SharedPrefs.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @param gateway the push gateway to store
|
||||
*/
|
||||
override fun storePushGateway(clientSecret: String, gateway: String?) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_PUSH_GATEWAY + clientSecret, gateway)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDistributorValue(userId: UserId): String? {
|
||||
return sharedPreferences.getString(PREFS_DISTRIBUTOR + userId, null)
|
||||
}
|
||||
|
||||
override fun setDistributorValue(userId: UserId, value: String) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_DISTRIBUTOR + userId, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN"
|
||||
private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY"
|
||||
private const val PREFS_DISTRIBUTOR = "DISTRIBUTOR"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
import timber.log.Timber
|
||||
|
||||
interface UnregisterUnifiedPushUseCase {
|
||||
/**
|
||||
* Unregister the app from the homeserver, then from UnifiedPush if [unregisterUnifiedPush] is true.
|
||||
*/
|
||||
suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean = true,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Cleanup any remaining data for the given client secret and unregister the app from UnifiedPush.
|
||||
*/
|
||||
fun cleanup(
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean = true,
|
||||
)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnregisterUnifiedPushUseCase(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
) : UnregisterUnifiedPushUseCase {
|
||||
override suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean,
|
||||
): Result<Unit> {
|
||||
val endpoint = unifiedPushStore.getEndpoint(clientSecret)
|
||||
val gateway = unifiedPushStore.getPushGateway(clientSecret)
|
||||
if (endpoint == null || gateway == null) {
|
||||
Timber.w("No endpoint or gateway found for client secret")
|
||||
// Ensure we don't have any remaining data, but ignore this error
|
||||
cleanup(clientSecret)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
return pusherSubscriber.unregisterPusher(matrixClient, endpoint, gateway)
|
||||
.onSuccess {
|
||||
cleanup(clientSecret, unregisterUnifiedPush)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup(clientSecret: String, unregisterUnifiedPush: Boolean) {
|
||||
unifiedPushStore.storeUpEndpoint(clientSecret, null)
|
||||
unifiedPushStore.storePushGateway(clientSecret, null)
|
||||
if (unregisterUnifiedPush) {
|
||||
UnifiedPush.unregister(context, clientSecret)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.unifiedpush.android.connector.FailedReason
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
import org.unifiedpush.android.connector.data.PushEndpoint
|
||||
import org.unifiedpush.android.connector.data.PushMessage
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver", LoggerTag.PushLoggerTag)
|
||||
|
||||
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||
@Inject lateinit var pushParser: UnifiedPushParser
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
||||
@Inject lateinit var unifiedPushStore: UnifiedPushStore
|
||||
@Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver
|
||||
@Inject lateinit var unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver
|
||||
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
|
||||
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
|
||||
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
|
||||
|
||||
@AppCoroutineScope
|
||||
@Inject lateinit var coroutineScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
context.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when message is received. The message contains the full POST body of the push message.
|
||||
*
|
||||
* @param context the Android context
|
||||
* @param message the message
|
||||
* @param instance connection, for multi-account
|
||||
*/
|
||||
override fun onMessage(context: Context, message: PushMessage, instance: String) {
|
||||
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
|
||||
coroutineScope.launch {
|
||||
val pushData = pushParser.parse(message.content, instance)
|
||||
if (pushData == null) {
|
||||
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
||||
pushHandler.handleInvalid(
|
||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
data = String(message.content),
|
||||
)
|
||||
} else {
|
||||
pushHandler.handle(
|
||||
pushData = pushData,
|
||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new endpoint is to be used for sending push messages.
|
||||
* You should send the endpoint to your application server and sync for missing notifications.
|
||||
*/
|
||||
override fun onNewEndpoint(context: Context, endpoint: PushEndpoint, instance: String) {
|
||||
Timber.tag(loggerTag.value).w("onNewEndpoint: $endpoint")
|
||||
coroutineScope.launch {
|
||||
val gateway = unifiedPushGatewayResolver.getGateway(endpoint.url)
|
||||
.let { gatewayResult ->
|
||||
unifiedPushGatewayUrlResolver.resolve(gatewayResult, instance)
|
||||
}
|
||||
unifiedPushStore.storePushGateway(instance, gateway)
|
||||
val result = newGatewayHandler.handle(endpoint.url, gateway, instance)
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).e(it, "Failed to handle new gateway")
|
||||
}
|
||||
.onSuccess {
|
||||
unifiedPushStore.storeUpEndpoint(instance, endpoint.url)
|
||||
}
|
||||
endpointRegistrationHandler.registrationDone(
|
||||
RegistrationResult(
|
||||
clientSecret = instance,
|
||||
result = result,
|
||||
)
|
||||
)
|
||||
}
|
||||
guardServiceStarter.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the registration is not possible, eg. no network.
|
||||
*/
|
||||
override fun onRegistrationFailed(context: Context, reason: FailedReason, instance: String) {
|
||||
Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance, reason: $reason")
|
||||
coroutineScope.launch {
|
||||
endpointRegistrationHandler.registrationDone(
|
||||
RegistrationResult(
|
||||
clientSecret = instance,
|
||||
result = Result.failure(Exception("Registration failed. Reason: $reason")),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this application is unregistered from receiving push messages.
|
||||
*/
|
||||
override fun onUnregistered(context: Context, instance: String) {
|
||||
Timber.tag(loggerTag.value).w("onUnregistered $instance")
|
||||
coroutineScope.launch {
|
||||
removedGatewayHandler.handle(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Binds
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface VectorUnifiedPushMessagingReceiverBindings {
|
||||
fun inject(receiver: VectorUnifiedPushMessagingReceiver)
|
||||
|
||||
@Binds
|
||||
fun bindsMessagingReceiver(vectorUnifiedPushMessagingReceiver: VectorUnifiedPushMessagingReceiver): MessagingReceiver
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.unifiedpush.network
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DiscoveryResponse(
|
||||
@SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.unifiedpush.network
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DiscoveryUnifiedPush(
|
||||
@SerialName("gateway") val gateway: String = ""
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.unifiedpush.network
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface UnifiedPushApi {
|
||||
@GET("_matrix/push/v1/notify")
|
||||
suspend fun discover(): DiscoveryResponse
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user