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 c7d4689473
commit d3339872ff
11 changed files with 441 additions and 355 deletions

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(),