Troubleshoot notifications screen

This commit is contained in:
Benoit Marty
2024-03-26 11:36:31 +01:00
parent 5ff74caed8
commit 8588ce7a72
80 changed files with 3086 additions and 99 deletions

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2024 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.pushproviders.api
data class CurrentUserPushConfig(
val url: String,
val pushKey: String,
)

View File

@@ -49,8 +49,5 @@ interface PushProvider {
*/
suspend fun unregister(matrixClient: MatrixClient)
/**
* Attempt to troubleshoot the push provider.
*/
suspend fun troubleshoot(): Result<Unit>
suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig?
}

View File

@@ -51,8 +51,10 @@ dependencies {
exclude(group = "com.google.firebase", module = "firebase-measurement-connector")
}
testImplementation(libs.coroutines.test)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
}

View File

@@ -16,14 +16,11 @@
package io.element.android.libraries.pushproviders.firebase
import android.content.Context
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushproviders.api.PusherSubscriber
@@ -34,25 +31,15 @@ private val loggerTag = LoggerTag("FirebasePushProvider", LoggerTag.PushLoggerTa
@ContributesMultibinding(AppScope::class)
class FirebasePushProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val firebaseStore: FirebaseStore,
private val firebaseTroubleshooter: FirebaseTroubleshooter,
private val pusherSubscriber: PusherSubscriber,
private val isPlayServiceAvailable: IsPlayServiceAvailable,
) : PushProvider {
override val index = FirebaseConfig.INDEX
override val name = FirebaseConfig.NAME
override fun isAvailable(): Boolean {
// The PlayServices has to be available
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
return if (resultCode == ConnectionResult.SUCCESS) {
Timber.tag(loggerTag.value).d("Google Play Services is available")
true
} else {
Timber.tag(loggerTag.value).w("Google Play Services is not available")
false
}
return isPlayServiceAvailable.isAvailable()
}
override fun getDistributors(): List<Distributor> {
@@ -73,7 +60,12 @@ class FirebasePushProvider @Inject constructor(
pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.PUSHER_HTTP_URL)
}
override suspend fun troubleshoot(): Result<Unit> {
return firebaseTroubleshooter.troubleshoot()
override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? {
return firebaseStore.getFcmToken()?.let { fcmToken ->
CurrentUserPushConfig(
url = FirebaseConfig.PUSHER_HTTP_URL,
pushKey = fcmToken
)
}
}
}

View File

@@ -18,20 +18,28 @@ package io.element.android.libraries.pushproviders.firebase
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.DefaultPreferences
import javax.inject.Inject
/**
* This class store the Firebase token in SharedPrefs.
*/
class FirebaseStore @Inject constructor(
interface FirebaseStore {
fun getFcmToken(): String?
fun storeFcmToken(token: String?)
}
@ContributesBinding(AppScope::class)
class DefaultFirebaseStore @Inject constructor(
@DefaultPreferences private val sharedPrefs: SharedPreferences,
) {
fun getFcmToken(): String? {
) : FirebaseStore {
override fun getFcmToken(): String? {
return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null)
}
fun storeFcmToken(token: String?) {
override fun storeFcmToken(token: String?) {
sharedPrefs.edit {
putString(PREFS_KEY_FCM_TOKEN, token)
}

View File

@@ -16,25 +16,28 @@
package io.element.android.libraries.pushproviders.firebase
import android.content.Context
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.messaging.FirebaseMessaging
import io.element.android.libraries.di.ApplicationContext
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
interface FirebaseTroubleshooter {
suspend fun troubleshoot(): Result<Unit>
}
/**
* This class force retrieving and storage of the Firebase token.
*/
class FirebaseTroubleshooter @Inject constructor(
@ApplicationContext private val context: Context,
@ContributesBinding(AppScope::class)
class DefaultFirebaseTroubleshooter @Inject constructor(
private val newTokenHandler: FirebaseNewTokenHandler,
) {
suspend fun troubleshoot(): Result<Unit> {
private val isPlayServiceAvailable: IsPlayServiceAvailable,
) : FirebaseTroubleshooter {
override suspend fun troubleshoot(): Result<Unit> {
return runCatching {
val token = retrievedFirebaseToken()
newTokenHandler.handle(token)
@@ -44,7 +47,7 @@ class FirebaseTroubleshooter @Inject constructor(
private suspend fun retrievedFirebaseToken(): String {
return suspendCoroutine { continuation ->
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(context)) {
if (isPlayServiceAvailable.isAvailable()) {
try {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token ->
@@ -65,15 +68,4 @@ class FirebaseTroubleshooter @Inject constructor(
}
}
}
/**
* Check the device to make sure it has the Google Play Services APK. If
* it doesn't, display a dialog that allows users to download the APK from
* the Google Play Store or enable it in the device's system settings.
*/
private fun checkPlayServices(context: Context): Boolean {
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
return resultCode == ConnectionResult.SUCCESS
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 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.pushproviders.firebase
import android.content.Context
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
interface IsPlayServiceAvailable {
fun isAvailable(): Boolean
}
@ContributesBinding(AppScope::class)
class DefaultIsPlayServiceAvailable @Inject constructor(
@ApplicationContext private val context: Context,
) : IsPlayServiceAvailable {
override fun isAvailable(): Boolean {
val apiAvailability = GoogleApiAvailability.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
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2024 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.pushproviders.firebase.troubleshoot
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.core.notifications.NotificationTroubleshootTest
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestDelegate
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestState
import io.element.android.libraries.core.notifications.TestFilterData
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.pushproviders.firebase.IsPlayServiceAvailable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class FirebaseAvailabilityTest @Inject constructor(
private val isPlayServiceAvailable: IsPlayServiceAvailable,
) : NotificationTroubleshootTest {
override val order = 300
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = "Check Firebase",
defaultDescription = "Ensure that Firebase is available.",
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 = "Firebase is available",
status = NotificationTroubleshootTestState.Status.Success
)
} else {
delegate.updateState(
description = "Firebase is not available",
status = NotificationTroubleshootTestState.Status.Failure(false)
)
}
}
override fun reset() = delegate.reset()
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2024 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.pushproviders.firebase.troubleshoot
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.core.notifications.NotificationTroubleshootTest
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestDelegate
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestState
import io.element.android.libraries.core.notifications.TestFilterData
import io.element.android.libraries.di.AppScope
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class FirebaseTokenTest @Inject constructor(
private val firebaseStore: FirebaseStore,
private val firebaseTroubleshooter: FirebaseTroubleshooter,
) : NotificationTroubleshootTest {
override val order = 310
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = "Check Firebase token",
defaultDescription = "Ensure that Firebase token is available.",
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 token = firebaseStore.getFcmToken()
if (token != null) {
delegate.updateState(
description = "Firebase token: ${token.take(8)}*****",
status = NotificationTroubleshootTestState.Status.Success
)
} else {
delegate.updateState(
description = "Firebase token is not known",
status = NotificationTroubleshootTestState.Status.Failure(true)
)
}
}
override fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) {
delegate.start()
firebaseTroubleshooter.troubleshoot()
run(coroutineScope)
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 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.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()
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 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.pushproviders.firebase
class InMemoryFirebaseStore(
private var token: String? = null
) : FirebaseStore {
override fun getFcmToken(): String? = token
override fun storeFcmToken(token: String?) {
this.token = token
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 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.pushproviders.firebase.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestState
import io.element.android.libraries.pushproviders.firebase.IsPlayServiceAvailable
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class FirebaseAvailabilityTestTest {
@Test
fun `test FirebaseAvailabilityTest success`() = runTest {
val sut = FirebaseAvailabilityTest(
isPlayServiceAvailable = object : IsPlayServiceAvailable {
override fun isAvailable(): Boolean {
return true
}
}
)
launch {
sut.run(this)
}
sut.state.test {
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 = object : IsPlayServiceAvailable {
override fun isAvailable(): Boolean {
return false
}
}
)
launch {
sut.run(this)
}
sut.state.test {
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(false))
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (c) 2024 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.pushproviders.firebase.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestState
import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter
import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore
import kotlinx.coroutines.launch
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(),
)
launch {
sut.run(this)
}
sut.state.test {
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.take(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)
}
),
)
launch {
sut.run(this)
}
sut.state.test {
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(true))
// Quick fix
sut.quickFix(this)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
companion object {
private const val FAKE_TOKEN = "abcdefghijk"
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 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.
*/
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)
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2024 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.pushproviders.test
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
class FakePushProvider(
override val index: Int = 0,
override val name: String = "aFakePushProvider",
private val isAvailable: Boolean = true,
private val distributors: List<Distributor> = emptyList()
) : PushProvider {
override fun isAvailable(): Boolean = isAvailable
override fun getDistributors(): List<Distributor> = distributors
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) {
// No-op
}
override suspend fun unregister(matrixClient: MatrixClient) {
// No-op
}
override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? {
return null
}
}

View File

@@ -37,6 +37,7 @@ dependencies {
implementation(projects.libraries.pushproviders.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api)
implementation(projects.libraries.network)
@@ -50,8 +51,10 @@ dependencies {
// UnifiedPush library
api(libs.unifiedpush)
testImplementation(libs.coroutines.test)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 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.pushproviders.unifiedpush
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.getApplicationLabel
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.pushproviders.api.Distributor
import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject
interface UnifiedPushDistributorProvider {
fun getDistributors(): List<Distributor>
}
@ContributesBinding(AppScope::class)
class DefaultUnifiedPushDistributorProvider @Inject constructor(
@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))
}
}
}
}

View File

@@ -16,17 +16,16 @@
package io.element.android.libraries.pushproviders.unifiedpush
import android.content.Context
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.androidutils.system.getApplicationLabel
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
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
import org.unifiedpush.android.connector.UnifiedPush
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.currentSessionId
import timber.log.Timber
import javax.inject.Inject
@@ -34,10 +33,12 @@ private val loggerTag = LoggerTag("UnifiedPushProvider", LoggerTag.PushLoggerTag
@ContributesMultibinding(AppScope::class)
class UnifiedPushProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
private val pushClientSecret: PushClientSecret,
private val unifiedPushStore: UnifiedPushStore,
private val appNavigationStateService: AppNavigationStateService,
) : PushProvider {
override val index = UnifiedPushConfig.INDEX
override val name = UnifiedPushConfig.NAME
@@ -54,15 +55,7 @@ class UnifiedPushProvider @Inject constructor(
}
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))
}
}
return unifiedPushDistributorProvider.getDistributors()
}
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) {
@@ -75,7 +68,14 @@ class UnifiedPushProvider @Inject constructor(
unRegisterUnifiedPushUseCase.execute(clientSecret)
}
override suspend fun troubleshoot(): Result<Unit> {
TODO("Not yet implemented")
override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? {
val currentSession = appNavigationStateService.appNavigationState.value.navigationState.currentSessionId() ?: return null
val clientSecret = pushClientSecret.getSecretForUser(currentSession)
val url = unifiedPushStore.getPushGateway(clientSecret) ?: return null
val pushKey = unifiedPushStore.getEndpoint(clientSecret) ?: return null
return CurrentUserPushConfig(
url = url,
pushKey = pushKey,
)
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 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.pushproviders.unifiedpush.troubleshoot
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import javax.inject.Inject
interface OpenDistributorWebPageAction {
fun execute()
}
@ContributesBinding(AppScope::class)
class DefaultOpenDistributorWebPageAction @Inject constructor(
@ApplicationContext private val context: Context,
) : OpenDistributorWebPageAction {
override fun execute() {
// Open the distributor download page
context.openUrlInExternalApp(
url = "https://unifiedpush.org/users/distributors/",
inNewTask = true
)
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) 2024 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.pushproviders.unifiedpush.troubleshoot
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.core.notifications.NotificationTroubleshootTest
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestDelegate
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestState
import io.element.android.libraries.core.notifications.TestFilterData
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class UnifiedPushTest @Inject constructor(
private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider,
private val openDistributorWebPageAction: OpenDistributorWebPageAction,
) : NotificationTroubleshootTest {
override val order = 400
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = "Check UnifiedPush",
defaultDescription = "Ensure that UnifiedPush distributors are available.",
visibleWhenIdle = false,
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override fun isRelevant(data: TestFilterData): Boolean {
return data.currentPushProviderName == UnifiedPushConfig.NAME
}
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val distributors = unifiedPushDistributorProvider.getDistributors()
if (distributors.isNotEmpty()) {
delegate.updateState(
description = "Distributors found: ${distributors.joinToString { it.name }}",
status = NotificationTroubleshootTestState.Status.Success
)
} else {
delegate.updateState(
description = "No push distributors found",
status = NotificationTroubleshootTestState.Status.Failure(true)
)
}
}
override fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) {
openDistributorWebPageAction.execute()
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 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.pushproviders.unifiedpush.troubleshoot
class FakeOpenDistributorWebPageAction(
private val executeAction: () -> Unit = {}
) : OpenDistributorWebPageAction {
override fun execute() = executeAction()
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 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.pushproviders.unifiedpush.troubleshoot
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider
class FakeUnifiedPushDistributorProvider(
private var getDistributorsResult: List<Distributor> = emptyList()
) : UnifiedPushDistributorProvider {
override fun getDistributors(): List<Distributor> {
return getDistributorsResult
}
fun setDistributorsResult(list: List<Distributor>) {
getDistributorsResult = list
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (c) 2024 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.pushproviders.unifiedpush.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.notifications.NotificationTroubleshootTestState
import io.element.android.libraries.pushproviders.api.Distributor
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class UnifiedPushTestTest {
@Test
fun `test UnifiedPushTest success`() = runTest {
val sut = UnifiedPushTest(
unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(
getDistributorsResult = listOf(
Distributor("value", "Name"),
)
),
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(),
)
launch {
sut.run(this)
}
sut.state.test {
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 UnifiedPushTest error`() = runTest {
val providers = FakeUnifiedPushDistributorProvider()
val sut = UnifiedPushTest(
unifiedPushDistributorProvider = providers,
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(
executeAction = {
providers.setDistributorsResult(
listOf(
Distributor("value", "Name"),
)
)
}
),
)
launch {
sut.run(this)
}
sut.state.test {
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(true))
// Quick fix
launch {
sut.quickFix(this)
sut.run(this)
}
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
}