UnifiedPush WIP

This commit is contained in:
Benoit Marty 2023-04-11 11:19:44 +02:00 committed by Benoit Marty
parent adfcd61287
commit 95bafe4059
33 changed files with 376 additions and 215 deletions

View file

@ -33,6 +33,7 @@ plugins {
id("com.google.firebase.appdistribution") version "4.0.0" id("com.google.firebase.appdistribution") version "4.0.0"
id("org.jetbrains.kotlinx.knit") version "0.4.0" id("org.jetbrains.kotlinx.knit") version "0.4.0"
id("kotlin-parcelize") id("kotlin-parcelize")
// TODO Move the plugin to the firebase module?
id("com.google.gms.google-services") id("com.google.gms.google-services")
} }

View file

@ -46,9 +46,9 @@ class LoggedInPresenter @Inject constructor(
override fun present(): LoggedInState { override fun present(): LoggedInState {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Ensure pusher is registered // Ensure pusher is registered
// TODO Register with Firebase for now // TODO Manually select push provider for now
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect val pushProvider = pushService.getAvailablePushProviders().find { it.name == "UnifiedPush" } ?: return@LaunchedEffect
val distributor = pushProvider.getDistributorNames().firstOrNull() ?: return@LaunchedEffect val distributor = pushProvider.getDistributors().first()
pushService.registerWith(matrixClient, pushProvider, distributor) pushService.registerWith(matrixClient, pushProvider, distributor)
} }

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
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.PushProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -60,7 +61,7 @@ class LoggedInPresenterTest {
return emptyList() return emptyList()
} }
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
} }
override suspend fun testPush() { override suspend fun testPush() {

View file

@ -18,4 +18,5 @@ package io.element.android.libraries.matrix.api.pusher
interface PushersService { interface PushersService {
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit> suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
suspend fun unsetHttpPusher(): Result<Unit>
} }

View file

@ -53,4 +53,9 @@ class RustPushersService(
} }
} }
} }
override suspend fun unsetHttpPusher(): Result<Unit> {
// TODO Missing client API. We need to set the pusher with Kind == null, but we do not have access to this field from the SDK.
return Result.success(Unit)
}
} }

View file

@ -21,4 +21,5 @@ import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
class FakePushersService : PushersService { class FakePushersService : PushersService {
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit) override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
override suspend fun unsetHttpPusher(): Result<Unit> = Result.success(Unit)
} }

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.push.api package io.element.android.libraries.push.api
import io.element.android.libraries.matrix.api.MatrixClient 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.PushProvider
interface PushService { interface PushService {
@ -30,7 +31,7 @@ interface PushService {
* *
* The method has effect only if the [PushProvider] is different than the current one. * The method has effect only if the [PushProvider] is different than the current one.
*/ */
suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor)
// TODO Move away // TODO Move away
suspend fun testPush() suspend fun testPush()

View file

@ -20,14 +20,19 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.impl.clientsecret.PushClientSecret
import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
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.PushProvider
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultPushService @Inject constructor( class DefaultPushService @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val pushersManager: PushersManager, private val pushersManager: PushersManager,
private val pushClientSecret: PushClientSecret,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>,
) : PushService { ) : PushService {
override fun notificationStyleChanged() { override fun notificationStyleChanged() {
@ -38,10 +43,22 @@ class DefaultPushService @Inject constructor(
return pushProviders.sortedBy { it.index } return pushProviders.sortedBy { it.index }
} }
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { /**
// TODO Get current push provider, compare with provided one, then unregister and register if different, and store change * Get current push provider, compare with provided one, then unregister and register if different, and store change
*/
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
val userPushStore = userPushStoreFactory.create(matrixClient.sessionId.value)
val currentPushProviderName = userPushStore.getPushProviderName()
if (currentPushProviderName != pushProvider.name) {
// Unregister previous one if any
pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient)
}
pushProvider.registerWith(matrixClient, distributorName) val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
pushProvider.registerWith(matrixClient, distributor, clientSecret)
// Store new value
userPushStore.setPushProviderName(pushProvider.name)
} }
override suspend fun testPush() { override suspend fun testPush() {

View file

@ -148,9 +148,8 @@ class PushersManager @Inject constructor(
// currentSession.pushersService().removeEmailPusher(email) // currentSession.pushersService().removeEmailPusher(email)
} }
suspend fun unregisterPusher(pushKey: String) { override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
// val currentSession = activeSessionHolder.getSafeActiveSession() ?: return matrixClient.pushersService().unsetHttpPusher()
// currentSession.pushersService().removeHttpPusher(pushKey, PushConfig.pusher_app_id)
} }
companion object { companion object {

View file

@ -0,0 +1,22 @@
/*
* 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.api
data class Distributor(
val value: String,
val name: String,
)

View file

@ -26,8 +26,23 @@ interface PushProvider {
* Allow to sort provider, from lower index to higher index * Allow to sort provider, from lower index to higher index
*/ */
val index: Int val index: Int
fun getDistributorNames(): List<String>
suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) /**
* User friendly name.
*/
val name: String
fun getDistributors(): List<Distributor>
/**
* Register the pusher to the homeserver
*/
suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String)
/**
* Unregister the pusher
*/
suspend fun unregister(matrixClient: MatrixClient)
/** /**
* Attempt to troubleshoot the push provider * Attempt to troubleshoot the push provider

View file

@ -20,4 +20,5 @@ import io.element.android.libraries.matrix.api.MatrixClient
interface PusherSubscriber { interface PusherSubscriber {
suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String)
suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String)
} }

View file

@ -40,7 +40,7 @@ dependencies {
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
implementation(platform(libs.google.firebase.bom)) implementation(platform(libs.google.firebase.bom))
implementation("com.google.firebase:firebase-messaging-ktx") api("com.google.firebase:firebase-messaging-ktx")
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)

View file

@ -23,5 +23,6 @@ object FirebaseConfig {
*/ */
const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify" const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify"
const val internalName = "NOTIFICATION_METHOD_FIREBASE" const val index = 0
const val name = "Firebase"
} }

View file

@ -43,7 +43,7 @@ class FirebaseNewTokenHandler @Inject constructor(
// Register the pusher for all the sessions // Register the pusher for all the sessions
sessionStore.getAllSessions().toUserList().forEach { userId -> sessionStore.getAllSessions().toUserList().forEach { userId ->
val userDataStore = userPushStoreFactory.create(userId) val userDataStore = userPushStoreFactory.create(userId)
if (userDataStore.getNotificationMethod() == FirebaseConfig.internalName) { if (userDataStore.getPushProviderName() == FirebaseConfig.name) {
matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client ->
pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url)
} }

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.push.providers.firebase
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.MatrixClient 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.PushProvider
import io.element.android.libraries.push.providers.api.PusherSubscriber import io.element.android.libraries.push.providers.api.PusherSubscriber
import timber.log.Timber import timber.log.Timber
@ -30,20 +31,27 @@ class FirebasePushProvider @Inject constructor(
private val firebaseTroubleshooter: FirebaseTroubleshooter, private val firebaseTroubleshooter: FirebaseTroubleshooter,
private val pusherSubscriber: PusherSubscriber, private val pusherSubscriber: PusherSubscriber,
) : PushProvider { ) : PushProvider {
override val index = 0 override val index = FirebaseConfig.index
override val name = FirebaseConfig.name
override fun getDistributorNames(): List<String> { override fun getDistributors(): List<Distributor> {
// Must return an non-empty list for now return listOf(Distributor("Firebase", "Firebase"))
return listOf("unused")
} }
override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) {
val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { val pushKey = firebaseStore.getFcmToken() ?: return Unit.also {
Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.")
} }
pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url)
} }
override suspend fun unregister(matrixClient: MatrixClient) {
val pushKey = firebaseStore.getFcmToken() ?: return Unit.also {
Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.")
}
pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url)
}
override suspend fun troubleshoot(): Result<Unit> { override suspend fun troubleshoot(): Result<Unit> {
return firebaseTroubleshooter.troubleshoot() return firebaseTroubleshooter.troubleshoot()
} }

View file

@ -30,14 +30,21 @@ anvil {
dependencies { dependencies {
implementation(libs.dagger) implementation(libs.dagger)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)
implementation(projects.libraries.network)
implementation(platform(libs.network.okhttp.bom))
implementation("com.squareup.okhttp3:okhttp")
implementation(libs.network.retrofit)
implementation(libs.serialization.json) implementation(libs.serialization.json)
// UnifiedPush library // UnifiedPush library

View file

@ -55,13 +55,13 @@ data class PushDataUnifiedPushCounts(
@SerialName("unread") val unread: Int? = null @SerialName("unread") val unread: Int? = null
) )
fun PushDataUnifiedPush.toPushData(): PushData? { fun PushDataUnifiedPush.toPushData(clientSecret: String): PushData? {
val safeEventId = notification?.eventId?.asEventId() ?: return null val safeEventId = notification?.eventId?.asEventId() ?: return null
val safeRoomId = notification.roomId?.asRoomId() ?: return null val safeRoomId = notification.roomId?.asRoomId() ?: return null
return PushData( return PushData(
eventId = safeEventId, eventId = safeEventId,
roomId = safeRoomId, roomId = safeRoomId,
unread = notification.counts?.unread, unread = notification.counts?.unread,
clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush clientSecret = clientSecret
) )
} }

View file

@ -18,45 +18,56 @@ package io.element.android.libraries.push.providers.unifiedpush
import android.content.Context import android.content.Context
import io.element.android.libraries.di.ApplicationContext 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.PusherSubscriber
import org.unifiedpush.android.connector.UnifiedPush import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject import javax.inject.Inject
class RegisterUnifiedPushUseCase @Inject constructor( class RegisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val pusherSubscriber: PusherSubscriber,
private val unifiedPushStore: UnifiedPushStore,
) { ) {
sealed interface RegisterUnifiedPushResult { sealed interface RegisterUnifiedPushResult {
object Success : RegisterUnifiedPushResult object Success : RegisterUnifiedPushResult
object NeedToAskUserForDistributor : RegisterUnifiedPushResult object NeedToAskUserForDistributor : RegisterUnifiedPushResult
object Error : RegisterUnifiedPushResult
} }
fun execute(distributor: String = ""): RegisterUnifiedPushResult { suspend fun execute(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String): RegisterUnifiedPushResult {
if (distributor.isNotEmpty()) { val distributorValue = distributor.value
saveAndRegisterApp(distributor) if (distributorValue.isNotEmpty()) {
saveAndRegisterApp(distributorValue, clientSecret)
val endpoint = unifiedPushStore.getEndpoint() ?: return RegisterUnifiedPushResult.Error
val gateway = unifiedPushStore.getPushGateway() ?: return RegisterUnifiedPushResult.Error
pusherSubscriber.registerPusher(matrixClient, endpoint, gateway)
return RegisterUnifiedPushResult.Success return RegisterUnifiedPushResult.Success
} }
// TODO Below should never happen?
if (UnifiedPush.getDistributor(context).isNotEmpty()) { if (UnifiedPush.getDistributor(context).isNotEmpty()) {
registerApp() registerApp(clientSecret)
return RegisterUnifiedPushResult.Success return RegisterUnifiedPushResult.Success
} }
val distributors = UnifiedPush.getDistributors(context) val distributors = UnifiedPush.getDistributors(context)
return if (distributors.size == 1) { return if (distributors.size == 1) {
saveAndRegisterApp(distributors.first()) saveAndRegisterApp(distributors.first(), clientSecret)
RegisterUnifiedPushResult.Success RegisterUnifiedPushResult.Success
} else { } else {
RegisterUnifiedPushResult.NeedToAskUserForDistributor RegisterUnifiedPushResult.NeedToAskUserForDistributor
} }
} }
private fun saveAndRegisterApp(distributor: String) { private fun saveAndRegisterApp(distributor: String, clientSecret: String) {
UnifiedPush.saveDistributor(context, distributor) UnifiedPush.saveDistributor(context, distributor)
registerApp() registerApp(clientSecret)
} }
private fun registerApp() { private fun registerApp(clientSecret: String) {
UnifiedPush.registerApp(context) UnifiedPush.registerApp(context = context, instance = clientSecret)
} }
} }

View file

@ -23,5 +23,6 @@ object UnifiedPushConfig {
*/ */
const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
const val internalName = "NOTIFICATION_METHOD_UNIFIEDPUSH" const val index = 1
const val name = "UnifiedPush"
} }

View file

@ -17,11 +17,13 @@
package io.element.android.libraries.push.providers.unifiedpush package io.element.android.libraries.push.providers.unifiedpush
import android.content.Context 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.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 io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.serialization.SerialName import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.unifiedpush.android.connector.UnifiedPush
import timber.log.Timber import timber.log.Timber
import java.net.URL import java.net.URL
import javax.inject.Inject import javax.inject.Inject
@ -30,132 +32,34 @@ class UnifiedPushHelper @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val unifiedPushStore: UnifiedPushStore, private val unifiedPushStore: UnifiedPushStore,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: CoroutineDispatchers,
) { ) {
suspend fun storeCustomOrDefaultGateway(endpoint: String) {
/* TODO EAx val gateway = UnifiedPushConfig.default_push_gateway_http_url
@MainThread
fun showSelectDistributorDialog(
context: Context,
onDistributorSelected: (String) -> Unit,
) {
val internalDistributorName = stringProvider.getString(
if (fcmHelper.isFirebaseAvailable()) {
R.string.push_distributor_firebase_android
} else {
R.string.push_distributor_background_sync_android
}
)
val distributors = UnifiedPush.getDistributors(context)
val distributorsName = distributors.map {
if (it == context.packageName) {
internalDistributorName
} else {
context.getApplicationLabel(it)
}
}
MaterialAlertDialogBuilder(context)
.setTitle(stringProvider.getString(R.string.push_choose_distributor_dialog_title_android))
.setItems(distributorsName.toTypedArray()) { _, which ->
val distributor = distributors[which]
onDistributorSelected(distributor)
}
.setOnCancelListener {
// we do not want to change the distributor on behalf of the user
if (UnifiedPush.getDistributor(context).isEmpty()) {
// By default, use internal solution (fcm/background sync)
onDistributorSelected(context.packageName)
}
}
.setCancelable(true)
.show()
}
*/
@Serializable
internal data class DiscoveryResponse(
@SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()
)
@Serializable
internal data class DiscoveryUnifiedPush(
@SerialName("gateway") val gateway: String = ""
)
suspend fun storeCustomOrDefaultGateway(
endpoint: String,
onDoneRunnable: Runnable? = null
) {
// if we use the embedded distributor,
// register app_id type upfcm on sygnal
// the pushkey if FCM key
/*
if (UnifiedPush.getDistributor(context) == context.packageName) {
unifiedPushStore.storePushGateway(PushConfig.pusher_http_url)
onDoneRunnable?.run()
return
}
*/
/* TODO EAx UnifiedPush
// else, unifiedpush, and pushkey is an endpoint
val gateway = PushConfig.default_push_gateway_http_url
val parsed = URL(endpoint) val parsed = URL(endpoint)
val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify"
Timber.i("Testing $custom") Timber.i("Testing $custom")
try { try {
val response = matrix.rawService().getUrl(custom, CacheStrategy.NoCache) withContext(coroutineDispatchers.io) {
tryOrNull { Json.decodeFromString<DiscoveryResponse>(response) } val api = retrofitFactory.create("${parsed.protocol}://${parsed.host}")
?.let { discoveryResponse -> .create(UnifiedPushApi::class.java)
if (discoveryResponse.unifiedpush.gateway == "matrix") { tryOrNull { api.discover() }
Timber.d("Using custom gateway") ?.let { discoveryResponse ->
unifiedPushStore.storePushGateway(custom) if (discoveryResponse.unifiedpush.gateway == "matrix") {
onDoneRunnable?.run() Timber.d("Using custom gateway")
return unifiedPushStore.storePushGateway(custom)
}
} }
} }
return
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.d(e, "Cannot try custom gateway") Timber.d(e, "Cannot try custom gateway")
} }
unifiedPushStore.storePushGateway(gateway) unifiedPushStore.storePushGateway(gateway)
onDoneRunnable?.run()
*/
} }
fun getExternalDistributors(): List<String> { private fun isEmbeddedDistributor() = false
return UnifiedPush.getDistributors(context)
.filterNot { it == context.packageName }
}
fun getCurrentDistributorName(): String {
TODO()
/*
return when {
isEmbeddedDistributor() -> stringProvider.getString(R.string.push_distributor_firebase_android)
isBackgroundSync() -> stringProvider.getString(R.string.push_distributor_background_sync_android)
else -> context.getApplicationLabel(UnifiedPush.getDistributor(context))
}
*/
}
fun isEmbeddedDistributor(): Boolean {
TODO()
//return isInternalDistributor() && fcmHelper.isFirebaseAvailable()
}
fun isBackgroundSync(): Boolean {
TODO()
//return isInternalDistributor() && !fcmHelper.isFirebaseAvailable()
}
private fun isInternalDistributor(): Boolean {
return UnifiedPush.getDistributor(context).isEmpty() ||
UnifiedPush.getDistributor(context) == context.packageName
}
fun getPrivacyFriendlyUpEndpoint(): String? { fun getPrivacyFriendlyUpEndpoint(): String? {
val endpoint = getEndpointOrToken() val endpoint = getEndpointOrToken()

View file

@ -0,0 +1,53 @@
/*
* 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.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 timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler")
/**
* Handle new endpoint received from UnifiedPush. Will update all the sessions which are using UnifiedPush as a push provider.
*/
class UnifiedPushNewGatewayHandler @Inject constructor(
private val pusherSubscriber: PusherSubscriber,
private val sessionStore: SessionStore,
private val userPushStoreFactory: UserPushStoreFactory,
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")
}
}
}
}

View file

@ -25,7 +25,7 @@ import javax.inject.Inject
class UnifiedPushParser @Inject constructor() { class UnifiedPushParser @Inject constructor() {
private val json by lazy { Json { ignoreUnknownKeys = true } } private val json by lazy { Json { ignoreUnknownKeys = true } }
fun parse(message: ByteArray): PushData? { fun parse(message: ByteArray, clientSecret: String): PushData? {
return tryOrNull { json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData() return tryOrNull { json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
} }
} }

View file

@ -16,19 +16,41 @@
package io.element.android.libraries.push.providers.unifiedpush package io.element.android.libraries.push.providers.unifiedpush
import android.content.Context
import io.element.android.libraries.androidutils.system.getApplicationLabel
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient 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.PushProvider
import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject import javax.inject.Inject
class UnifiedPushProvider @Inject constructor() : PushProvider { class UnifiedPushProvider @Inject constructor(
override val index = 1 @ApplicationContext private val context: Context,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
) : PushProvider {
override val index = UnifiedPushConfig.index
override val name = UnifiedPushConfig.name
override fun getDistributorNames(): List<String> { override fun getDistributors(): List<Distributor> {
TODO("Not yet implemented") val distributors = UnifiedPush.getDistributors(context)
return distributors.mapNotNull {
if (it == context.packageName) {
// Exclude self
null
} else {
Distributor(it, context.getApplicationLabel(it))
}
}
} }
override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) {
TODO("Not yet implemented") registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret)
}
override suspend fun unregister(matrixClient: MatrixClient) {
unRegisterUnifiedPushUseCase.execute()
} }
override suspend fun troubleshoot(): Result<Unit> { override suspend fun troubleshoot(): Result<Unit> {

View file

@ -16,21 +16,26 @@
package io.element.android.libraries.push.providers.unifiedpush package io.element.android.libraries.push.providers.unifiedpush
/* import android.content.Context
import io.element.android.libraries.di.ApplicationContext
import org.unifiedpush.android.connector.UnifiedPush
import timber.log.Timber
import javax.inject.Inject
class UnregisterUnifiedPushUseCase @Inject constructor( class UnregisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val pushDataStore: PushDataStore, //private val pushDataStore: PushDataStore,
private val unifiedPushStore: UnifiedPushStore, private val unifiedPushStore: UnifiedPushStore,
private val unifiedPushHelper: UnifiedPushHelper, private val unifiedPushHelper: UnifiedPushHelper,
) { ) {
suspend fun execute(pushersManager: PushersManager?) { suspend fun execute(/*pushersManager: PushersManager?*/) {
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
pushDataStore.setFdroidSyncBackgroundMode(mode) //pushDataStore.setFdroidSyncBackgroundMode(mode)
try { try {
unifiedPushHelper.getEndpointOrToken()?.let { unifiedPushHelper.getEndpointOrToken()?.let {
Timber.d("Removing $it") Timber.d("Removing $it")
pushersManager?.unregisterPusher(it) // TODO pushersManager?.unregisterPusher(it)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher") Timber.d(e, "Probably unregistering a non existing pusher")
@ -40,4 +45,3 @@ class UnregisterUnifiedPushUseCase @Inject constructor(
UnifiedPush.unregisterApp(context) UnifiedPush.unregisterApp(context)
} }
} }
*/

View file

@ -21,6 +21,7 @@ import android.content.Intent
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag 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.PushHandler
import io.element.android.libraries.push.providers.api.PusherSubscriber
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -32,22 +33,24 @@ private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver")
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
@Inject lateinit var pushParser: UnifiedPushParser @Inject lateinit var pushParser: UnifiedPushParser
// @Inject lateinit var pushDataStore: PushDataStore
// @Inject lateinit var pushDataStore: PushDataStore
@Inject lateinit var pushHandler: PushHandler @Inject lateinit var pushHandler: PushHandler
@Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var guardServiceStarter: GuardServiceStarter
// @Inject lateinit var unifiedPushStore: UnifiedPushStore @Inject lateinit var unifiedPushStore: UnifiedPushStore
// @Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var pusherSubscriber: PusherSubscriber
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
private val coroutineScope = CoroutineScope(SupervisorJob()) private val coroutineScope = CoroutineScope(SupervisorJob())
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
// Inject
context.applicationContext.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this) context.applicationContext.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
super.onReceive(context, intent)
} }
/** /**
* Called when message is received. * Called when message is received. The message contains the full POST body of the push message.
* *
* @param context the Android context * @param context the Android context
* @param message the message * @param message the message
@ -56,7 +59,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
override fun onMessage(context: Context, message: ByteArray, instance: String) { override fun onMessage(context: Context, message: ByteArray, instance: String) {
Timber.tag(loggerTag.value).d("New message") Timber.tag(loggerTag.value).d("New message")
coroutineScope.launch { coroutineScope.launch {
val pushData = pushParser.parse(message) val pushData = pushParser.parse(message, instance)
if (pushData == null) { if (pushData == null) {
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush") Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
} else { } else {
@ -65,36 +68,36 @@ 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) { override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
TODO()
/*
Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint")
if (pushDataStore.areNotificationEnabledForDevice() /* TODO EAx && activeSessionHolder.hasActiveSession() */) { // If the endpoint has changed
// If the endpoint has changed // or the gateway has changed
// or the gateway has changed if (unifiedPushHelper.getEndpointOrToken() != endpoint) {
if (unifiedPushHelper.getEndpointOrToken() != endpoint) { unifiedPushStore.storeUpEndpoint(endpoint)
unifiedPushStore.storeUpEndpoint(endpoint) coroutineScope.launch {
coroutineScope.launch { unifiedPushHelper.storeCustomOrDefaultGateway(endpoint)
unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { unifiedPushHelper.getPushGateway()?.let { pushGateway ->
unifiedPushHelper.getPushGateway()?.let { newGatewayHandler.handle(endpoint, pushGateway)
coroutineScope.launch {
pushersManager.onNewUnifiedPushEndpoint(endpoint, it)
}
}
}
} }
} else {
Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
} }
} else {
Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
} }
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED
pushDataStore.setFdroidSyncBackgroundMode(mode) //pushDataStore.setFdroidSyncBackgroundMode(mode)
guardServiceStarter.stop() guardServiceStarter.stop()
*/
} }
/**
* Called when the registration is not possible, eg. no network.
*/
override fun onRegistrationFailed(context: Context, instance: String) { override fun onRegistrationFailed(context: Context, instance: String) {
TODO() Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance")
/* /*
Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show()
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
@ -103,10 +106,13 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
*/ */
} }
/**
* Called when this application is unregistered from receiving push messages.
*/
override fun onUnregistered(context: Context, instance: String) { override fun onUnregistered(context: Context, instance: String) {
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
TODO() TODO()
/* /*
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
pushDataStore.setFdroidSyncBackgroundMode(mode) pushDataStore.setFdroidSyncBackgroundMode(mode)
guardServiceStarter.start() guardServiceStarter.start()

View file

@ -0,0 +1,25 @@
/*
* 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.network
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class DiscoveryResponse(
@SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()
)

View file

@ -0,0 +1,25 @@
/*
* 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.network
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class DiscoveryUnifiedPush(
@SerialName("gateway") val gateway: String = ""
)

View file

@ -0,0 +1,24 @@
/*
* 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.network
import retrofit2.http.GET
interface UnifiedPushApi {
@GET("_matrix/push/v1/notify")
suspend fun discover(): DiscoveryResponse
}

View file

@ -20,56 +20,65 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EVENT_ID 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_ROOM_ID
import io.element.android.libraries.push.providers.api.PushData import io.element.android.libraries.push.providers.api.PushData
import org.junit.Assert.assertThrows
import org.junit.Test import org.junit.Test
class UnifiedPushParserTest { class UnifiedPushParserTest {
private val aClientSecret = "a-client-secret"
private val validData = PushData( private val validData = PushData(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
unread = 1, unread = 1,
// TODO handle client secret here. clientSecret = aClientSecret
clientSecret = null
) )
@Test @Test
fun `test edge cases UnifiedPush`() { fun `test edge cases UnifiedPush`() {
val pushParser = UnifiedPushParser() val pushParser = UnifiedPushParser()
// Empty string // Empty string
assertThat(pushParser.parse("".toByteArray())).isNull() assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull()
// Empty Json // Empty Json
assertThat(pushParser.parse("{}".toByteArray())).isNull() assertThat(pushParser.parse("{}".toByteArray(), aClientSecret)).isNull()
// Bad Json // Bad Json
assertThat(pushParser.parse("ABC".toByteArray())).isNull() assertThat(pushParser.parse("ABC".toByteArray(), aClientSecret)).isNull()
} }
@Test @Test
fun `test UnifiedPush format`() { fun `test UnifiedPush format`() {
val pushParser = UnifiedPushParser() val pushParser = UnifiedPushParser()
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray())).isEqualTo(validData) assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData)
} }
@Test @Test
fun `test empty roomId`() { fun `test empty roomId`() {
val pushParser = UnifiedPushParser() val pushParser = UnifiedPushParser()
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isNull() assertThrows(IllegalStateException::class.java) {
pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret)
}
} }
@Test @Test
fun `test invalid roomId`() { fun `test invalid roomId`() {
val pushParser = UnifiedPushParser() val pushParser = UnifiedPushParser()
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isNull() assertThrows(IllegalStateException::class.java) {
pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret)
}
} }
@Test @Test
fun `test empty eventId`() { fun `test empty eventId`() {
val pushParser = UnifiedPushParser() val pushParser = UnifiedPushParser()
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isNull() assertThrows(IllegalStateException::class.java) {
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret)
}
} }
@Test @Test
fun `test invalid eventId`() { fun `test invalid eventId`() {
val pushParser = UnifiedPushParser() val pushParser = UnifiedPushParser()
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isNull() assertThrows(IllegalStateException::class.java) {
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret)
}
} }
companion object { companion object {

View file

@ -20,12 +20,9 @@ package io.element.android.libraries.pushstore.api
* Store data related to push about a user. * Store data related to push about a user.
*/ */
interface UserPushStore { interface UserPushStore {
/** suspend fun getPushProviderName(): String?
* [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH].
*/
suspend fun getNotificationMethod(): String?
suspend fun setNotificationMethod(value: String) suspend fun setPushProviderName(value: String)
suspend fun getCurrentRegisteredPushKey(): String? suspend fun getCurrentRegisteredPushKey(): String?

View file

@ -33,16 +33,16 @@ class UserPushStoreDataStore(
userId: String, userId: String,
) : UserPushStore { ) : UserPushStore {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_store_$userId") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_store_$userId")
private val notificationMethod = stringPreferencesKey("notificationMethod") private val pushProviderName = stringPreferencesKey("pushProviderName")
private val currentPushKey = stringPreferencesKey("currentPushKey") private val currentPushKey = stringPreferencesKey("currentPushKey")
override suspend fun getNotificationMethod(): String? { override suspend fun getPushProviderName(): String? {
return context.dataStore.data.first()[notificationMethod] return context.dataStore.data.first()[pushProviderName]
} }
override suspend fun setNotificationMethod(value: String) { override suspend fun setPushProviderName(value: String) {
context.dataStore.edit { context.dataStore.edit {
it[notificationMethod] = value it[pushProviderName] = value
} }
} }

View file

@ -79,7 +79,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
// Comment to not include firebase in the project // Comment to not include firebase in the project
implementation(project(":libraries:pushproviders:firebase")) implementation(project(":libraries:pushproviders:firebase"))
// Comment to not include unified push in the project // Comment to not include unified push in the project
// implementation(project(":libraries:pushproviders:unifiedpush")) implementation(project(":libraries:pushproviders:unifiedpush"))
implementation(project(":libraries:pushstore:impl")) implementation(project(":libraries:pushstore:impl"))
implementation(project(":libraries:architecture")) implementation(project(":libraries:architecture"))
implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:dateformatter:impl"))