UnifiedPush WIP

This commit is contained in:
Benoit Marty
2023-04-11 16:29:31 +02:00
committed by Benoit Marty
parent 95bafe4059
commit 80268156b5
28 changed files with 155 additions and 171 deletions

View File

@@ -37,7 +37,7 @@ interface PushProvider {
/**
* Register the pusher to the homeserver
*/
suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String)
suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor)
/**
* Unregister the pusher

View File

@@ -18,7 +18,7 @@ package io.element.android.libraries.push.providers.firebase
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.asSessionId
import io.element.android.libraries.push.providers.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.sessionstorage.api.SessionStore
@@ -41,15 +41,17 @@ class FirebaseNewTokenHandler @Inject constructor(
suspend fun handle(firebaseToken: String) {
firebaseStore.storeFcmToken(firebaseToken)
// Register the pusher for all the sessions
sessionStore.getAllSessions().toUserList().forEach { userId ->
val userDataStore = userPushStoreFactory.create(userId)
if (userDataStore.getPushProviderName() == FirebaseConfig.name) {
matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client ->
pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url)
sessionStore.getAllSessions().toUserList()
.map { it.asSessionId() }
.forEach { userId ->
val userDataStore = userPushStoreFactory.create(userId)
if (userDataStore.getPushProviderName() == FirebaseConfig.name) {
matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client ->
pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url)
}
} else {
Timber.tag(loggerTag.value).d("This session is not using Firebase pusher")
}
} else {
Timber.tag(loggerTag.value).d("This session is not using Firebase pusher")
}
}
}
}

View File

@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.providers.api.Distributor
import io.element.android.libraries.push.providers.api.PushProvider
import io.element.android.libraries.push.providers.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import timber.log.Timber
import javax.inject.Inject
@@ -30,6 +31,7 @@ class FirebasePushProvider @Inject constructor(
private val firebaseStore: FirebaseStore,
private val firebaseTroubleshooter: FirebaseTroubleshooter,
private val pusherSubscriber: PusherSubscriber,
private val pushClientSecret: PushClientSecret,
) : PushProvider {
override val index = FirebaseConfig.index
override val name = FirebaseConfig.name
@@ -38,7 +40,7 @@ class FirebasePushProvider @Inject constructor(
return listOf(Distributor("Firebase", "Firebase"))
}
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) {
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) {
val pushKey = firebaseStore.getFcmToken() ?: return Unit.also {
Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.")
}

View File

@@ -40,8 +40,8 @@ class RegisterUnifiedPushUseCase @Inject constructor(
val distributorValue = distributor.value
if (distributorValue.isNotEmpty()) {
saveAndRegisterApp(distributorValue, clientSecret)
val endpoint = unifiedPushStore.getEndpoint() ?: return RegisterUnifiedPushResult.Error
val gateway = unifiedPushStore.getPushGateway() ?: return RegisterUnifiedPushResult.Error
val endpoint = unifiedPushStore.getEndpoint(clientSecret) ?: return RegisterUnifiedPushResult.Error
val gateway = unifiedPushStore.getPushGateway(clientSecret) ?: return RegisterUnifiedPushResult.Error
pusherSubscriber.registerPusher(matrixClient, endpoint, gateway)
return RegisterUnifiedPushResult.Success
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.providers.unifiedpush
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.URL
import javax.inject.Inject
class UnifiedPushGatewayResolver @Inject constructor(
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: CoroutineDispatchers,
) {
suspend fun getGateway(endpoint: String): String? {
val gateway = UnifiedPushConfig.default_push_gateway_http_url
val url = URL(endpoint)
val custom = "${url.protocol}://${url.host}/_matrix/push/v1/notify"
Timber.i("Testing $custom")
try {
return withContext(coroutineDispatchers.io) {
val api = retrofitFactory.create("${url.protocol}://${url.host}")
.create(UnifiedPushApi::class.java)
try {
val discoveryResponse = api.discover()
if (discoveryResponse.unifiedpush.gateway == "matrix") {
Timber.d("Using custom gateway")
return@withContext custom
}
} catch (throwable: Throwable) {
Timber.tag("UnifiedPushHelper").e(throwable)
}
return@withContext gateway
}
} catch (e: Throwable) {
Timber.d(e, "Cannot try custom gateway")
}
return gateway
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.providers.unifiedpush
import android.content.Context
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.URL
import javax.inject.Inject
class UnifiedPushHelper @Inject constructor(
@ApplicationContext private val context: Context,
private val unifiedPushStore: UnifiedPushStore,
private val stringProvider: StringProvider,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: CoroutineDispatchers,
) {
suspend fun storeCustomOrDefaultGateway(endpoint: String) {
val gateway = UnifiedPushConfig.default_push_gateway_http_url
val parsed = URL(endpoint)
val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify"
Timber.i("Testing $custom")
try {
withContext(coroutineDispatchers.io) {
val api = retrofitFactory.create("${parsed.protocol}://${parsed.host}")
.create(UnifiedPushApi::class.java)
tryOrNull { api.discover() }
?.let { discoveryResponse ->
if (discoveryResponse.unifiedpush.gateway == "matrix") {
Timber.d("Using custom gateway")
unifiedPushStore.storePushGateway(custom)
}
}
}
return
} catch (e: Throwable) {
Timber.d(e, "Cannot try custom gateway")
}
unifiedPushStore.storePushGateway(gateway)
}
private fun isEmbeddedDistributor() = false
fun getPrivacyFriendlyUpEndpoint(): String? {
val endpoint = getEndpointOrToken()
if (endpoint.isNullOrEmpty()) return null
if (isEmbeddedDistributor()) {
return endpoint
}
return try {
val parsed = URL(endpoint)
"${parsed.protocol}://${parsed.host}/***"
} catch (e: Exception) {
Timber.e(e, "Error parsing unifiedpush endpoint")
null
}
}
fun getEndpointOrToken(): String? {
// TODO
return if (isEmbeddedDistributor()) "" // fcmHelper.getFcmToken()
else unifiedPushStore.getEndpoint()
}
fun getPushGateway(): String? {
return if (isEmbeddedDistributor()) "" // PushConfig.pusher_http_url
else unifiedPushStore.getPushGateway()
}
}

View File

@@ -18,11 +18,9 @@ package io.element.android.libraries.push.providers.unifiedpush
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.providers.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 io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import timber.log.Timber
import javax.inject.Inject
@@ -33,21 +31,22 @@ private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler")
*/
class UnifiedPushNewGatewayHandler @Inject constructor(
private val pusherSubscriber: PusherSubscriber,
private val sessionStore: SessionStore,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushClientSecret: PushClientSecret,
private val matrixAuthenticationService: MatrixAuthenticationService,
) {
suspend fun handle(endpoint: String, pushGateway: String) {
// Register the pusher for all the sessions which are using UnifiedPush.
sessionStore.getAllSessions().toUserList().forEach { userId ->
val userDataStore = userPushStoreFactory.create(userId)
if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) {
matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client ->
pusherSubscriber.registerPusher(client, endpoint, pushGateway)
}
} else {
Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher")
suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String) {
// Register the pusher for the session with this client secret, if is it using UnifiedPush.
val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Unit.also {
Timber.w("Unable to retrieve session")
}
val userDataStore = userPushStoreFactory.create(userId)
if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) {
matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client ->
pusherSubscriber.registerPusher(client, endpoint, pushGateway)
}
} else {
Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher")
}
}
}

View File

@@ -22,6 +22,7 @@ import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.providers.api.Distributor
import io.element.android.libraries.push.providers.api.PushProvider
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject
@@ -29,6 +30,7 @@ class UnifiedPushProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
private val pushClientSecret: PushClientSecret,
) : PushProvider {
override val index = UnifiedPushConfig.index
override val name = UnifiedPushConfig.name
@@ -45,12 +47,14 @@ class UnifiedPushProvider @Inject constructor(
}
}
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) {
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) {
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret)
}
override suspend fun unregister(matrixClient: MatrixClient) {
unRegisterUnifiedPushUseCase.execute()
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
unRegisterUnifiedPushUseCase.execute(clientSecret)
}
override suspend fun troubleshoot(): Result<Unit> {

View File

@@ -23,9 +23,6 @@ import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.DefaultPreferences
import javax.inject.Inject
/**
* TODO EAx Store in BDD (for multisession).
*/
class UnifiedPushStore @Inject constructor(
@ApplicationContext val context: Context,
@DefaultPreferences private val defaultPrefs: SharedPreferences,
@@ -35,8 +32,8 @@ class UnifiedPushStore @Inject constructor(
*
* @return the UnifiedPush Endpoint or null if not received
*/
fun getEndpoint(): String? {
return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null)
fun getEndpoint(clientSecret: String): String? {
return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null)
}
/**
@@ -44,9 +41,9 @@ class UnifiedPushStore @Inject constructor(
*
* @param endpoint the endpoint to store
*/
fun storeUpEndpoint(endpoint: String?) {
fun storeUpEndpoint(endpoint: String?, clientSecret: String) {
defaultPrefs.edit {
putString(PREFS_ENDPOINT_OR_TOKEN, endpoint)
putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint)
}
}
@@ -55,8 +52,8 @@ class UnifiedPushStore @Inject constructor(
*
* @return the Push Gateway or null if not defined
*/
fun getPushGateway(): String? {
return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null)
fun getPushGateway(clientSecret: String): String? {
return defaultPrefs.getString(PREFS_PUSH_GATEWAY + clientSecret, null)
}
/**
@@ -64,9 +61,9 @@ class UnifiedPushStore @Inject constructor(
*
* @param gateway the push gateway to store
*/
fun storePushGateway(gateway: String?) {
fun storePushGateway(gateway: String?, clientSecret: String) {
defaultPrefs.edit {
putString(PREFS_PUSH_GATEWAY, gateway)
putString(PREFS_PUSH_GATEWAY + clientSecret, gateway)
}
}

View File

@@ -26,22 +26,22 @@ class UnregisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
//private val pushDataStore: PushDataStore,
private val unifiedPushStore: UnifiedPushStore,
private val unifiedPushHelper: UnifiedPushHelper,
private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver,
) {
suspend fun execute(/*pushersManager: PushersManager?*/) {
suspend fun execute(clientSecret: String /*pushersManager: PushersManager?*/) {
//val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
//pushDataStore.setFdroidSyncBackgroundMode(mode)
try {
unifiedPushHelper.getEndpointOrToken()?.let {
unifiedPushStore.getEndpoint(clientSecret)?.let {
Timber.d("Removing $it")
// TODO pushersManager?.unregisterPusher(it)
}
} catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher")
}
unifiedPushStore.storeUpEndpoint(null)
unifiedPushStore.storePushGateway(null)
unifiedPushStore.storeUpEndpoint(null, clientSecret)
unifiedPushStore.storePushGateway(null, clientSecret)
UnifiedPush.unregisterApp(context)
}
}

View File

@@ -21,7 +21,6 @@ import android.content.Intent
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.push.providers.api.PushHandler
import io.element.android.libraries.push.providers.api.PusherSubscriber
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@@ -33,13 +32,10 @@ private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver")
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
@Inject lateinit var pushParser: UnifiedPushParser
// @Inject lateinit var pushDataStore: PushDataStore
@Inject lateinit var pushHandler: PushHandler
@Inject lateinit var guardServiceStarter: GuardServiceStarter
@Inject lateinit var unifiedPushStore: UnifiedPushStore
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var pusherSubscriber: PusherSubscriber
@Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
private val coroutineScope = CoroutineScope(SupervisorJob())
@@ -71,25 +67,23 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
/**
* 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.
* TODO use [instance] for multi-account
*/
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint")
// If the endpoint has changed
// or the gateway has changed
if (unifiedPushHelper.getEndpointOrToken() != endpoint) {
unifiedPushStore.storeUpEndpoint(endpoint)
if (unifiedPushStore.getEndpoint(instance) != endpoint) {
unifiedPushStore.storeUpEndpoint(endpoint, instance)
coroutineScope.launch {
unifiedPushHelper.storeCustomOrDefaultGateway(endpoint)
unifiedPushHelper.getPushGateway()?.let { pushGateway ->
newGatewayHandler.handle(endpoint, pushGateway)
val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
unifiedPushStore.storePushGateway(gateway, instance)
gateway?.let { pushGateway ->
newGatewayHandler.handle(endpoint, pushGateway, instance)
}
}
} else {
Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
}
//val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED
//pushDataStore.setFdroidSyncBackgroundMode(mode)
guardServiceStarter.stop()
}