Merge pull request #5726 from element-hq/feature/bma/notificationCleanup
Notification robustness
This commit is contained in:
commit
b40ccd94c8
49 changed files with 1254 additions and 480 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class RingingCallNotificationCreator(
|
|||
): Notification? {
|
||||
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||
val imageLoader = imageLoaderHolder.get(matrixClient)
|
||||
val largeIcon = notificationBitmapLoader.getUserIcon(
|
||||
val userIcon = notificationBitmapLoader.getUserIcon(
|
||||
avatarData = AvatarData(
|
||||
id = roomId.value,
|
||||
name = roomName,
|
||||
|
|
@ -84,7 +84,7 @@ class RingingCallNotificationCreator(
|
|||
|
||||
val caller = Person.Builder()
|
||||
.setName(senderDisplayName)
|
||||
.setIcon(largeIcon)
|
||||
.setIcon(userIcon)
|
||||
.setImportant(true)
|
||||
.build()
|
||||
|
||||
|
|
@ -133,12 +133,8 @@ class RingingCallNotificationCreator(
|
|||
.setWhen(timestamp)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.apply {
|
||||
if (textContent != null) {
|
||||
setContentText(textContent)
|
||||
// Else the content text is set by the style (will be "Incoming call")
|
||||
}
|
||||
}
|
||||
// If textContent is null, the content text is set by the style (will be "Incoming call")
|
||||
.setContentText(textContent)
|
||||
.setSound(Settings.System.DEFAULT_RINGTONE_URI, AudioManager.STREAM_RING)
|
||||
.setTimeoutAfter(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds.inWholeMilliseconds)
|
||||
.setContentIntent(answerIntent)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import io.element.android.features.call.impl.R
|
||||
import io.element.android.features.call.impl.ui.ElementCallActivity
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
|
|
@ -69,7 +68,7 @@ class CallForegroundService : Service() {
|
|||
val callActivityIntent = Intent(this, ElementCallActivity::class.java)
|
||||
val pendingIntent = PendingIntentCompat.getActivity(this, 0, callActivityIntent, 0, false)
|
||||
val notification = NotificationCompat.Builder(this, foregroundServiceChannel.id)
|
||||
.setSmallIcon(IconCompat.createWithResource(this, CommonDrawables.ic_notification))
|
||||
.setSmallIcon(CommonDrawables.ic_notification)
|
||||
.setContentTitle(getString(R.string.call_foreground_service_title_android))
|
||||
.setContentText(getString(R.string.call_foreground_service_message_android))
|
||||
.setContentIntent(pendingIntent)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class DefaultActiveCallManager(
|
|||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
private val systemClock: SystemClock,
|
||||
) : ActiveCallManager {
|
||||
private val tag = "DefaultActiveCallManager"
|
||||
private val tag = "ActiveCallManager"
|
||||
private var timedOutCallJob: Job? = null
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,9 +318,7 @@ class NotificationSettingsPresenterTest {
|
|||
},
|
||||
),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider)
|
||||
val withDialog = awaitItem()
|
||||
|
|
@ -341,7 +333,7 @@ class NotificationSettingsPresenterTest {
|
|||
}
|
||||
|
||||
private fun createFakePushService(
|
||||
registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
registerWithLambda: (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
): PushService {
|
||||
|
|
@ -361,7 +353,7 @@ class NotificationSettingsPresenterTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createNotificationSettingsPresenter(
|
||||
private fun TestScope.createNotificationSettingsPresenter(
|
||||
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
|
||||
pushService: PushService = FakePushService(),
|
||||
fullScreenIntentPermissionsStateLambda: () -> FullScreenIntentPermissionsState = { aFullScreenIntentPermissionsState() },
|
||||
|
|
@ -374,6 +366,7 @@ class NotificationSettingsPresenterTest {
|
|||
pushService = pushService,
|
||||
systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(),
|
||||
fullScreenIntentPermissionsPresenter = { fullScreenIntentPermissionsStateLambda() },
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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.matrix.api.core.UserId
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
|
|
@ -37,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.
|
||||
|
|
@ -73,4 +83,9 @@ interface PushService {
|
|||
* Reset the battery optimization state.
|
||||
*/
|
||||
suspend fun resetBatteryOptimizationState()
|
||||
|
||||
/**
|
||||
* Notify the user that the service is un-registered.
|
||||
*/
|
||||
suspend fun onServiceUnregistered(userId: UserId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ object NotificationIdProvider {
|
|||
}
|
||||
|
||||
fun getForegroundServiceNotificationId(type: ForegroundServiceType): Int {
|
||||
return type.id * 10 + FOREGROUND_SERVICE_NOTIFICATION_ID
|
||||
return type.ordinal * 10 + FOREGROUND_SERVICE_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
private fun getOffset(sessionId: SessionId): Int {
|
||||
|
|
@ -50,7 +50,7 @@ object NotificationIdProvider {
|
|||
private const val FOREGROUND_SERVICE_NOTIFICATION_ID = 4
|
||||
}
|
||||
|
||||
enum class ForegroundServiceType(val id: Int) {
|
||||
INCOMING_CALL(1),
|
||||
ONGOING_CALL(2),
|
||||
enum class ForegroundServiceType {
|
||||
INCOMING_CALL,
|
||||
ONGOING_CALL,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,19 +14,25 @@ import dev.zacsweers.metro.SingleIn
|
|||
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
|
||||
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>())
|
||||
|
|
@ -40,6 +46,7 @@ class DefaultPushService(
|
|||
private val pushClientSecretStore: PushClientSecretStore,
|
||||
private val pushDataStore: PushDataStore,
|
||||
private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore,
|
||||
private val serviceUnregisteredHandler: ServiceUnregisteredHandler,
|
||||
) : PushService, SessionListener {
|
||||
init {
|
||||
observeSessions()
|
||||
|
|
@ -81,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,
|
||||
|
|
@ -141,4 +201,8 @@ class DefaultPushService(
|
|||
override suspend fun resetBatteryOptimizationState() {
|
||||
mutableBatteryOptimizationStore.reset()
|
||||
}
|
||||
|
||||
override suspend fun onServiceUnregistered(userId: UserId) {
|
||||
serviceUnregisteredHandler.handle(userId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ interface NotificationDisplayer {
|
|||
fun cancelNotification(tag: String?, id: Int)
|
||||
fun displayDiagnosticNotification(notification: Notification): Boolean
|
||||
fun dismissDiagnosticNotification()
|
||||
fun displayUnregistrationNotification(notification: Notification): Boolean
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -60,6 +61,14 @@ class DefaultNotificationDisplayer(
|
|||
)
|
||||
}
|
||||
|
||||
override fun displayUnregistrationNotification(notification: Notification): Boolean {
|
||||
return showNotification(
|
||||
tag = TAG_DIAGNOSTIC,
|
||||
id = NOTIFICATION_ID_UNREGISTRATION,
|
||||
notification = notification,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG_DIAGNOSTIC = "DIAGNOSTIC"
|
||||
|
||||
|
|
@ -67,5 +76,6 @@ class DefaultNotificationDisplayer(
|
|||
* IDs for notifications
|
||||
* ========================================================================================== */
|
||||
private const val NOTIFICATION_ID_DIAGNOSTIC = 888
|
||||
private const val NOTIFICATION_ID_UNREGISTRATION = 889
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ package io.element.android.libraries.push.impl.notifications.factories
|
|||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.MessagingStyle
|
||||
|
|
@ -92,6 +91,10 @@ interface NotificationCreator {
|
|||
@ColorInt color: Int,
|
||||
): Notification
|
||||
|
||||
fun createUnregistrationNotification(
|
||||
notificationAccountParams: NotificationAccountParams,
|
||||
): Notification
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a tag for a message notification given its [roomId] and optional [threadId].
|
||||
|
|
@ -143,14 +146,21 @@ class DefaultNotificationCreator(
|
|||
} else {
|
||||
notificationChannels.getChannelIdForMessage(noisy = roomInfo.shouldBing)
|
||||
}
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
// If any of the events are of rtc notification type it means a missed call, set the category to the right value
|
||||
val category = if (containsMissedCall) {
|
||||
NotificationCompat.CATEGORY_MISSED_CALL
|
||||
} else {
|
||||
NotificationCompat.CATEGORY_MESSAGE
|
||||
}
|
||||
val builder = if (existingNotification != null) {
|
||||
NotificationCompat.Builder(context, existingNotification)
|
||||
// Clear existing actions
|
||||
.clearActions()
|
||||
} else {
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
// ID of the corresponding shortcut, for conversation features under API 30+
|
||||
// Must match those created in the ShortcutInfoCompat.Builder()
|
||||
// for the notification to appear as a "Conversation":
|
||||
|
|
@ -166,7 +176,6 @@ class DefaultNotificationCreator(
|
|||
// Remove notification after opening it or using an action
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
val messagingStyle = existingNotification?.let {
|
||||
MessagingStyle.extractMessagingStyleFromNotification(it)
|
||||
} ?: createMessagingStyleFromCurrentUser(
|
||||
|
|
@ -176,54 +185,35 @@ class DefaultNotificationCreator(
|
|||
isThread = threadId != null,
|
||||
roomIsGroup = !roomInfo.isDm,
|
||||
)
|
||||
|
||||
messagingStyle.addMessagesFromEvents(events, imageLoader)
|
||||
|
||||
return builder
|
||||
.setCategory(category)
|
||||
.setNumber(events.size)
|
||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
||||
.setWhen(lastMessageTimestamp)
|
||||
// MESSAGING_STYLE sets title and content for API 16 and above devices.
|
||||
.setStyle(messagingStyle)
|
||||
.configureWith(notificationAccountParams)
|
||||
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
|
||||
// 'importance' which is set in the NotificationChannel. The integers representing
|
||||
// 'priority' are different from 'importance', so make sure you don't mix them.
|
||||
// Mark room/thread as read
|
||||
.addAction(markAsReadActionFactory.create(roomInfo, threadId))
|
||||
.setContentIntent(openIntent)
|
||||
.setLargeIcon(largeIcon)
|
||||
.setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
.apply {
|
||||
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
|
||||
// 'importance' which is set in the NotificationChannel. The integers representing
|
||||
// 'priority' are different from 'importance', so make sure you don't mix them.
|
||||
if (roomInfo.shouldBing) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(notificationAccountParams.color, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
// Clear existing actions since we might be updating an existing notification
|
||||
clearActions()
|
||||
// Add actions and notification intents
|
||||
// Mark room/thread as read
|
||||
addAction(markAsReadActionFactory.create(roomInfo, threadId))
|
||||
// Quick reply
|
||||
if (!roomInfo.hasSmartReplyError) {
|
||||
val latestEventId = events.lastOrNull()?.eventId
|
||||
addAction(quickReplyActionFactory.create(roomInfo, latestEventId, threadId))
|
||||
}
|
||||
if (openIntent != null) {
|
||||
setContentIntent(openIntent)
|
||||
}
|
||||
if (largeIcon != null) {
|
||||
setLargeIcon(Icon.createWithBitmap(largeIcon))
|
||||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
|
||||
// If any of the events are of rtc notification type it means a missed call, set the category to the right value
|
||||
if (events.any { it.type == EventType.RTC_NOTIFICATION }) {
|
||||
setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
||||
}
|
||||
}
|
||||
.setTicker(tickerText)
|
||||
.build()
|
||||
|
|
@ -240,32 +230,26 @@ class DefaultNotificationCreator(
|
|||
.setContentText(inviteNotifiableEvent.description.annotateForDebug(6))
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.configureWith(notificationAccountParams)
|
||||
.addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
.addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
// Build the pending intent for when the notification is clicked
|
||||
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(inviteNotifiableEvent.sessionId, inviteNotifiableEvent.roomId, null))
|
||||
.apply {
|
||||
addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
// Build the pending intent for when the notification is clicked
|
||||
setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(inviteNotifiableEvent.sessionId, inviteNotifiableEvent.roomId, null))
|
||||
|
||||
if (inviteNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(notificationAccountParams.color, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
setDeleteIntent(
|
||||
pendingIntentFactory.createDismissInvitePendingIntent(
|
||||
inviteNotifiableEvent.sessionId,
|
||||
inviteNotifiableEvent.roomId,
|
||||
)
|
||||
)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
.setDeleteIntent(
|
||||
pendingIntentFactory.createDismissInvitePendingIntent(
|
||||
inviteNotifiableEvent.sessionId,
|
||||
inviteNotifiableEvent.roomId,
|
||||
)
|
||||
)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
@ -286,11 +270,6 @@ class DefaultNotificationCreator(
|
|||
if (simpleNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(notificationAccountParams.color, 500, 500)
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
|
|
@ -349,11 +328,6 @@ class DefaultNotificationCreator(
|
|||
if (noisy) {
|
||||
// Compat
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
/*
|
||||
vectorPreferences.getNotificationRingTone()?.let {
|
||||
setSound(it)
|
||||
}
|
||||
*/
|
||||
setLights(notificationAccountParams.color, 500, 500)
|
||||
} else {
|
||||
// compat
|
||||
|
|
@ -382,6 +356,25 @@ class DefaultNotificationCreator(
|
|||
.build()
|
||||
}
|
||||
|
||||
override fun createUnregistrationNotification(
|
||||
notificationAccountParams: NotificationAccountParams,
|
||||
): Notification {
|
||||
val userId = notificationAccountParams.user.userId
|
||||
val text = stringProvider.getString(R.string.notification_error_unified_push_unregistered_android)
|
||||
return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest())
|
||||
.setSubText(userId.value)
|
||||
// The text is long and can be truncated so use BigTextStyle.
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setContentTitle(stringProvider.getString(CommonStrings.dialog_title_warning))
|
||||
.setContentText(text)
|
||||
.configureWith(notificationAccountParams)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(userId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun MessagingStyle.addMessagesFromEvents(
|
||||
events: List<NotifiableMessageEvent>,
|
||||
imageLoader: ImageLoader,
|
||||
|
|
@ -483,7 +476,7 @@ class DefaultNotificationCreator(
|
|||
.build()
|
||||
).also {
|
||||
it.conversationTitle = if (isThread) {
|
||||
stringProvider.getString(CommonStrings.notification_thread_in_room, roomName)
|
||||
stringProvider.getString(R.string.notification_thread_in_room, roomName)
|
||||
} else {
|
||||
roomName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.unregistration
|
||||
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.appconfig.NotificationConfig
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
interface ServiceUnregisteredHandler {
|
||||
suspend fun handle(userId: UserId)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultServiceUnregisteredHandler(
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val notificationCreator: NotificationCreator,
|
||||
private val notificationDisplayer: NotificationDisplayer,
|
||||
private val sessionStore: SessionStore,
|
||||
) : ServiceUnregisteredHandler {
|
||||
override suspend fun handle(userId: UserId) {
|
||||
val color = enterpriseService.brandColorsFlow(userId).first()?.toArgb()
|
||||
?: NotificationConfig.NOTIFICATION_ACCENT_COLOR
|
||||
val hasMultipleAccounts = sessionStore.numberOfSessions() > 1
|
||||
val notification = notificationCreator.createUnregistrationNotification(
|
||||
NotificationAccountParams(
|
||||
user = MatrixUser(userId),
|
||||
color = color,
|
||||
showSessionId = hasMultipleAccounts,
|
||||
)
|
||||
)
|
||||
notificationDisplayer.displayUnregistrationNotification(notification)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
<string name="notification_sender_me">"Já"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s zmínil(a) nebo odpověděl(a)"</string>
|
||||
<string name="notification_test_push_notification_content">"Prohlížíte si oznámení! Klikněte na mě!"</string>
|
||||
<string name="notification_thread_in_room">"Vlákno v %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
<string name="notification_sender_me">"Mig"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s nævnt eller besvaret"</string>
|
||||
<string name="notification_test_push_notification_content">"Du ser notifikationen! Klik på mig!"</string>
|
||||
<string name="notification_thread_in_room">"Tråd i %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
<string name="notification_sender_me">"Ich"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s hat Dich erwähnt oder geantwortet"</string>
|
||||
<string name="notification_test_push_notification_content">"Du siehst dir die Benachrichtigung an! Klicke hier!"</string>
|
||||
<string name="notification_thread_in_room">"Thread in %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
<string name="notification_sender_me">"Mina"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s mainis või vastas"</string>
|
||||
<string name="notification_test_push_notification_content">"See ongi teavitus! Klõpsi mind!"</string>
|
||||
<string name="notification_thread_in_room">"Jutulõng „%1$s“ jututoas"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
<string name="notification_sender_me">"Moi"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s mentionné ou en réponse"</string>
|
||||
<string name="notification_test_push_notification_content">"Vous êtes en train de voir la notification ! Cliquez-moi !"</string>
|
||||
<string name="notification_thread_in_room">"Discussion dans %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s : %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s : %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
<string name="notification_sender_me">"Ja"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s spomenul/a alebo odpovedal/a"</string>
|
||||
<string name="notification_test_push_notification_content">"Prezeráte si oznámenie! Kliknite na mňa!"</string>
|
||||
<string name="notification_thread_in_room">"Vlákno v %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<string name="notification_sender_me">"我"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s 提及或回覆"</string>
|
||||
<string name="notification_test_push_notification_content">"您正在查看通知!點我!"</string>
|
||||
<string name="notification_thread_in_room">"在 %1$s 的討論串"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s:%2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s:%2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<item quantity="one">"%d notification"</item>
|
||||
<item quantity="other">"%d notifications"</item>
|
||||
</plurals>
|
||||
<string name="notification_error_unified_push_unregistered_android">"The UnifiedPush notification distributor couldn\'t be registered, so you will not receive notifications anymore. Please check the notifications settings of the app and the status of the push distributor."</string>
|
||||
<string name="notification_fallback_content">"You have new messages."</string>
|
||||
<string name="notification_incoming_call">"📹 Incoming call"</string>
|
||||
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
<string name="notification_sender_me">"Me"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s mentioned or replied"</string>
|
||||
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
|
||||
<string name="notification_thread_in_room">"Thread in %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -25,6 +28,8 @@ import io.element.android.libraries.push.impl.store.InMemoryPushDataStore
|
|||
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||
import io.element.android.libraries.push.impl.test.FakeTestPush
|
||||
import io.element.android.libraries.push.impl.test.TestPush
|
||||
import io.element.android.libraries.push.impl.unregistration.FakeServiceUnregisteredHandler
|
||||
import io.element.android.libraries.push.impl.unregistration.ServiceUnregisteredHandler
|
||||
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
|
|
@ -38,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
|
||||
|
|
@ -337,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(),
|
||||
|
|
@ -346,6 +627,7 @@ class DefaultPushServiceTest {
|
|||
pushClientSecretStore: PushClientSecretStore = InMemoryPushClientSecretStore(),
|
||||
pushDataStore: PushDataStore = InMemoryPushDataStore(),
|
||||
mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(),
|
||||
serviceUnregisteredHandler: ServiceUnregisteredHandler = FakeServiceUnregisteredHandler(),
|
||||
): DefaultPushService {
|
||||
return DefaultPushService(
|
||||
testPush = testPush,
|
||||
|
|
@ -356,6 +638,7 @@ class DefaultPushServiceTest {
|
|||
pushClientSecretStore = pushClientSecretStore,
|
||||
pushDataStore = pushDataStore,
|
||||
mutableBatteryOptimizationStore = mutableBatteryOptimizationStore,
|
||||
serviceUnregisteredHandler = serviceUnregisteredHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,21 @@ class DefaultNotificationCreatorTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createUnregistrationNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
val matrixUser = aMatrixUser()
|
||||
val result = sut.createUnregistrationNotification(
|
||||
notificationAccountParams = aNotificationAccountParams(
|
||||
user = matrixUser,
|
||||
),
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedGroup = matrixUser.userId.value,
|
||||
expectedCategory = NotificationCompat.CATEGORY_ERROR,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createFallbackNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ class FakeNotificationCreator(
|
|||
> = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION },
|
||||
var createDiagnosticNotificationResult: LambdaOneParamRecorder<Int, Notification> =
|
||||
lambdaRecorder<Int, Notification> { _ -> A_NOTIFICATION },
|
||||
val createUnregistrationNotificationResult: LambdaOneParamRecorder<NotificationAccountParams, Notification> =
|
||||
lambdaRecorder { _ -> A_NOTIFICATION },
|
||||
) : NotificationCreator {
|
||||
override suspend fun createMessagesListNotification(
|
||||
notificationAccountParams: NotificationAccountParams,
|
||||
|
|
@ -93,4 +95,8 @@ class FakeNotificationCreator(
|
|||
): Notification {
|
||||
return createDiagnosticNotificationResult(color)
|
||||
}
|
||||
|
||||
override fun createUnregistrationNotification(notificationAccountParams: NotificationAccountParams): Notification {
|
||||
return createUnregistrationNotificationResult(notificationAccountParams)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class FakeNotificationDisplayer(
|
|||
var cancelNotificationResult: LambdaTwoParamsRecorder<String?, Int, Unit> = lambdaRecorder { _, _ -> },
|
||||
var displayDiagnosticNotificationResult: LambdaOneParamRecorder<Notification, Boolean> = lambdaRecorder { _ -> true },
|
||||
var dismissDiagnosticNotificationResult: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
|
||||
var displayUnregistrationNotificationResult: LambdaOneParamRecorder<Notification, Boolean> = lambdaRecorder { _ -> true },
|
||||
) : NotificationDisplayer {
|
||||
override fun showNotification(tag: String?, id: Int, notification: Notification): Boolean {
|
||||
return showNotificationResult(tag, id, notification)
|
||||
|
|
@ -41,6 +42,10 @@ class FakeNotificationDisplayer(
|
|||
return dismissDiagnosticNotificationResult()
|
||||
}
|
||||
|
||||
override fun displayUnregistrationNotification(notification: Notification): Boolean {
|
||||
return displayUnregistrationNotificationResult(notification)
|
||||
}
|
||||
|
||||
fun verifySummaryCancelled(times: Int = 1) {
|
||||
cancelNotificationResult.assertions().isCalledExactly(times).withSequence(
|
||||
listOf(value(null), value(NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.unregistration
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import io.element.android.appconfig.NotificationConfig
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultServiceUnregisteredHandlerTest {
|
||||
@Test
|
||||
fun `handle will create a notification and render it`() = runTest {
|
||||
val notification = A_NOTIFICATION
|
||||
val createUnregistrationNotificationResult = lambdaRecorder<NotificationAccountParams, Notification> { notification }
|
||||
val displayUnregistrationNotificationResult = lambdaRecorder<Notification, Boolean> { true }
|
||||
val sut = createDefaultServiceUnregisteredHandler(
|
||||
notificationCreator = FakeNotificationCreator(
|
||||
createUnregistrationNotificationResult = createUnregistrationNotificationResult,
|
||||
),
|
||||
notificationDisplayer = FakeNotificationDisplayer(
|
||||
displayUnregistrationNotificationResult = displayUnregistrationNotificationResult,
|
||||
)
|
||||
)
|
||||
sut.handle(A_SESSION_ID)
|
||||
createUnregistrationNotificationResult.assertions().isCalledOnce().with(
|
||||
value(
|
||||
NotificationAccountParams(
|
||||
MatrixUser(
|
||||
userId = A_SESSION_ID,
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
),
|
||||
color = NotificationConfig.NOTIFICATION_ACCENT_COLOR,
|
||||
showSessionId = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
displayUnregistrationNotificationResult.assertions().isCalledOnce().with(
|
||||
value(notification)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle will create a notification and render it - custom color and multi accounts`() = runTest {
|
||||
val notification = A_NOTIFICATION
|
||||
val createUnregistrationNotificationResult = lambdaRecorder<NotificationAccountParams, Notification> { notification }
|
||||
val displayUnregistrationNotificationResult = lambdaRecorder<Notification, Boolean> { true }
|
||||
val sut = createDefaultServiceUnregisteredHandler(
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
initialBrandColor = Color.Red,
|
||||
),
|
||||
notificationCreator = FakeNotificationCreator(
|
||||
createUnregistrationNotificationResult = createUnregistrationNotificationResult,
|
||||
),
|
||||
notificationDisplayer = FakeNotificationDisplayer(
|
||||
displayUnregistrationNotificationResult = displayUnregistrationNotificationResult,
|
||||
),
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(sessionId = A_SESSION_ID.value),
|
||||
aSessionData(sessionId = A_SESSION_ID_2.value),
|
||||
)
|
||||
)
|
||||
)
|
||||
sut.handle(A_SESSION_ID)
|
||||
createUnregistrationNotificationResult.assertions().isCalledOnce().with(
|
||||
value(
|
||||
NotificationAccountParams(
|
||||
MatrixUser(
|
||||
userId = A_SESSION_ID,
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
),
|
||||
color = Color.Red.toArgb(),
|
||||
showSessionId = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
displayUnregistrationNotificationResult.assertions().isCalledOnce().with(
|
||||
value(notification)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDefaultServiceUnregisteredHandler(
|
||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||
notificationCreator: NotificationCreator = FakeNotificationCreator(),
|
||||
notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
) = DefaultServiceUnregisteredHandler(
|
||||
enterpriseService = enterpriseService,
|
||||
notificationCreator = notificationCreator,
|
||||
notificationDisplayer = notificationDisplayer,
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.unregistration
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeServiceUnregisteredHandler(
|
||||
private val handleResult: (UserId) -> Unit = { lambdaError() },
|
||||
) : ServiceUnregisteredHandler {
|
||||
override suspend fun handle(userId: UserId) {
|
||||
handleResult(userId)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ 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.matrix.api.core.UserId
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
|
|
@ -22,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
class FakePushService(
|
||||
private val testPushBlock: suspend (SessionId) -> Boolean = { true },
|
||||
private val availablePushProviders: List<PushProvider> = emptyList(),
|
||||
private val registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
private val registerWithLambda: (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
},
|
||||
private val currentPushProvider: (SessionId) -> PushProvider? = { availablePushProviders.firstOrNull() },
|
||||
|
|
@ -30,6 +31,8 @@ class FakePushService(
|
|||
private val setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
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)
|
||||
|
|
@ -54,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)
|
||||
}
|
||||
|
|
@ -98,4 +105,8 @@ class FakePushService(
|
|||
override suspend fun resetBatteryOptimizationState() {
|
||||
resetBatteryOptimizationStateResult()
|
||||
}
|
||||
|
||||
override suspend fun onServiceUnregistered(userId: UserId) {
|
||||
onServiceUnregisteredResult(userId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ dependencies {
|
|||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.troubleshoot.api)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class DefaultUnifiedPushNewGatewayHandler(
|
|||
val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>(
|
||||
IllegalStateException("Unable to retrieve session")
|
||||
).also {
|
||||
Timber.w("Unable to retrieve session")
|
||||
Timber.tag(loggerTag.value).w("Unable to retrieve session")
|
||||
}
|
||||
val userDataStore = userPushStoreFactory.getOrCreate(userId)
|
||||
return if (userDataStore.getPushProviderName() == UnifiedPushConfig.NAME) {
|
||||
|
|
@ -48,6 +48,9 @@ class DefaultUnifiedPushNewGatewayHandler(
|
|||
.flatMap { client ->
|
||||
pusherSubscriber.registerPusher(client, endpoint, pushGateway)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to register pusher")
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher")
|
||||
Result.failure(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("UnifiedPushRemovedGatewayHandler", LoggerTag.PushLoggerTag)
|
||||
|
||||
/**
|
||||
* Handle endpoint removal received from UnifiedPush. Will try to register again.
|
||||
*/
|
||||
fun interface UnifiedPushRemovedGatewayHandler {
|
||||
suspend fun handle(clientSecret: String): Result<Unit>
|
||||
}
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class UnifiedPushRemovedGatewayThrottler(
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) {
|
||||
private val firstThrottler = FirstThrottler(
|
||||
minimumInterval = 60_000,
|
||||
coroutineScope = appCoroutineScope,
|
||||
)
|
||||
|
||||
fun canRegisterAgain(): Boolean {
|
||||
return firstThrottler.canHandle()
|
||||
}
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushRemovedGatewayHandler(
|
||||
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val pushService: PushService,
|
||||
private val unifiedPushRemovedGatewayThrottler: UnifiedPushRemovedGatewayThrottler,
|
||||
) : UnifiedPushRemovedGatewayHandler {
|
||||
/**
|
||||
* The application has been informed by the UnifiedPush distributor that the topic has been deleted.
|
||||
* So this code aim to unregister the pusher from the homeserver, register a new topic on the
|
||||
* UnifiedPush application then register a new pusher to the homeserver.
|
||||
* No registration will happen if the topic deletion has already occurred in the last minute.
|
||||
*/
|
||||
override suspend fun handle(clientSecret: String): Result<Unit> {
|
||||
val sessionId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>(
|
||||
IllegalStateException("Unable to retrieve session")
|
||||
).also {
|
||||
Timber.tag(loggerTag.value).w("Unable to retrieve session")
|
||||
}
|
||||
return matrixClientProvider
|
||||
.getOrRestore(sessionId)
|
||||
.onFailure {
|
||||
// Silently ignore this error (do not invoke onServiceUnregistered)
|
||||
Timber.tag(loggerTag.value).w(it, "Fails to restore client")
|
||||
}
|
||||
.flatMap { client ->
|
||||
client.rotateRegistration(clientSecret = clientSecret)
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Issue during pusher unregistration / re registration")
|
||||
// Let the user know
|
||||
pushService.onServiceUnregistered(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the pusher for the session. Then register again if possible.
|
||||
*/
|
||||
private suspend fun MatrixClient.rotateRegistration(clientSecret: String): Result<Unit> {
|
||||
val unregisterResult = unregisterUnifiedPushUseCase.unregister(
|
||||
matrixClient = this,
|
||||
clientSecret = clientSecret,
|
||||
unregisterUnifiedPush = false,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to unregister pusher")
|
||||
}
|
||||
return unregisterResult.flatMap {
|
||||
registerAgain()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to register again, if possible i.e. the current configuration is known and the
|
||||
* deletion of data in the UnifiedPush application has not already occurred in the last minute.
|
||||
*/
|
||||
private suspend fun MatrixClient.registerAgain(): Result<Unit> {
|
||||
return if (unifiedPushRemovedGatewayThrottler.canRegisterAgain()) {
|
||||
val pushProvider = pushService.getCurrentPushProvider(sessionId)
|
||||
val distributor = pushProvider?.getCurrentDistributor(sessionId)
|
||||
if (pushProvider != null && distributor != null) {
|
||||
pushService.registerWith(
|
||||
matrixClient = this,
|
||||
pushProvider = pushProvider,
|
||||
distributor = distributor,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to register with current data")
|
||||
}
|
||||
} else {
|
||||
Result.failure(IllegalStateException("Unable to register again"))
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).w("Second removal in less than 1 minute, do not register again")
|
||||
Result.failure(IllegalStateException("Too many requests to register again"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,21 @@ import timber.log.Timber
|
|||
|
||||
interface UnregisterUnifiedPushUseCase {
|
||||
/**
|
||||
* Unregister the app from the homeserver, then from UnifiedPush.
|
||||
* Unregister the app from the homeserver, then from UnifiedPush if [unregisterUnifiedPush] is true.
|
||||
*/
|
||||
suspend fun unregister(matrixClient: MatrixClient, clientSecret: String): Result<Unit>
|
||||
suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean = true,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Cleanup any remaining data for the given client secret and unregister the app from UnifiedPush.
|
||||
*/
|
||||
fun cleanup(clientSecret: String)
|
||||
fun cleanup(
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean = true,
|
||||
)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -35,7 +42,11 @@ class DefaultUnregisterUnifiedPushUseCase(
|
|||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
) : UnregisterUnifiedPushUseCase {
|
||||
override suspend fun unregister(matrixClient: MatrixClient, clientSecret: String): Result<Unit> {
|
||||
override suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean,
|
||||
): Result<Unit> {
|
||||
val endpoint = unifiedPushStore.getEndpoint(clientSecret)
|
||||
val gateway = unifiedPushStore.getPushGateway(clientSecret)
|
||||
if (endpoint == null || gateway == null) {
|
||||
|
|
@ -46,13 +57,15 @@ class DefaultUnregisterUnifiedPushUseCase(
|
|||
}
|
||||
return pusherSubscriber.unregisterPusher(matrixClient, endpoint, gateway)
|
||||
.onSuccess {
|
||||
cleanup(clientSecret)
|
||||
cleanup(clientSecret, unregisterUnifiedPush)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup(clientSecret: String) {
|
||||
override fun cleanup(clientSecret: String, unregisterUnifiedPush: Boolean) {
|
||||
unifiedPushStore.storeUpEndpoint(clientSecret, null)
|
||||
unifiedPushStore.storePushGateway(clientSecret, null)
|
||||
UnifiedPush.unregister(context, clientSecret)
|
||||
if (unregisterUnifiedPush) {
|
||||
UnifiedPush.unregister(context, clientSecret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
@Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver
|
||||
@Inject lateinit var unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver
|
||||
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
|
||||
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
|
||||
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
|
||||
|
||||
@AppCoroutineScope
|
||||
@Inject lateinit var coroutineScope: CoroutineScope
|
||||
|
||||
|
|
@ -104,30 +106,23 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
*/
|
||||
override fun onRegistrationFailed(context: Context, reason: FailedReason, instance: String) {
|
||||
Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance, reason: $reason")
|
||||
/*
|
||||
Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show()
|
||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||
guardServiceStarter.start()
|
||||
*/
|
||||
coroutineScope.launch {
|
||||
endpointRegistrationHandler.registrationDone(
|
||||
RegistrationResult(
|
||||
clientSecret = instance,
|
||||
result = Result.failure(Exception("Registration failed. Reason: $reason")),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this application is unregistered from receiving push messages.
|
||||
*/
|
||||
override fun onUnregistered(context: Context, instance: String) {
|
||||
Timber.tag(loggerTag.value).w("UnifiedPush: Unregistered")
|
||||
/*
|
||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||
guardServiceStarter.start()
|
||||
runBlocking {
|
||||
try {
|
||||
pushersManager.unregisterPusher(unifiedPushHelper.getEndpointOrToken().orEmpty())
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher")
|
||||
}
|
||||
Timber.tag(loggerTag.value).w("onUnregistered $instance")
|
||||
coroutineScope.launch {
|
||||
removedGatewayHandler.handle(instance)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_SECRET
|
||||
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.FakeMatrixClientProvider
|
||||
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.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
||||
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.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class DefaultUnifiedPushRemovedGatewayHandlerTest {
|
||||
@Test
|
||||
fun `handle returns error if the secret is unknown`() = runTest {
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { null },
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot restore the client`() = runTest {
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.failure(AN_EXCEPTION) },
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot unregister the pusher, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.failure(AN_EXCEPTION) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot get current push provider, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = { null },
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot get current distributor, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { null },
|
||||
)
|
||||
},
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot register again, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = { _, _, _ -> Result.failure(AN_EXCEPTION) },
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns success if can register again, and user is not notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = { _, _, _ -> Result.success(Unit) },
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
value(A_SECRET),
|
||||
value(false),
|
||||
)
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns success if can register again, but after 2 removals user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val registerWithLambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = registerWithLambda,
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
value(A_SECRET),
|
||||
value(false),
|
||||
)
|
||||
registerWithLambda.assertions().isCalledOnce()
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
// Second attempt in less than 1 minute
|
||||
val result2 = sut.handle(A_SECRET)
|
||||
assertThat(result2.isFailure).isTrue()
|
||||
unregisterLambda.assertions().isCalledExactly(2)
|
||||
// Registration is not called twice
|
||||
registerWithLambda.assertions().isCalledOnce()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `handle returns success if can register again, but after 2 distant removals user is not notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val registerWithLambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = registerWithLambda,
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
value(A_SECRET),
|
||||
value(false),
|
||||
)
|
||||
registerWithLambda.assertions().isCalledOnce()
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
// Second attempt in more than 1 minute
|
||||
advanceTimeBy(61.seconds)
|
||||
val result2 = sut.handle(A_SECRET)
|
||||
assertThat(result2.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledExactly(2)
|
||||
// Registration is not called twice
|
||||
registerWithLambda.assertions().isCalledExactly(2)
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(),
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
pushService: PushService = FakePushService(),
|
||||
) = DefaultUnifiedPushRemovedGatewayHandler(
|
||||
unregisterUnifiedPushUseCase = unregisterUnifiedPushUseCase,
|
||||
pushClientSecret = pushClientSecret,
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
pushService = pushService,
|
||||
unifiedPushRemovedGatewayThrottler = UnifiedPushRemovedGatewayThrottler(
|
||||
appCoroutineScope = backgroundScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -12,14 +12,21 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeUnregisterUnifiedPushUseCase(
|
||||
private val unregisterLambda: (MatrixClient, String) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val cleanupLambda: (String) -> Unit = { lambdaError() },
|
||||
private val unregisterLambda: (MatrixClient, String, Boolean) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
||||
private val cleanupLambda: (String, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
) : UnregisterUnifiedPushUseCase {
|
||||
override suspend fun unregister(matrixClient: MatrixClient, clientSecret: String): Result<Unit> {
|
||||
return unregisterLambda(matrixClient, clientSecret)
|
||||
override suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean,
|
||||
): Result<Unit> {
|
||||
return unregisterLambda(matrixClient, clientSecret, unregisterUnifiedPush)
|
||||
}
|
||||
|
||||
override fun cleanup(clientSecret: String) {
|
||||
cleanupLambda(clientSecret)
|
||||
override fun cleanup(
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean,
|
||||
) {
|
||||
cleanupLambda(clientSecret, unregisterUnifiedPush)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class UnifiedPushProviderTest {
|
|||
fun `unregister ok`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val getSecretForUserResultLambda = lambdaRecorder<SessionId, String> { A_SECRET }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Result<Unit>> { _, _ -> Result.success(Unit) }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = getSecretForUserResultLambda,
|
||||
|
|
@ -134,14 +134,14 @@ class UnifiedPushProviderTest {
|
|||
.with(value(A_SESSION_ID))
|
||||
unregisterLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(matrixClient), value(A_SECRET))
|
||||
.with(value(matrixClient), value(A_SECRET), value(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unregister ko`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val getSecretForUserResultLambda = lambdaRecorder<SessionId, String> { A_SECRET }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Result<Unit>> { _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = getSecretForUserResultLambda,
|
||||
|
|
@ -157,7 +157,7 @@ class UnifiedPushProviderTest {
|
|||
.with(value(A_SESSION_ID))
|
||||
unregisterLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(matrixClient), value(A_SECRET))
|
||||
.with(value(matrixClient), value(A_SECRET), value(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -230,7 +230,7 @@ class UnifiedPushProviderTest {
|
|||
|
||||
@Test
|
||||
fun `onSessionDeleted should do the cleanup`() = runTest {
|
||||
val cleanupLambda = lambdaRecorder<String, Unit> { }
|
||||
val cleanupLambda = lambdaRecorder<String, Boolean, Unit> { _, _ -> }
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = { A_SECRET }
|
||||
|
|
@ -240,7 +240,7 @@ class UnifiedPushProviderTest {
|
|||
),
|
||||
)
|
||||
unifiedPushProvider.onSessionDeleted(A_SESSION_ID)
|
||||
cleanupLambda.assertions().isCalledOnce().with(value(A_SECRET))
|
||||
cleanupLambda.assertions().isCalledOnce().with(value(A_SECRET), value(true))
|
||||
}
|
||||
|
||||
private fun createUnifiedPushProvider(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.pushproviders.api.PushData
|
|||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
|
||||
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.ExperimentalCoroutinesApi
|
||||
|
|
@ -51,10 +52,17 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `onUnregistered does nothing`() = runTest {
|
||||
fun `onUnregistered invokes the removedGatewayHandler`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver()
|
||||
val handleResult = lambdaRecorder<String, Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
removedGatewayHandler = UnifiedPushRemovedGatewayHandler { handleResult(it) },
|
||||
)
|
||||
vectorUnifiedPushMessagingReceiver.onUnregistered(context, A_SECRET)
|
||||
advanceUntilIdle()
|
||||
handleResult.assertions().isCalledOnce().with(value(A_SECRET))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -199,6 +207,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver = FakeUnifiedPushGatewayUrlResolver(),
|
||||
unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(),
|
||||
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
||||
removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() },
|
||||
): VectorUnifiedPushMessagingReceiver {
|
||||
return VectorUnifiedPushMessagingReceiver().apply {
|
||||
this.pushParser = unifiedPushParser
|
||||
|
|
@ -208,6 +217,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
this.unifiedPushGatewayResolver = unifiedPushGatewayResolver
|
||||
this.unifiedPushGatewayUrlResolver = unifiedPushGatewayUrlResolver
|
||||
this.newGatewayHandler = unifiedPushNewGatewayHandler
|
||||
this.removedGatewayHandler = removedGatewayHandler
|
||||
this.endpointRegistrationHandler = endpointRegistrationHandler
|
||||
this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver
|
||||
}
|
||||
|
|
|
|||
|
|
@ -427,7 +427,6 @@ Opravdu chcete pokračovat?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Připojte se ke mně na %1$s"</string>
|
||||
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Vlákno v %1$s"</string>
|
||||
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Snímek obrazovky"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -420,7 +420,6 @@ Er du sikker på, at du vil fortsætte?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Kom med mig til %1$s"</string>
|
||||
<string name="invite_friends_text">"Hej, lad os snakkes på %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Tråd i %1$s"</string>
|
||||
<string name="preference_rageshake">"Ryst enheden i frustration for at anmelde en fejl"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Skærmbillede"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -420,7 +420,6 @@ Möchtest du wirklich fortfahren?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Begleite mich auf %1$s"</string>
|
||||
<string name="invite_friends_text">"Hey, sprich mit mir auf %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Thread in %1$s"</string>
|
||||
<string name="preference_rageshake">"Heftiges Schütteln um Fehler zu melden"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Bildschirmfoto"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -421,7 +421,6 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Liitu minuga rakenduses %1$s"</string>
|
||||
<string name="invite_friends_text">"Hei, suhtle minuga %1$s võrgus: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Jutulõng „%1$s“ jututoas"</string>
|
||||
<string name="preference_rageshake">"Veast teatamiseks raputa nutiseadet ägedalt"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Ekraanitõmmis"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -421,7 +421,6 @@ Raison : %1$s."</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Rejoignez-moi sur %1$s"</string>
|
||||
<string name="invite_friends_text">"Salut, parle-moi sur %1$s : %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Discussion dans %1$s"</string>
|
||||
<string name="preference_rageshake">"Rageshake pour signaler un problème"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Capture d’écran"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -429,7 +429,6 @@ Naozaj chcete pokračovať?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Pripojte sa ku mne na %1$s"</string>
|
||||
<string name="invite_friends_text">"Ahoj, porozprávajte sa so mnou na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Vlákno v %1$s"</string>
|
||||
<string name="preference_rageshake">"Zúrivo potriasť pre nahlásenie chyby"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Snímka obrazovky"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -412,7 +412,6 @@
|
|||
<string name="invite_friends_rich_title">"🔐️ 在 %1$s 上加入我"</string>
|
||||
<string name="invite_friends_text">"嘿,來 %1$s 和我聊天:%2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"在 %1$s 的討論串"</string>
|
||||
<string name="preference_rageshake">"憤怒搖晃以回報臭蟲"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"螢幕截圖"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s:%2$s"</string>
|
||||
|
|
|
|||
|
|
@ -421,7 +421,6 @@ Are you sure you want to continue?"</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Join me on %1$s"</string>
|
||||
<string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="notification_thread_in_room">"Thread in %1$s"</string>
|
||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Screenshot"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@
|
|||
"includeRegex" : [
|
||||
"push_.*",
|
||||
"notification_.*",
|
||||
"notification\\..*",
|
||||
"troubleshoot_notifications\\.test_blocked_users\\..*",
|
||||
"troubleshoot_notifications_test_current_push_provider.*",
|
||||
"troubleshoot_notifications_test_detect_push_provider.*",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue