Ensure that disabling (resp. enabling) notification unregisters (resp. registers) the pusher

This commit is contained in:
Benoit Marty
2025-11-13 17:46:49 +01:00
parent 92c02fdd68
commit 13854bb2c7
11 changed files with 441 additions and 355 deletions

View File

@@ -36,7 +36,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
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.libraries.push.api.PusherRegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
@@ -71,7 +71,17 @@ class LoggedInPresenter(
when (sessionVerifiedStatus) {
SessionVerifiedStatus.Unknown -> Unit
SessionVerifiedStatus.Verified -> {
ensurePusherIsRegistered(pusherRegistrationState)
Timber.tag(pusherTag.value).d("Ensure pusher is registered")
pushService.ensurePusherIsRegistered(matrixClient).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")
pusherRegistrationState.value = AsyncData.Failure(it)
},
)
}
SessionVerifiedStatus.NotVerified -> {
pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.AccountNotVerified())
@@ -133,59 +143,6 @@ class LoggedInPresenter(
currentSlidingSyncVersion == SlidingSyncVersion.Proxy
}
private suspend fun ensurePusherIsRegistered(pusherRegistrationState: MutableState<AsyncData<Unit>>) {
Timber.tag(pusherTag.value).d("Ensure pusher is registered")
val currentPushProvider = pushService.getCurrentPushProvider(matrixClient.sessionId)
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.sessionId, pushProvider)
}
.also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
pushService.registerWith(matrixClient, pushProvider, distributor)
} else {
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient.sessionId)
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)
}
}
)
}
private fun reportCryptoStatusToAnalytics(verificationState: SessionVerifiedStatus, recoveryState: RecoveryState) {
// Update first the user property, to store the current status for that posthog user
val userVerificationState = verificationState.toAnalyticsUserPropertyValue()

View File

@@ -10,6 +10,7 @@ package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.push.api.PusherRegistrationFailure
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState>

View File

@@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.matrix.api.exception.isNetworkError
import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.libraries.ui.strings.CommonStrings
@Composable

View File

@@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.libraries.push.test.FakePushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
@@ -42,7 +43,6 @@ 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
@@ -115,7 +115,9 @@ class LoggedInPresenterTest {
encryptionService = encryptionService,
),
syncService = FakeSyncService(initialSyncState = SyncState.Running),
pushService = FakePushService(),
pushService = FakePushService(
ensurePusherIsRegisteredResult = { Result.success(Unit) },
),
sessionVerificationService = verificationService,
analyticsService = analyticsService,
encryptionService = encryptionService,
@@ -139,10 +141,10 @@ 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>> { _, _, _ ->
val lambda = lambdaRecorder<Result<Unit>> {
Result.success(Unit)
}
val pushService = createFakePushService(registerWithLambda = lambda)
val pushService = createFakePushService(ensurePusherIsRegisteredResult = lambda)
val verificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified
)
@@ -153,21 +155,18 @@ class LoggedInPresenterTest {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.AccountNotVerified::class.java)
lambda.assertions()
.isNeverCalled()
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 lambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushService = createFakePushService(
registerWithLambda = lambda,
ensurePusherIsRegisteredResult = lambda,
)
createLoggedInPresenter(
pushService = pushService,
@@ -180,27 +179,17 @@ class LoggedInPresenterTest {
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 lambda = lambdaRecorder<Result<Unit>> { Result.failure(AN_EXCEPTION) }
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushService = createFakePushService(
registerWithLambda = lambda,
ensurePusherIsRegisteredResult = lambda,
)
createLoggedInPresenter(
pushService = pushService,
@@ -213,158 +202,36 @@ class LoggedInPresenterTest {
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]),
)
// Reset the error and do not show again
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
val lastState = awaitItem()
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
assertThat(lastState.ignoreRegistrationError).isFalse()
}
}
@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,
)
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).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,
)
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).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,
)
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
).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)
fun `present - ensure default pusher is registered with default provider - fail to register - do not show again`() = runTest {
val lambda = lambdaRecorder<Result<Unit>> { Result.failure(AN_EXCEPTION) }
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushService = createFakePushService(
pushProvider0 = null,
pushProvider1 = null,
registerWithLambda = lambda,
ensurePusherIsRegisteredResult = lambda,
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,
)
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java)
assertThat(finalState.pusherRegistrationState.isFailure()).isTrue()
lambda.assertions()
.isNeverCalled()
.isCalledOnce()
// Reset the error and do not show again
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = true))
skipItems(1)
@@ -382,95 +249,6 @@ class LoggedInPresenterTest {
}
}
@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<SessionId, 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,
)
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
lambda.assertions()
.isNeverCalled()
selectPushProviderLambda.assertions()
.isCalledOnce()
.with(
// SessionId
value(A_SESSION_ID),
// 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,
)
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).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,
@@ -484,7 +262,7 @@ class LoggedInPresenterTest {
distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")),
currentDistributor = { null },
),
registerWithLambda: (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
ensurePusherIsRegisteredResult: () -> Result<Unit> = {
Result.success(Unit)
},
selectPushProviderLambda: (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() },
@@ -493,7 +271,7 @@ class LoggedInPresenterTest {
): PushService {
return FakePushService(
availablePushProviders = listOfNotNull(pushProvider0, pushProvider1),
registerWithLambda = registerWithLambda,
ensurePusherIsRegisteredResult = ensurePusherIsRegisteredResult,
currentPushProvider = currentPushProvider,
selectPushProviderLambda = selectPushProviderLambda,
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,

View File

@@ -25,6 +25,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
@@ -51,6 +52,8 @@ class NotificationSettingsPresenter(
private val pushService: PushService,
private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider,
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
) : Presenter<NotificationSettingsState> {
@Composable
override fun present(): NotificationSettingsState {
@@ -141,7 +144,7 @@ class NotificationSettingsPresenter(
is NotificationSettingsEvents.SetInviteForMeNotificationsEnabled -> {
localCoroutineScope.setInviteForMeNotificationsEnabled(event.enabled, changeNotificationSettingAction)
}
is NotificationSettingsEvents.SetNotificationsEnabled -> localCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled)
is NotificationSettingsEvents.SetNotificationsEnabled -> sessionCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled)
NotificationSettingsEvents.ClearConfigurationMismatchError -> {
matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
}
@@ -262,5 +265,10 @@ class NotificationSettingsPresenter(
private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch {
userPushStore.setNotificationEnabledForDevice(enabled)
if (enabled) {
pushService.ensurePusherIsRegistered(matrixClient)
} else {
pushService.getCurrentPushProvider(matrixClient.sessionId)?.unregister(matrixClient)
}
}
}

View File

@@ -8,9 +8,6 @@
package io.element.android.features.preferences.impl.notifications
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
@@ -28,6 +25,9 @@ import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds
@@ -36,9 +36,7 @@ class NotificationSettingsPresenterTest {
@Test
fun `present - ensures initial state is correct`() = runTest {
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitItem()
assertThat(initialState.appSettings.appNotificationsEnabled).isFalse()
assertThat(initialState.appSettings.systemNotificationsEnabled).isTrue()
@@ -62,9 +60,7 @@ class NotificationSettingsPresenterTest {
fun `present - default group notification mode changed`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES)
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES)
val updatedState = consumeItemsUntilPredicate {
@@ -80,9 +76,7 @@ class NotificationSettingsPresenterTest {
fun `present - notification settings mismatched`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = true,
isOneToOne = false,
@@ -110,9 +104,7 @@ class NotificationSettingsPresenterTest {
initialOneToOneDefaultMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
)
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitItem()
initialState.eventSink(NotificationSettingsEvents.FixConfigurationMismatch)
val fixedState = consumeItemsUntilPredicate(timeout = 2000.milliseconds) {
@@ -125,10 +117,19 @@ class NotificationSettingsPresenterTest {
@Test
fun `present - set notifications enabled`() = runTest {
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val unregisterWithResult = lambdaRecorder<MatrixClient, Result<Unit>> { Result.success(Unit) }
val ensurePusherIsRegisteredResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val presenter = createNotificationSettingsPresenter(
pushService = FakePushService(
currentPushProvider = {
FakePushProvider(
unregisterWithResult = unregisterWithResult,
)
},
ensurePusherIsRegisteredResult = ensurePusherIsRegisteredResult,
)
)
presenter.test {
val loadedState = consumeItemsUntilPredicate {
it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid
}.last()
@@ -138,16 +139,21 @@ class NotificationSettingsPresenterTest {
!it.appSettings.appNotificationsEnabled
}.last()
assertThat(updatedState.appSettings.appNotificationsEnabled).isFalse()
cancelAndIgnoreRemainingEvents()
unregisterWithResult.assertions().isCalledOnce()
// Enable notification again
loadedState.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(true))
val updatedState2 = consumeItemsUntilPredicate {
it.appSettings.appNotificationsEnabled
}.last()
assertThat(updatedState2.appSettings.appNotificationsEnabled).isTrue()
ensurePusherIsRegisteredResult.assertions().isCalledOnce()
}
}
@Test
fun `present - set call notifications enabled`() = runTest {
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val loadedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.callNotificationsEnabled == false
}.last()
@@ -166,9 +172,7 @@ class NotificationSettingsPresenterTest {
@Test
fun `present - set invite for me notifications enabled`() = runTest {
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val loadedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.inviteForMeNotificationsEnabled == false
}.last()
@@ -187,9 +191,7 @@ class NotificationSettingsPresenterTest {
@Test
fun `present - set atRoom notifications enabled`() = runTest {
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val loadedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == false
}.last()
@@ -210,9 +212,7 @@ class NotificationSettingsPresenterTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
notificationSettingsService.givenSetAtRoomError(AN_EXCEPTION)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val loadedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == false
}.last()
@@ -237,9 +237,7 @@ class NotificationSettingsPresenterTest {
val presenter = createNotificationSettingsPresenter(
pushService = createFakePushService(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue0", name = "aDistributorName0")))
assertThat(initialState.availablePushDistributors).containsExactly(
@@ -271,9 +269,7 @@ class NotificationSettingsPresenterTest {
val presenter = createNotificationSettingsPresenter(
pushService = createFakePushService(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue0", name = "aDistributorName0")))
assertThat(initialState.availablePushDistributors).containsExactly(
@@ -298,9 +294,7 @@ class NotificationSettingsPresenterTest {
pushService = createFakePushService(),
fullScreenIntentPermissionsStateLambda = fullScreenIntentPermissionsStateLambda,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.fullScreenIntentPermissionsState.permissionGranted).isFalse()
@@ -324,11 +318,7 @@ class NotificationSettingsPresenterTest {
},
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider)
presenter.test {
val withDialog = awaitItem()
assertThat(withDialog.showChangePushProviderDialog).isTrue()
withDialog.eventSink(NotificationSettingsEvents.SetPushProvider(1))
@@ -361,7 +351,7 @@ class NotificationSettingsPresenterTest {
)
}
private fun createNotificationSettingsPresenter(
private fun TestScope.createNotificationSettingsPresenter(
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
pushService: PushService = FakePushService(),
fullScreenIntentPermissionsStateLambda: () -> FullScreenIntentPermissionsState = { aFullScreenIntentPermissionsState() },
@@ -374,6 +364,7 @@ class NotificationSettingsPresenterTest {
pushService = pushService,
systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(),
fullScreenIntentPermissionsPresenter = { fullScreenIntentPermissionsStateLambda() },
sessionCoroutineScope = backgroundScope,
)
}
}

View File

@@ -38,6 +38,15 @@ interface PushService {
distributor: Distributor,
): Result<Unit>
/**
* Ensure that the pusher with the current push provider and distributor is registered.
* If there is no current config, the default push provider with the default distributor will be used.
* Error can be [PusherRegistrationFailure].
*/
suspend fun ensurePusherIsRegistered(
matrixClient: MatrixClient,
): Result<Unit>
/**
* Store the given push provider as the current one, but do not register.
* To be used when there is no distributor available.

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.loggedin
package io.element.android.libraries.push.api
import io.element.android.libraries.matrix.api.exception.ClientException

View File

@@ -15,8 +15,10 @@ import dev.zacsweers.metro.binding
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.libraries.push.api.history.PushHistoryItem
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
import io.element.android.libraries.push.impl.store.PushDataStore
@@ -24,11 +26,13 @@ import io.element.android.libraries.push.impl.test.TestPush
import io.element.android.libraries.push.impl.unregistration.ServiceUnregisteredHandler
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushproviders.api.RegistrationFailure
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import timber.log.Timber
@ContributesBinding(AppScope::class, binding = binding<PushService>())
@@ -84,6 +88,59 @@ class DefaultPushService(
return pushProvider.registerWith(matrixClient, distributor)
}
override suspend fun ensurePusherIsRegistered(matrixClient: MatrixClient): Result<Unit> {
val verificationStatus = matrixClient.sessionVerificationService.sessionVerifiedStatus.first()
if (verificationStatus != SessionVerifiedStatus.Verified) {
return Result.failure<Unit>(PusherRegistrationFailure.AccountNotVerified())
.also { Timber.w("Account is not verified") }
}
Timber.d("Ensure pusher is registered")
val currentPushProvider = getCurrentPushProvider(matrixClient.sessionId)
val result = if (currentPushProvider == null) {
Timber.d("Register with the first available push provider with at least one distributor")
val pushProvider = getAvailablePushProviders()
.firstOrNull { it.getDistributors().isNotEmpty() }
// Else fallback to the first available push provider (the list should never be empty)
?: getAvailablePushProviders().firstOrNull()
?: return Result.failure<Unit>(PusherRegistrationFailure.NoProvidersAvailable())
.also { Timber.w("No push providers available") }
val distributor = pushProvider.getDistributors().firstOrNull()
?: return Result.failure<Unit>(PusherRegistrationFailure.NoDistributorsAvailable())
.also { Timber.w("No distributors available") }
.also {
// In this case, consider the push provider is chosen.
selectPushProvider(matrixClient.sessionId, pushProvider)
}
registerWith(matrixClient, pushProvider, distributor)
} else {
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient.sessionId)
if (currentPushDistributor == null) {
Timber.d("Register with the first available distributor")
val distributor = currentPushProvider.getDistributors().firstOrNull()
?: return Result.failure<Unit>(PusherRegistrationFailure.NoDistributorsAvailable())
.also { Timber.w("No distributors available") }
registerWith(matrixClient, currentPushProvider, distributor)
} else {
Timber.d("Re-register with the current distributor")
registerWith(matrixClient, currentPushProvider, currentPushDistributor)
}
}
return result.fold(
onSuccess = {
Timber.d("Pusher registered")
Result.success(Unit)
},
onFailure = {
Timber.e(it, "Failed to register pusher")
if (it is RegistrationFailure) {
Result.failure(PusherRegistrationFailure.RegistrationFailure(it.clientException, it.isRegisteringAgain))
} else {
Result.failure(it)
}
}
)
}
override suspend fun selectPushProvider(
sessionId: SessionId,
pushProvider: PushProvider,

View File

@@ -12,12 +12,15 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
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.verification.FakeSessionVerificationService
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.libraries.push.api.history.PushHistoryItem
import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
@@ -40,6 +43,7 @@ import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushSto
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.InMemoryPushClientSecretStore
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.flow.first
@@ -339,6 +343,281 @@ class DefaultPushServiceTest {
}
}
@Test
fun `ensurePusher - error when account is not verified`() = runTest {
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified
)
val pushService = createDefaultPushService()
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.exceptionOrNull()!!).isInstanceOf(PusherRegistrationFailure.AccountNotVerified::class.java)
}
@Test
fun `ensurePusher - case two push providers but first one does not have distributor - second one will be used`() = runTest {
val lambda = lambdaRecorder<MatrixClient, 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),
registerWithResult = lambda,
)
val pushService = createDefaultPushService(
pushProviders = setOf(
pushProvider0,
pushProvider1,
),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.isSuccess).isTrue()
lambda.assertions().isCalledOnce()
.with(
// MatrixClient
any(),
// First distributor of second push provider
value(distributor),
)
}
@Test
fun `ensurePusher - case one push provider but no distributor available`() = runTest {
val lambda = lambdaRecorder<MatrixClient, Distributor, Result<Unit>> { _, _ ->
Result.success(Unit)
}
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushProvider = FakePushProvider(
index = 0,
name = "aFakePushProvider",
distributors = emptyList(),
registerWithResult = lambda,
)
val pushService = createDefaultPushService(
pushProviders = setOf(pushProvider),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.exceptionOrNull()).isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
lambda.assertions().isNeverCalled()
}
@Test
fun `ensurePusher - ensure default pusher is registered with default provider`() = runTest {
val lambda = lambdaRecorder<MatrixClient, Distributor, Result<Unit>> { _, _ ->
Result.success(Unit)
}
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushService = createDefaultPushService(
pushProviders = setOf(
FakePushProvider(
index = 0,
name = "aFakePushProvider",
distributors = listOf(Distributor("aDistributorValue0", "aDistributorName0")),
registerWithResult = lambda,
)
),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.isSuccess).isTrue()
lambda.assertions()
.isCalledOnce()
.with(
// MatrixClient
any(),
// First distributor
value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
)
}
@Test
fun `ensurePusher - ensure default pusher is registered with default provider - fail to register`() = runTest {
val lambda = lambdaRecorder<MatrixClient, Distributor, Result<Unit>> { _, _ ->
Result.failure(AN_EXCEPTION)
}
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushService = createDefaultPushService(
pushProviders = setOf(
FakePushProvider(
index = 0,
name = "aFakePushProvider",
distributors = listOf(Distributor("aDistributorValue0", "aDistributorName0")),
registerWithResult = lambda,
)
),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.isFailure).isTrue()
lambda.assertions()
.isCalledOnce()
.with(
// MatrixClient
any(),
// First distributor
value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
)
}
@Test
fun `ensurePusher - if current push provider does not have distributors, nothing happen`() = runTest {
val lambda = lambdaRecorder<MatrixClient, Distributor, Result<Unit>> { _, _ ->
Result.success(Unit)
}
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
val pushProvider = FakePushProvider(
index = 0,
name = "aFakePushProvider0",
distributors = emptyList(),
registerWithResult = lambda,
)
val pushService = createDefaultPushService(
pushProviders = setOf(pushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = pushProvider.name),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.exceptionOrNull())
.isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
lambda.assertions()
.isNeverCalled()
}
@Test
fun `ensurePusher - ensure current provider is registered with current distributor`() = runTest {
val lambda = lambdaRecorder<MatrixClient, 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 },
registerWithResult = lambda,
)
val pushService = createDefaultPushService(
pushProviders = setOf(pushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = pushProvider.name),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.isSuccess).isTrue()
lambda.assertions()
.isCalledOnce()
.with(
// MatrixClient
any(),
// Current distributor
value(distributor),
)
}
@Test
fun `ensurePusher - case no push provider available provider`() = runTest {
val lambda = lambdaRecorder<MatrixClient, Distributor, Result<Unit>> { _, _ ->
Result.success(Unit)
}
val sessionVerificationService = FakeSessionVerificationService(SessionVerifiedStatus.Verified)
val pushService = createDefaultPushService(
pushProviders = emptySet(),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.exceptionOrNull())
.isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java)
lambda.assertions()
.isNeverCalled()
}
@Test
fun `ensurePusher - if current push provider does not have current distributor, the first one is used`() = runTest {
val lambda = lambdaRecorder<MatrixClient, 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 },
registerWithResult = lambda,
)
val pushService = createDefaultPushService(
pushProviders = setOf(pushProvider),
getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = pushProvider.name),
)
val result = pushService.ensurePusherIsRegistered(
FakeMatrixClient(
sessionVerificationService = sessionVerificationService,
)
)
assertThat(result.isSuccess).isTrue()
lambda.assertions()
.isCalledOnce()
.with(
// MatrixClient
any(),
// First distributor
value(pushService.getAvailablePushProviders()[0].getDistributors()[0]),
)
}
private fun createDefaultPushService(
testPush: TestPush = FakeTestPush(),
userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(),

View File

@@ -32,6 +32,7 @@ class FakePushService(
private val resetPushHistoryResult: () -> Unit = { lambdaError() },
private val resetBatteryOptimizationStateResult: () -> Unit = { lambdaError() },
private val onServiceUnregisteredResult: (UserId) -> Unit = { lambdaError() },
private val ensurePusherIsRegisteredResult: () -> Result<Unit> = { lambdaError() },
) : PushService {
override suspend fun getCurrentPushProvider(sessionId: SessionId): PushProvider? {
return registeredPushProvider ?: currentPushProvider(sessionId)
@@ -56,6 +57,10 @@ class FakePushService(
}
}
override suspend fun ensurePusherIsRegistered(matrixClient: MatrixClient): Result<Unit> {
return ensurePusherIsRegisteredResult()
}
override suspend fun selectPushProvider(sessionId: SessionId, pushProvider: PushProvider) {
selectPushProviderLambda(sessionId, pushProvider)
}