Merge pull request #3035 from element-hq/feature/bma/fixFdroidNotification
Feature/bma/fix fdroid notification
This commit is contained in:
commit
7b5e7c4c00
43 changed files with 1060 additions and 106 deletions
|
|
@ -67,6 +67,7 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.features.login.impl)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
|
|
|||
|
|
@ -238,7 +238,12 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
return when (navTarget) {
|
||||
NavTarget.Placeholder -> createNode<PlaceholderNode>(buildContext)
|
||||
NavTarget.LoggedInPermanent -> {
|
||||
createNode<LoggedInNode>(buildContext)
|
||||
val callback = object : LoggedInNode.Callback {
|
||||
override fun navigateToNotificationTroubleshoot() {
|
||||
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationTroubleshoot))
|
||||
}
|
||||
}
|
||||
createNode<LoggedInNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.RoomList -> {
|
||||
val callback = object : RoomListEntryPoint.Callback {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
sealed interface LoggedInEvents {
|
||||
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
|
|
@ -35,11 +36,22 @@ class LoggedInNode @AssistedInject constructor(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
interface Callback : Plugin {
|
||||
fun navigateToNotificationTroubleshoot()
|
||||
}
|
||||
|
||||
private fun navigateToNotificationTroubleshoot() {
|
||||
plugins<Callback>().forEach {
|
||||
it.navigateToNotificationTroubleshoot()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val loggedInState = loggedInPresenter.present()
|
||||
LoggedInView(
|
||||
state = loggedInState,
|
||||
navigateToNotificationTroubleshoot = ::navigateToNotificationTroubleshoot,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,20 @@ package io.element.android.appnav.loggedin
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
|
|
@ -34,11 +39,17 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.pushproviders.api.RegistrationFailure
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val pusherTag = LoggerTag("Pusher", LoggerTag.PushLoggerTag)
|
||||
|
||||
class LoggedInPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
|
|
@ -49,36 +60,26 @@ class LoggedInPresenter @Inject constructor(
|
|||
) : Presenter<LoggedInState> {
|
||||
@Composable
|
||||
override fun present(): LoggedInState {
|
||||
val isVerified by remember {
|
||||
sessionVerificationService.sessionVerifiedStatus.map { it == SessionVerifiedStatus.Verified }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val ignoreRegistrationError by remember {
|
||||
pushService.ignoreRegistrationError(matrixClient.sessionId)
|
||||
}.collectAsState(initial = false)
|
||||
|
||||
LaunchedEffect(isVerified) {
|
||||
if (isVerified) {
|
||||
// Ensure pusher is registered
|
||||
val currentPushProvider = pushService.getCurrentPushProvider()
|
||||
val result = if (currentPushProvider == null) {
|
||||
// Register with the first available push provider
|
||||
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
||||
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||
} else {
|
||||
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient)
|
||||
if (currentPushDistributor == null) {
|
||||
// Register with the first available distributor
|
||||
val distributor = currentPushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||
pushService.registerWith(matrixClient, currentPushProvider, distributor)
|
||||
} else {
|
||||
// Re-register with the current distributor
|
||||
pushService.registerWith(matrixClient, currentPushProvider, currentPushDistributor)
|
||||
val pusherRegistrationState = remember<MutableState<AsyncData<Unit>>> { mutableStateOf(AsyncData.Uninitialized) }
|
||||
LaunchedEffect(Unit) {
|
||||
sessionVerificationService.sessionVerifiedStatus
|
||||
.onEach { sessionVerifiedStatus ->
|
||||
when (sessionVerifiedStatus) {
|
||||
SessionVerifiedStatus.Unknown -> Unit
|
||||
SessionVerifiedStatus.Verified -> {
|
||||
ensurePusherIsRegistered(pusherRegistrationState)
|
||||
}
|
||||
SessionVerifiedStatus.NotVerified -> {
|
||||
pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.AccountNotVerified())
|
||||
}
|
||||
}
|
||||
}
|
||||
result.onFailure {
|
||||
Timber.e(it, "Failed to register pusher")
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
||||
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||
val showSyncSpinner by remember {
|
||||
|
|
@ -86,14 +87,86 @@ class LoggedInPresenter @Inject constructor(
|
|||
networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show
|
||||
}
|
||||
}
|
||||
val verificationState by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||
LaunchedEffect(verificationState, recoveryState) {
|
||||
reportCryptoStatusToAnalytics(verificationState, recoveryState)
|
||||
LaunchedEffect(Unit) {
|
||||
combine(
|
||||
sessionVerificationService.sessionVerifiedStatus,
|
||||
encryptionService.recoveryStateStateFlow
|
||||
) { verificationState, recoveryState ->
|
||||
reportCryptoStatusToAnalytics(verificationState, recoveryState)
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
fun handleEvent(event: LoggedInEvents) {
|
||||
when (event) {
|
||||
is LoggedInEvents.CloseErrorDialog -> {
|
||||
pusherRegistrationState.value = AsyncData.Uninitialized
|
||||
if (event.doNotShowAgain) {
|
||||
coroutineScope.launch {
|
||||
pushService.setIgnoreRegistrationError(matrixClient.sessionId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LoggedInState(
|
||||
showSyncSpinner = showSyncSpinner,
|
||||
pusherRegistrationState = pusherRegistrationState.value,
|
||||
ignoreRegistrationError = ignoreRegistrationError,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun ensurePusherIsRegistered(pusherRegistrationState: MutableState<AsyncData<Unit>>) {
|
||||
Timber.tag(pusherTag.value).d("Ensure pusher is registered")
|
||||
val currentPushProvider = pushService.getCurrentPushProvider()
|
||||
val result = if (currentPushProvider == null) {
|
||||
Timber.tag(pusherTag.value).d("Register with the first available push provider with at least one distributor")
|
||||
val pushProvider = pushService.getAvailablePushProviders()
|
||||
.firstOrNull { it.getDistributors().isNotEmpty() }
|
||||
// Else fallback to the first available push provider (the list should never be empty)
|
||||
?: pushService.getAvailablePushProviders().firstOrNull()
|
||||
?: return Unit
|
||||
.also { Timber.tag(pusherTag.value).w("No push providers available") }
|
||||
.also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoProvidersAvailable()) }
|
||||
val distributor = pushProvider.getDistributors().firstOrNull()
|
||||
?: return Unit
|
||||
.also { Timber.tag(pusherTag.value).w("No distributors available") }
|
||||
.also {
|
||||
// In this case, consider the push provider is chosen.
|
||||
pushService.selectPushProvider(matrixClient, pushProvider)
|
||||
}
|
||||
.also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
|
||||
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||
} else {
|
||||
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient)
|
||||
if (currentPushDistributor == null) {
|
||||
Timber.tag(pusherTag.value).d("Register with the first available distributor")
|
||||
val distributor = currentPushProvider.getDistributors().firstOrNull()
|
||||
?: return Unit
|
||||
.also { Timber.tag(pusherTag.value).w("No distributors available") }
|
||||
.also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
|
||||
pushService.registerWith(matrixClient, currentPushProvider, distributor)
|
||||
} else {
|
||||
Timber.tag(pusherTag.value).d("Re-register with the current distributor")
|
||||
pushService.registerWith(matrixClient, currentPushProvider, currentPushDistributor)
|
||||
}
|
||||
}
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
Timber.tag(pusherTag.value).d("Pusher registered")
|
||||
pusherRegistrationState.value = AsyncData.Success(Unit)
|
||||
},
|
||||
onFailure = {
|
||||
Timber.tag(pusherTag.value).e(it, "Failed to register pusher")
|
||||
if (it is RegistrationFailure) {
|
||||
pusherRegistrationState.value = AsyncData.Failure(
|
||||
PusherRegistrationFailure.RegistrationFailure(it.clientException, it.isRegisteringAgain)
|
||||
)
|
||||
} else {
|
||||
pusherRegistrationState.value = AsyncData.Failure(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class LoggedInState(
|
||||
val showSyncSpinner: Boolean,
|
||||
val pusherRegistrationState: AsyncData<Unit>,
|
||||
val ignoreRegistrationError: Boolean,
|
||||
val eventSink: (LoggedInEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,18 +17,23 @@
|
|||
package io.element.android.appnav.loggedin
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||
override val values: Sequence<LoggedInState>
|
||||
get() = sequenceOf(
|
||||
aLoggedInState(false),
|
||||
aLoggedInState(true),
|
||||
// Add other state here
|
||||
aLoggedInState(),
|
||||
aLoggedInState(showSyncSpinner = true),
|
||||
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLoggedInState(
|
||||
showSyncSpinner: Boolean = true,
|
||||
showSyncSpinner: Boolean = false,
|
||||
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||
) = LoggedInState(
|
||||
showSyncSpinner = showSyncSpinner,
|
||||
pusherRegistrationState = pusherRegistrationState,
|
||||
ignoreRegistrationError = false,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,19 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun LoggedInView(
|
||||
state: LoggedInState,
|
||||
navigateToNotificationTroubleshoot: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
|
|
@ -41,12 +47,53 @@ fun LoggedInView(
|
|||
isVisible = state.showSyncSpinner,
|
||||
)
|
||||
}
|
||||
when (state.pusherRegistrationState) {
|
||||
is AsyncData.Uninitialized,
|
||||
is AsyncData.Loading,
|
||||
is AsyncData.Success -> Unit
|
||||
is AsyncData.Failure -> {
|
||||
state.pusherRegistrationState.errorOrNull()
|
||||
?.takeIf { !state.ignoreRegistrationError }
|
||||
?.getReason()
|
||||
?.let { reason ->
|
||||
ErrorDialogWithDoNotShowAgain(
|
||||
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
|
||||
cancelText = stringResource(id = CommonStrings.common_settings),
|
||||
onDismiss = {
|
||||
state.eventSink(LoggedInEvents.CloseErrorDialog(it))
|
||||
},
|
||||
onCancel = {
|
||||
state.eventSink(LoggedInEvents.CloseErrorDialog(false))
|
||||
navigateToNotificationTroubleshoot()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.getReason(): String? {
|
||||
return when (this) {
|
||||
is PusherRegistrationFailure.RegistrationFailure -> {
|
||||
if (isRegisteringAgain && clientException.isNetworkError()) {
|
||||
// When registering again, ignore network error
|
||||
null
|
||||
} else {
|
||||
clientException.message ?: "Unknown error"
|
||||
}
|
||||
}
|
||||
is PusherRegistrationFailure.AccountNotVerified -> null
|
||||
is PusherRegistrationFailure.NoDistributorsAvailable -> "No distributors available"
|
||||
is PusherRegistrationFailure.NoProvidersAvailable -> "No providers available"
|
||||
else -> "Other error"
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
|
||||
LoggedInView(
|
||||
state = state
|
||||
state = state,
|
||||
navigateToNotificationTroubleshoot = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.appnav.loggedin
|
||||
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
|
||||
sealed class PusherRegistrationFailure : Exception() {
|
||||
class AccountNotVerified : PusherRegistrationFailure()
|
||||
class NoProvidersAvailable : PusherRegistrationFailure()
|
||||
class NoDistributorsAvailable : PusherRegistrationFailure()
|
||||
|
||||
/**
|
||||
* @param clientException the failure that occurred.
|
||||
* @param isRegisteringAgain true if the server should already have a the same pusher registered.
|
||||
*/
|
||||
class RegistrationFailure(
|
||||
val clientException: ClientException,
|
||||
val isRegisteringAgain: Boolean,
|
||||
) : PusherRegistrationFailure()
|
||||
}
|
||||
|
|
@ -18,23 +18,39 @@ package io.element.android.appnav.loggedin
|
|||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.libraries.pushproviders.test.FakePushProvider
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
@ -51,6 +67,8 @@ class LoggedInPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showSyncSpinner).isFalse()
|
||||
assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue()
|
||||
assertThat(initialState.ignoreRegistrationError).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,16 +108,12 @@ class LoggedInPresenterTest {
|
|||
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
|
||||
skipItems(4)
|
||||
|
||||
skipItems(2)
|
||||
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||
assertThat(analyticsService.capturedEvents[0]).isInstanceOf(CryptoSessionStateChange::class.java)
|
||||
|
||||
assertThat(analyticsService.capturedUserProperties.size).isEqualTo(1)
|
||||
assertThat(analyticsService.capturedUserProperties[0].recoveryState).isEqualTo(UserProperties.RecoveryState.Incomplete)
|
||||
assertThat(analyticsService.capturedUserProperties[0].verificationState).isEqualTo(UserProperties.VerificationState.Verified)
|
||||
|
||||
// ensure a sync status change does not trigger a new capture
|
||||
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
|
||||
skipItems(1)
|
||||
|
|
@ -107,17 +121,399 @@ class LoggedInPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensure default pusher is not registered if session is not verified`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val pushService = createFakePushService(registerWithLambda = lambda)
|
||||
val verificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = verificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.errorOrNull())
|
||||
.isInstanceOf(PusherRegistrationFailure.AccountNotVerified::class.java)
|
||||
lambda.assertions()
|
||||
.isNeverCalled()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensure default pusher is registered with default provider`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val sessionVerificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
|
||||
)
|
||||
val pushService = createFakePushService(
|
||||
registerWithLambda = lambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
// MatrixClient
|
||||
any(),
|
||||
// PushProvider with highest priority (lower index)
|
||||
value(pushService.getAvailablePushProviders()[0]),
|
||||
// First distributor
|
||||
value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensure default pusher is registered with default provider - fail to register`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
val sessionVerificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
|
||||
)
|
||||
val pushService = createFakePushService(
|
||||
registerWithLambda = lambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.isFailure()).isTrue()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
// MatrixClient
|
||||
any(),
|
||||
// PushProvider with highest priority (lower index)
|
||||
value(pushService.getAvailablePushProviders()[0]),
|
||||
// First distributor
|
||||
value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensure current provider is registered with current distributor`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val sessionVerificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
|
||||
)
|
||||
val distributor = Distributor("aDistributorValue1", "aDistributorName1")
|
||||
val pushProvider = FakePushProvider(
|
||||
index = 0,
|
||||
name = "aFakePushProvider0",
|
||||
distributors = listOf(
|
||||
Distributor("aDistributorValue0", "aDistributorName0"),
|
||||
distributor,
|
||||
),
|
||||
currentDistributor = { distributor },
|
||||
)
|
||||
val pushService = createFakePushService(
|
||||
pushProvider1 = pushProvider,
|
||||
currentPushProvider = { pushProvider },
|
||||
registerWithLambda = lambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
// MatrixClient
|
||||
any(),
|
||||
// Current push provider
|
||||
value(pushProvider),
|
||||
// Current distributor
|
||||
value(distributor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - if current push provider does not have current distributor, the first one is used`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val sessionVerificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
|
||||
)
|
||||
val pushProvider = FakePushProvider(
|
||||
index = 0,
|
||||
name = "aFakePushProvider0",
|
||||
distributors = listOf(
|
||||
Distributor("aDistributorValue0", "aDistributorName0"),
|
||||
Distributor("aDistributorValue1", "aDistributorName1"),
|
||||
),
|
||||
currentDistributor = { null },
|
||||
)
|
||||
val pushService = createFakePushService(
|
||||
pushProvider0 = pushProvider,
|
||||
currentPushProvider = { pushProvider },
|
||||
registerWithLambda = lambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
// MatrixClient
|
||||
any(),
|
||||
// PushProvider with highest priority (lower index)
|
||||
value(pushService.getAvailablePushProviders()[0]),
|
||||
// First distributor
|
||||
value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - if current push provider does not have distributors, nothing happen`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val sessionVerificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
|
||||
)
|
||||
val pushProvider = FakePushProvider(
|
||||
index = 0,
|
||||
name = "aFakePushProvider0",
|
||||
distributors = emptyList(),
|
||||
)
|
||||
val pushService = createFakePushService(
|
||||
pushProvider0 = pushProvider,
|
||||
currentPushProvider = { pushProvider },
|
||||
registerWithLambda = lambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.errorOrNull())
|
||||
.isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
|
||||
lambda.assertions()
|
||||
.isNeverCalled()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - case no push provider available provider`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val sessionVerificationService = FakeSessionVerificationService(SessionVerifiedStatus.Verified)
|
||||
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
||||
val pushService = createFakePushService(
|
||||
pushProvider0 = null,
|
||||
pushProvider1 = null,
|
||||
registerWithLambda = lambda,
|
||||
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.errorOrNull())
|
||||
.isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java)
|
||||
lambda.assertions()
|
||||
.isNeverCalled()
|
||||
// Reset the error and do not show again
|
||||
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = true))
|
||||
skipItems(1)
|
||||
setIgnoreRegistrationErrorLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
// SessionId
|
||||
value(A_SESSION_ID),
|
||||
// Ignore
|
||||
value(true),
|
||||
)
|
||||
val lastState = awaitItem()
|
||||
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
|
||||
assertThat(lastState.ignoreRegistrationError).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - case one push provider but no distributor available`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val selectPushProviderLambda = lambdaRecorder<MatrixClient, PushProvider, Unit> { _, _ -> }
|
||||
val sessionVerificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
|
||||
)
|
||||
val pushProvider = FakePushProvider(
|
||||
index = 0,
|
||||
name = "aFakePushProvider",
|
||||
distributors = emptyList(),
|
||||
)
|
||||
val pushService = createFakePushService(
|
||||
pushProvider0 = pushProvider,
|
||||
pushProvider1 = null,
|
||||
registerWithLambda = lambda,
|
||||
selectPushProviderLambda = selectPushProviderLambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.errorOrNull())
|
||||
.isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
|
||||
lambda.assertions()
|
||||
.isNeverCalled()
|
||||
selectPushProviderLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
// MatrixClient
|
||||
any(),
|
||||
// PushProvider
|
||||
value(pushProvider),
|
||||
)
|
||||
// Reset the error
|
||||
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
|
||||
val lastState = awaitItem()
|
||||
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - case two push providers but first one does not have distributor - second one will be used`() = runTest {
|
||||
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val sessionVerificationService = FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
|
||||
)
|
||||
val pushProvider0 = FakePushProvider(
|
||||
index = 0,
|
||||
name = "aFakePushProvider0",
|
||||
distributors = emptyList(),
|
||||
)
|
||||
val distributor = Distributor("aDistributorValue1", "aDistributorName1")
|
||||
val pushProvider1 = FakePushProvider(
|
||||
index = 1,
|
||||
name = "aFakePushProvider1",
|
||||
distributors = listOf(distributor),
|
||||
)
|
||||
val pushService = createFakePushService(
|
||||
pushProvider0 = pushProvider0,
|
||||
pushProvider1 = pushProvider1,
|
||||
registerWithLambda = lambda,
|
||||
)
|
||||
val presenter = createLoggedInPresenter(
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val finalState = awaitFirstItem()
|
||||
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
|
||||
lambda.assertions().isCalledOnce()
|
||||
.with(
|
||||
// MatrixClient
|
||||
any(),
|
||||
// PushProvider with the distributor
|
||||
value(pushProvider1),
|
||||
// First distributor of second push provider
|
||||
value(distributor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFakePushService(
|
||||
pushProvider0: PushProvider? = FakePushProvider(
|
||||
index = 0,
|
||||
name = "aFakePushProvider0",
|
||||
distributors = listOf(Distributor("aDistributorValue0", "aDistributorName0")),
|
||||
currentDistributor = { null },
|
||||
),
|
||||
pushProvider1: PushProvider? = FakePushProvider(
|
||||
index = 1,
|
||||
name = "aFakePushProvider1",
|
||||
distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")),
|
||||
currentDistributor = { null },
|
||||
),
|
||||
registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
},
|
||||
selectPushProviderLambda: (MatrixClient, PushProvider) -> Unit = { _, _ -> lambdaError() },
|
||||
currentPushProvider: () -> PushProvider? = { null },
|
||||
setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
): PushService {
|
||||
return FakePushService(
|
||||
availablePushProviders = listOfNotNull(pushProvider0, pushProvider1),
|
||||
registerWithLambda = registerWithLambda,
|
||||
currentPushProvider = currentPushProvider,
|
||||
selectPushProviderLambda = selectPushProviderLambda,
|
||||
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createLoggedInPresenter(
|
||||
roomListService: RoomListService = FakeRoomListService(),
|
||||
networkStatus: NetworkStatus = NetworkStatus.Offline,
|
||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
pushService: PushService = FakePushService(),
|
||||
): LoggedInPresenter {
|
||||
return LoggedInPresenter(
|
||||
matrixClient = FakeMatrixClient(roomListService = roomListService),
|
||||
networkMonitor = FakeNetworkMonitor(networkStatus),
|
||||
pushService = FakePushService(),
|
||||
sessionVerificationService = FakeSessionVerificationService(),
|
||||
pushService = pushService,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
analyticsService = analyticsService,
|
||||
encryptionService = encryptionService
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
|||
|
||||
@Parcelize
|
||||
data object NotificationSettings : InitialTarget
|
||||
|
||||
@Parcelize
|
||||
data object NotificationTroubleshoot : InitialTarget
|
||||
}
|
||||
|
||||
data class Params(val initialElement: InitialTarget) : NodeInputs
|
||||
|
|
|
|||
|
|
@ -51,4 +51,5 @@ class DefaultPreferencesEntryPoint @Inject constructor() : PreferencesEntryPoint
|
|||
internal fun PreferencesEntryPoint.InitialTarget.toNavTarget() = when (this) {
|
||||
is PreferencesEntryPoint.InitialTarget.Root -> PreferencesFlowNode.NavTarget.Root
|
||||
is PreferencesEntryPoint.InitialTarget.NotificationSettings -> PreferencesFlowNode.NavTarget.NotificationSettings
|
||||
PreferencesEntryPoint.InitialTarget.NotificationTroubleshoot -> PreferencesFlowNode.NavTarget.TroubleshootNotifications
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import io.element.android.features.preferences.impl.root.PreferencesRootNode
|
|||
import io.element.android.features.preferences.impl.user.editprofile.EditUserProfileNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.appyx.canPop
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -190,7 +191,11 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
notificationTroubleShootEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(object : NotificationTroubleShootEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
if (backstack.canPop()) {
|
||||
backstack.pop()
|
||||
} else {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
|||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
|
|
@ -47,6 +48,7 @@ class DefaultClearCacheUseCase @Inject constructor(
|
|||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
private val ftueService: FtueService,
|
||||
private val migrationScreenStore: MigrationScreenStore,
|
||||
private val pushService: PushService,
|
||||
) : ClearCacheUseCase {
|
||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||
// Clear Matrix cache
|
||||
|
|
@ -64,6 +66,8 @@ class DefaultClearCacheUseCase @Inject constructor(
|
|||
ftueService.reset()
|
||||
// Clear migration screen store
|
||||
migrationScreenStore.reset()
|
||||
// Ensure any error will be displayed again
|
||||
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
|
||||
// Ensure the app is restarted
|
||||
defaultCacheService.onClearedCache(matrixClient.sessionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -329,13 +329,11 @@ class NotificationSettingsPresenterTest {
|
|||
val pushProvider1 = FakePushProvider(
|
||||
index = 0,
|
||||
name = "aFakePushProvider0",
|
||||
isAvailable = true,
|
||||
distributors = listOf(Distributor("aDistributorValue0", "aDistributorName0")),
|
||||
)
|
||||
val pushProvider2 = FakePushProvider(
|
||||
index = 1,
|
||||
name = "aFakePushProvider1",
|
||||
isAvailable = true,
|
||||
distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")),
|
||||
)
|
||||
return FakePushService(
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.ftue.test.FakeFtueService
|
||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
@ -48,6 +51,10 @@ class DefaultClearCacheUseCaseTest {
|
|||
val migrationScreenStore = InMemoryMigrationScreenStore(
|
||||
resetLambda = resetMigrationLambda,
|
||||
)
|
||||
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
||||
val pushService = FakePushService(
|
||||
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda
|
||||
)
|
||||
val sut = DefaultClearCacheUseCase(
|
||||
context = InstrumentationRegistry.getInstrumentation().context,
|
||||
matrixClient = matrixClient,
|
||||
|
|
@ -55,13 +62,16 @@ class DefaultClearCacheUseCaseTest {
|
|||
defaultCacheService = defaultCacheService,
|
||||
okHttpClient = { OkHttpClient.Builder().build() },
|
||||
ftueService = ftueService,
|
||||
migrationScreenStore = migrationScreenStore
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
pushService = pushService,
|
||||
)
|
||||
defaultCacheService.clearedCacheEventFlow.test {
|
||||
sut.invoke()
|
||||
clearCacheLambda.assertions().isCalledOnce()
|
||||
resetFtueLambda.assertions().isCalledOnce()
|
||||
resetMigrationLambda.assertions().isCalledOnce()
|
||||
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
|
||||
.with(value(matrixClient.sessionId), value(false))
|
||||
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.architecture.appyx
|
||||
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
|
||||
fun <T : Any> BackStack<T>.canPop(): Boolean {
|
||||
val elements = elements.value
|
||||
return elements.any { it.targetState == BackStack.State.ACTIVE } &&
|
||||
elements.any { it.targetState == BackStack.State.STASHED }
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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.designsystem.components.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ErrorDialogWithDoNotShowAgain(
|
||||
content: String,
|
||||
onDismiss: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = ErrorDialogDefaults.title,
|
||||
submitText: String = ErrorDialogDefaults.submitText,
|
||||
cancelText: String? = null,
|
||||
onCancel: () -> Unit = {},
|
||||
) {
|
||||
var doNotShowAgain by remember { mutableStateOf(false) }
|
||||
BasicAlertDialog(
|
||||
modifier = modifier,
|
||||
onDismissRequest = { onDismiss(doNotShowAgain) }
|
||||
) {
|
||||
SimpleAlertDialogContent(
|
||||
title = title,
|
||||
submitText = submitText,
|
||||
cancelText = cancelText,
|
||||
onSubmitClick = { onDismiss(doNotShowAgain) },
|
||||
onCancelClick = onCancel,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = content,
|
||||
style = ElementTheme.materialTypography.bodyMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = doNotShowAgain, onCheckedChange = { doNotShowAgain = it })
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.common_do_not_show_this_again),
|
||||
style = ElementTheme.materialTypography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ErrorDialogWithDoNotShowAgainPreview() = ElementPreview {
|
||||
ErrorDialogWithDoNotShowAgain(
|
||||
content = "Content",
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -20,3 +20,7 @@ sealed class ClientException(message: String) : Exception(message) {
|
|||
class Generic(message: String) : ClientException(message)
|
||||
class Other(message: String) : ClientException(message)
|
||||
}
|
||||
|
||||
fun ClientException.isNetworkError(): Boolean {
|
||||
return this is ClientException.Generic && message?.contains("error sending request for url", ignoreCase = true) == true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@
|
|||
package io.element.android.libraries.matrix.impl.pushers
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
||||
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.HttpPusherData
|
||||
|
|
@ -52,6 +54,7 @@ class RustPushersService(
|
|||
lang = setHttpPusherData.lang
|
||||
)
|
||||
}
|
||||
.mapFailure { it.mapClientException() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class FakeSessionVerificationService : SessionVerificationService {
|
||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||
class FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown,
|
||||
) : SessionVerificationService {
|
||||
private val _sessionVerifiedStatus = MutableStateFlow(initialSessionVerifiedStatus)
|
||||
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||
private var _needsSessionVerification = MutableStateFlow(true)
|
||||
var shouldFail = false
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
package io.element.android.libraries.push.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PushService {
|
||||
/**
|
||||
|
|
@ -27,8 +29,7 @@ interface PushService {
|
|||
suspend fun getCurrentPushProvider(): PushProvider?
|
||||
|
||||
/**
|
||||
* Return the list of push providers, available at compile time, and
|
||||
* available at runtime, sorted by index.
|
||||
* Return the list of push providers, available at compile time, sorted by index.
|
||||
*/
|
||||
fun getAvailablePushProviders(): List<PushProvider>
|
||||
|
||||
|
|
@ -43,6 +44,18 @@ interface PushService {
|
|||
distributor: Distributor,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Store the given push provider as the current one, but do not register.
|
||||
* To be used when there is no distributor available.
|
||||
*/
|
||||
suspend fun selectPushProvider(
|
||||
matrixClient: MatrixClient,
|
||||
pushProvider: PushProvider,
|
||||
)
|
||||
|
||||
fun ignoreRegistrationError(sessionId: SessionId): Flow<Boolean>
|
||||
suspend fun setIgnoreRegistrationError(sessionId: SessionId, ignore: Boolean)
|
||||
|
||||
/**
|
||||
* Return false in case of early error.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ package io.element.android.libraries.push.impl
|
|||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.impl.test.TestPush
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -42,7 +44,6 @@ class DefaultPushService @Inject constructor(
|
|||
|
||||
override fun getAvailablePushProviders(): List<PushProvider> {
|
||||
return pushProviders
|
||||
.filter { it.isAvailable() }
|
||||
.sortedBy { it.index }
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ class DefaultPushService @Inject constructor(
|
|||
pushProvider: PushProvider,
|
||||
distributor: Distributor,
|
||||
): Result<Unit> {
|
||||
Timber.d("Registering with ${pushProvider.name}/${distributor.name}}")
|
||||
Timber.d("Registering with ${pushProvider.name}/${distributor.name}")
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
|
||||
val currentPushProviderName = userPushStore.getPushProviderName()
|
||||
val currentPushProvider = pushProviders.find { it.name == currentPushProviderName }
|
||||
|
|
@ -72,6 +73,23 @@ class DefaultPushService @Inject constructor(
|
|||
return pushProvider.registerWith(matrixClient, distributor)
|
||||
}
|
||||
|
||||
override suspend fun selectPushProvider(
|
||||
matrixClient: MatrixClient,
|
||||
pushProvider: PushProvider,
|
||||
) {
|
||||
Timber.d("Select ${pushProvider.name}")
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
|
||||
userPushStore.setPushProviderName(pushProvider.name)
|
||||
}
|
||||
|
||||
override fun ignoreRegistrationError(sessionId: SessionId): Flow<Boolean> {
|
||||
return userPushStoreFactory.getOrCreate(sessionId).ignoreRegistrationError()
|
||||
}
|
||||
|
||||
override suspend fun setIgnoreRegistrationError(sessionId: SessionId, ignore: Boolean) {
|
||||
userPushStoreFactory.getOrCreate(sessionId).setIgnoreRegistrationError(ignore)
|
||||
}
|
||||
|
||||
override suspend fun testPush(): Boolean {
|
||||
val pushProvider = getCurrentPushProvider() ?: return false
|
||||
val config = pushProvider.getCurrentUserPushConfig() ?: return false
|
||||
|
|
|
|||
|
|
@ -18,14 +18,17 @@ package io.element.android.libraries.push.impl
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.appconfig.PushConfig
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
|
||||
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import io.element.android.libraries.pushproviders.api.RegistrationFailure
|
||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import timber.log.Timber
|
||||
|
|
@ -50,7 +53,8 @@ class DefaultPusherSubscriber @Inject constructor(
|
|||
gateway: String,
|
||||
): Result<Unit> {
|
||||
val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
|
||||
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) {
|
||||
val isRegisteringAgain = userDataStore.getCurrentRegisteredPushKey() == pushKey
|
||||
if (isRegisteringAgain) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("Unnecessary to register again the same pusher, but do it in case the pusher has been removed from the server")
|
||||
}
|
||||
|
|
@ -61,8 +65,14 @@ class DefaultPusherSubscriber @Inject constructor(
|
|||
.onSuccess {
|
||||
userDataStore.setCurrentRegisteredPushKey(pushKey)
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
.mapFailure { throwable ->
|
||||
Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher")
|
||||
if (throwable is ClientException) {
|
||||
// It should always be the case.
|
||||
RegistrationFailure(throwable, isRegisteringAgain = isRegisteringAgain)
|
||||
} else {
|
||||
throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
||||
import io.element.android.libraries.push.impl.test.FakeTestPush
|
||||
|
|
@ -33,6 +34,7 @@ import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushSto
|
|||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -205,6 +207,20 @@ class DefaultPushServiceTest {
|
|||
assertThat(result).containsExactly(aPushProvider1, aPushProvider2, aPushProvider3).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test setIgnoreRegistrationError is sent to the store`() = runTest {
|
||||
val userPushStore = FakeUserPushStore().apply {
|
||||
}
|
||||
val defaultPushService = createDefaultPushService(
|
||||
userPushStoreFactory = FakeUserPushStoreFactory(
|
||||
userPushStore = { userPushStore },
|
||||
),
|
||||
)
|
||||
assertThat(defaultPushService.ignoreRegistrationError(A_SESSION_ID).first()).isFalse()
|
||||
defaultPushService.setIgnoreRegistrationError(A_SESSION_ID, true)
|
||||
assertThat(defaultPushService.ignoreRegistrationError(A_SESSION_ID).first()).isTrue()
|
||||
}
|
||||
|
||||
private fun createDefaultPushService(
|
||||
testPush: TestPush = FakeTestPush(),
|
||||
userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(),
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@
|
|||
package io.element.android.libraries.push.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakePushService(
|
||||
private val testPushBlock: suspend () -> Boolean = { true },
|
||||
|
|
@ -28,9 +32,12 @@ class FakePushService(
|
|||
private val registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
},
|
||||
private val currentPushProvider: () -> PushProvider? = { availablePushProviders.firstOrNull() },
|
||||
private val selectPushProviderLambda: suspend (MatrixClient, PushProvider) -> Unit = { _, _ -> lambdaError() },
|
||||
private val setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
) : PushService {
|
||||
override suspend fun getCurrentPushProvider(): PushProvider? {
|
||||
return registeredPushProvider ?: availablePushProviders.firstOrNull()
|
||||
return registeredPushProvider ?: currentPushProvider()
|
||||
}
|
||||
|
||||
override fun getAvailablePushProviders(): List<PushProvider> {
|
||||
|
|
@ -52,6 +59,21 @@ class FakePushService(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun selectPushProvider(matrixClient: MatrixClient, pushProvider: PushProvider) {
|
||||
selectPushProviderLambda(matrixClient, pushProvider)
|
||||
}
|
||||
|
||||
private val ignoreRegistrationError = MutableStateFlow(false)
|
||||
|
||||
override fun ignoreRegistrationError(sessionId: SessionId): Flow<Boolean> {
|
||||
return ignoreRegistrationError
|
||||
}
|
||||
|
||||
override suspend fun setIgnoreRegistrationError(sessionId: SessionId, ignore: Boolean) {
|
||||
ignoreRegistrationError.value = ignore
|
||||
setIgnoreRegistrationErrorLambda(sessionId, ignore)
|
||||
}
|
||||
|
||||
override suspend fun testPush(): Boolean = simulateLongTask {
|
||||
testPushBlock()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,10 +33,8 @@ interface PushProvider {
|
|||
val name: String
|
||||
|
||||
/**
|
||||
* Return true if the push provider is available on this device.
|
||||
* Return the list of available distributors.
|
||||
*/
|
||||
fun isAvailable(): Boolean
|
||||
|
||||
fun getDistributors(): List<Distributor>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,8 +17,21 @@
|
|||
package io.element.android.libraries.pushproviders.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
|
||||
interface PusherSubscriber {
|
||||
/**
|
||||
* Register a pusher. Note that failure will be a [RegistrationFailure].
|
||||
*/
|
||||
suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit>
|
||||
|
||||
/**
|
||||
* Unregister a pusher.
|
||||
*/
|
||||
suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit>
|
||||
}
|
||||
|
||||
class RegistrationFailure(
|
||||
val clientException: ClientException,
|
||||
val isRegisteringAgain: Boolean
|
||||
) : Exception(clientException)
|
||||
|
|
|
|||
|
|
@ -38,12 +38,10 @@ class FirebasePushProvider @Inject constructor(
|
|||
override val index = FirebaseConfig.INDEX
|
||||
override val name = FirebaseConfig.NAME
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return isPlayServiceAvailable.isAvailable()
|
||||
}
|
||||
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
return listOf(firebaseDistributor)
|
||||
return listOfNotNull(
|
||||
firebaseDistributor.takeIf { isPlayServiceAvailable.isAvailable() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
|
||||
|
|
|
|||
|
|
@ -38,12 +38,23 @@ class FirebasePushProviderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getDistributors return the unique distributor`() {
|
||||
val firebasePushProvider = createFirebasePushProvider()
|
||||
fun `getDistributors return the unique distributor if available`() {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = true)
|
||||
)
|
||||
val result = firebasePushProvider.getDistributors()
|
||||
assertThat(result).containsExactly(Distributor("Firebase", "Firebase"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDistributors return empty list if service is not available`() {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = false)
|
||||
)
|
||||
val result = firebasePushProvider.getDistributors()
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentDistributor always return the unique distributor`() = runTest {
|
||||
val firebasePushProvider = createFirebasePushProvider()
|
||||
|
|
@ -51,22 +62,6 @@ class FirebasePushProviderTest {
|
|||
assertThat(result).isEqualTo(Distributor("Firebase", "Firebase"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAvailable true`() {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = true)
|
||||
)
|
||||
assertThat(firebasePushProvider.isAvailable()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAvailable false`() {
|
||||
val firebasePushProvider = createFirebasePushProvider(
|
||||
isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = false)
|
||||
)
|
||||
assertThat(firebasePushProvider.isAvailable()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register ok`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
|
|
|
|||
|
|
@ -25,14 +25,12 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
|||
class FakePushProvider(
|
||||
override val index: Int = 0,
|
||||
override val name: String = "aFakePushProvider",
|
||||
private val isAvailable: Boolean = true,
|
||||
private val distributors: List<Distributor> = listOf(Distributor("aDistributorValue", "aDistributorName")),
|
||||
private val currentDistributor: () -> Distributor? = { distributors.firstOrNull() },
|
||||
private val currentUserPushConfig: CurrentUserPushConfig? = null,
|
||||
private val registerWithResult: (MatrixClient, Distributor) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val unregisterWithResult: (MatrixClient) -> Result<Unit> = { lambdaError() },
|
||||
) : PushProvider {
|
||||
override fun isAvailable(): Boolean = isAvailable
|
||||
|
||||
override fun getDistributors(): List<Distributor> = distributors
|
||||
|
||||
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
|
||||
|
|
@ -40,7 +38,7 @@ class FakePushProvider(
|
|||
}
|
||||
|
||||
override suspend fun getCurrentDistributor(matrixClient: MatrixClient): Distributor? {
|
||||
return distributors.firstOrNull()
|
||||
return currentDistributor()
|
||||
}
|
||||
|
||||
override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
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.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
|
||||
|
|
@ -26,11 +25,8 @@ import io.element.android.libraries.pushproviders.api.PushProvider
|
|||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.currentSessionId
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("UnifiedPushProvider", LoggerTag.PushLoggerTag)
|
||||
|
||||
@ContributesMultibinding(AppScope::class)
|
||||
class UnifiedPushProvider @Inject constructor(
|
||||
private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider,
|
||||
|
|
@ -43,17 +39,6 @@ class UnifiedPushProvider @Inject constructor(
|
|||
override val index = UnifiedPushConfig.INDEX
|
||||
override val name = UnifiedPushConfig.NAME
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
val isAvailable = getDistributors().isNotEmpty()
|
||||
return if (isAvailable) {
|
||||
Timber.tag(loggerTag.value).d("UnifiedPush is available")
|
||||
true
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).w("UnifiedPush is not available")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
return unifiedPushDistributorProvider.getDistributors()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ class UnifiedPushProviderTest {
|
|||
)
|
||||
val result = unifiedPushProvider.getDistributors()
|
||||
assertThat(result).containsExactly(Distributor("value", "Name"))
|
||||
assertThat(unifiedPushProvider.isAvailable()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -70,7 +69,6 @@ class UnifiedPushProviderTest {
|
|||
)
|
||||
val result = unifiedPushProvider.getDistributors()
|
||||
assertThat(result).isEmpty()
|
||||
assertThat(unifiedPushProvider.isAvailable()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ interface UserPushStore {
|
|||
fun getNotificationEnabledForDevice(): Flow<Boolean>
|
||||
suspend fun setNotificationEnabledForDevice(enabled: Boolean)
|
||||
|
||||
fun ignoreRegistrationError(): Flow<Boolean>
|
||||
suspend fun setIgnoreRegistrationError(ignore: Boolean)
|
||||
|
||||
/**
|
||||
* Return true if Pin code is disabled, or if user set the settings to see full notification content.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ dependencies {
|
|||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
|||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||
|
|
@ -61,6 +62,7 @@ class UserPushStoreDataStore(
|
|||
private val pushProviderName = stringPreferencesKey("pushProviderName")
|
||||
private val currentPushKey = stringPreferencesKey("currentPushKey")
|
||||
private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
|
||||
private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError")
|
||||
|
||||
override suspend fun getPushProviderName(): String? {
|
||||
return context.dataStore.data.first()[pushProviderName]
|
||||
|
|
@ -100,6 +102,16 @@ class UserPushStoreDataStore(
|
|||
return true
|
||||
}
|
||||
|
||||
override fun ignoreRegistrationError(): Flow<Boolean> {
|
||||
return context.dataStore.data.map { it[ignoreRegistrationError].orFalse() }
|
||||
}
|
||||
|
||||
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
|
||||
context.dataStore.edit {
|
||||
it[ignoreRegistrationError] = ignore
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
context.dataStore.edit {
|
||||
it.clear()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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.pushstore.impl
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class UserPushStoreDataStoreTest {
|
||||
@Test
|
||||
fun `test getPushProviderName`() = runTest {
|
||||
val sut = createUserPushStoreDataStore()
|
||||
assertThat(sut.getPushProviderName()).isNull()
|
||||
sut.setPushProviderName("name")
|
||||
assertThat(sut.getPushProviderName()).isEqualTo("name")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getCurrentRegisteredPushKey`() = runTest {
|
||||
val sut = createUserPushStoreDataStore()
|
||||
assertThat(sut.getCurrentRegisteredPushKey()).isNull()
|
||||
sut.setCurrentRegisteredPushKey("aKey")
|
||||
assertThat(sut.getCurrentRegisteredPushKey()).isEqualTo("aKey")
|
||||
sut.setCurrentRegisteredPushKey(null)
|
||||
assertThat(sut.getCurrentRegisteredPushKey()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getNotificationEnabledForDevice`() = runTest {
|
||||
val sut = createUserPushStoreDataStore()
|
||||
assertThat(sut.getNotificationEnabledForDevice().first()).isTrue()
|
||||
sut.setNotificationEnabledForDevice(false)
|
||||
assertThat(sut.getNotificationEnabledForDevice().first()).isFalse()
|
||||
sut.setNotificationEnabledForDevice(true)
|
||||
assertThat(sut.getNotificationEnabledForDevice().first()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test useCompleteNotificationFormat`() = runTest {
|
||||
val sut = createUserPushStoreDataStore()
|
||||
assertThat(sut.useCompleteNotificationFormat()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ignoreRegistrationError`() = runTest {
|
||||
val sut = createUserPushStoreDataStore()
|
||||
assertThat(sut.ignoreRegistrationError().first()).isFalse()
|
||||
sut.setIgnoreRegistrationError(true)
|
||||
assertThat(sut.ignoreRegistrationError().first()).isTrue()
|
||||
sut.setIgnoreRegistrationError(false)
|
||||
assertThat(sut.ignoreRegistrationError().first()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test reset`() = runTest {
|
||||
val sut = createUserPushStoreDataStore()
|
||||
sut.setPushProviderName("name")
|
||||
sut.setCurrentRegisteredPushKey("aKey")
|
||||
sut.setNotificationEnabledForDevice(false)
|
||||
sut.setIgnoreRegistrationError(true)
|
||||
sut.reset()
|
||||
assertThat(sut.getPushProviderName()).isNull()
|
||||
assertThat(sut.getCurrentRegisteredPushKey()).isNull()
|
||||
assertThat(sut.getNotificationEnabledForDevice().first()).isTrue()
|
||||
assertThat(sut.ignoreRegistrationError().first()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure a store is created per session`() = runTest {
|
||||
val sut1 = createUserPushStoreDataStore()
|
||||
sut1.setPushProviderName("name")
|
||||
val sut2 = createUserPushStoreDataStore(A_SESSION_ID_2)
|
||||
assertThat(sut1.getPushProviderName()).isEqualTo("name")
|
||||
assertThat(sut2.getPushProviderName()).isNull()
|
||||
}
|
||||
|
||||
private fun createUserPushStoreDataStore(
|
||||
sessionId: SessionId = A_SESSION_ID,
|
||||
) = UserPushStoreDataStore(
|
||||
context = InstrumentationRegistry.getInstrumentation().context,
|
||||
userId = sessionId,
|
||||
)
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ class FakeUserPushStore(
|
|||
) : UserPushStore {
|
||||
private var currentRegisteredPushKey: String? = null
|
||||
private val notificationEnabledForDevice = MutableStateFlow(true)
|
||||
private val ignoreRegistrationError = MutableStateFlow(false)
|
||||
override suspend fun getPushProviderName(): String? {
|
||||
return pushProviderName
|
||||
}
|
||||
|
|
@ -53,6 +54,14 @@ class FakeUserPushStore(
|
|||
return true
|
||||
}
|
||||
|
||||
override fun ignoreRegistrationError(): Flow<Boolean> {
|
||||
return ignoreRegistrationError
|
||||
}
|
||||
|
||||
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
|
||||
ignoreRegistrationError.value = ignore
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,12 +129,16 @@
|
|||
<string name="common_decryption_error">"Decryption error"</string>
|
||||
<string name="common_developer_options">"Developer options"</string>
|
||||
<string name="common_direct_chat">"Direct chat"</string>
|
||||
<string name="common_do_not_show_this_again">"Do not show this again"</string>
|
||||
<string name="common_edited_suffix">"(edited)"</string>
|
||||
<string name="common_editing">"Editing"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
||||
<string name="common_enter_your_pin">"Enter your PIN"</string>
|
||||
<string name="common_error">"Error"</string>
|
||||
<string name="common_error_registering_pusher_android">"An error occurred, you may not receive notifications for new messages. Please troubleshoot notifications from the settings.
|
||||
|
||||
Reason: %1$s."</string>
|
||||
<string name="common_everyone">"Everyone"</string>
|
||||
<string name="common_failed">"Failed"</string>
|
||||
<string name="common_favourite">"Favourite"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:92c0b21d5de4540e7b3b784d18531cdf428bd0c9f13bdd445f811d6df73ef50b
|
||||
size 36779
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9613aed2c45f172c7afec90e6205cba7f6112c6108d577c5377e5c3b07720426
|
||||
size 34664
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5aab6b3399b7826b3d1081474c6c0647bcdec933caef6bf563ea3f90d2c99bd1
|
||||
size 13688
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3f232b2d478fbbb30f50333514d8678f9eec114e2119ef64e3429887a12b452b
|
||||
size 12114
|
||||
Loading…
Add table
Add a link
Reference in a new issue