Render an error dialog in case registering a pusher fails.

This commit is contained in:
Benoit Marty 2024-06-17 09:48:21 +02:00 committed by Benoit Marty
parent f7b8e0c931
commit e6f6e82ce2
12 changed files with 131 additions and 9 deletions

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.appnav.loggedin
sealed interface LoggedInEvents {
data object CloseErrorDialog : LoggedInEvents
}

View file

@ -38,6 +38,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.RegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import timber.log.Timber import timber.log.Timber
@ -81,9 +82,16 @@ class LoggedInPresenter @Inject constructor(
reportCryptoStatusToAnalytics(verificationState, recoveryState) reportCryptoStatusToAnalytics(verificationState, recoveryState)
} }
fun handleEvent(event: LoggedInEvents) {
when (event) {
LoggedInEvents.CloseErrorDialog -> pusherRegistrationState.value = AsyncData.Uninitialized
}
}
return LoggedInState( return LoggedInState(
showSyncSpinner = showSyncSpinner, showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState.value, pusherRegistrationState = pusherRegistrationState.value,
eventSink = ::handleEvent
) )
} }
@ -122,7 +130,13 @@ class LoggedInPresenter @Inject constructor(
}, },
onFailure = { onFailure = {
Timber.tag(pusherTag.value).e(it, "Failed to register pusher") Timber.tag(pusherTag.value).e(it, "Failed to register pusher")
pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.RegistrationFailure(it)) if (it is RegistrationFailure) {
pusherRegistrationState.value = AsyncData.Failure(
PusherRegistrationFailure.RegistrationFailure(it.clientException, it.isRegisteringAgain)
)
} else {
pusherRegistrationState.value = AsyncData.Failure(it)
}
} }
) )
} }

View file

@ -21,4 +21,5 @@ import io.element.android.libraries.architecture.AsyncData
data class LoggedInState( data class LoggedInState(
val showSyncSpinner: Boolean, val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>, val pusherRegistrationState: AsyncData<Unit>,
val eventSink: (LoggedInEvents) -> Unit,
) )

View file

@ -22,15 +22,17 @@ import io.element.android.libraries.architecture.AsyncData
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> { open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState> override val values: Sequence<LoggedInState>
get() = sequenceOf( get() = sequenceOf(
aLoggedInState(false), aLoggedInState(),
aLoggedInState(true), aLoggedInState(showSyncSpinner = true),
// Add other state here aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoProvidersAvailable())),
) )
} }
fun aLoggedInState( fun aLoggedInState(
showSyncSpinner: Boolean = true, showSyncSpinner: Boolean = false,
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
) = LoggedInState( ) = LoggedInState(
showSyncSpinner = showSyncSpinner, showSyncSpinner = showSyncSpinner,
pusherRegistrationState = AsyncData.Uninitialized, pusherRegistrationState = pusherRegistrationState,
eventSink = {},
) )

View file

@ -22,9 +22,14 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.exception.isNetworkError
import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun LoggedInView( fun LoggedInView(
@ -41,6 +46,38 @@ fun LoggedInView(
isVisible = state.showSyncSpinner, isVisible = state.showSyncSpinner,
) )
} }
when (state.pusherRegistrationState) {
is AsyncData.Uninitialized,
is AsyncData.Loading,
is AsyncData.Success -> Unit
is AsyncData.Failure -> {
state.pusherRegistrationState.errorOrNull()
?.getReason()
?.let { reason ->
ErrorDialog(
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
onDismiss = { state.eventSink(LoggedInEvents.CloseErrorDialog) },
)
}
}
}
}
private fun Throwable.getReason(): String? {
return when (this) {
is PusherRegistrationFailure.RegistrationFailure -> {
if (isRegisteringAgain && clientException.isNetworkError()) {
// When registering again, ignore network error
null
} else {
clientException.message ?: "Unknown error"
}
}
is PusherRegistrationFailure.AccountNotVerified -> null
is PusherRegistrationFailure.NoDistributorsAvailable -> "No distributors available"
is PusherRegistrationFailure.NoProvidersAvailable -> "No providers available"
else -> "Other error"
}
} }
@PreviewsDayNight @PreviewsDayNight

View file

@ -16,9 +16,19 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import io.element.android.libraries.matrix.api.exception.ClientException
sealed class PusherRegistrationFailure : Exception() { sealed class PusherRegistrationFailure : Exception() {
class AccountNotVerified : PusherRegistrationFailure() class AccountNotVerified : PusherRegistrationFailure()
class NoProvidersAvailable : PusherRegistrationFailure() class NoProvidersAvailable : PusherRegistrationFailure()
class NoDistributorsAvailable : PusherRegistrationFailure() class NoDistributorsAvailable : PusherRegistrationFailure()
class RegistrationFailure(val failure: Throwable) : PusherRegistrationFailure()
/**
* @param clientException the failure that occurred.
* @param isRegisteringAgain true if the server should already have a the same pusher registered.
*/
class RegistrationFailure(
val clientException: ClientException,
val isRegisteringAgain: Boolean,
) : PusherRegistrationFailure()
} }

View file

@ -389,6 +389,10 @@ class LoggedInPresenterTest {
.isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java) .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
lambda.assertions() lambda.assertions()
.isNeverCalled() .isNeverCalled()
// Reset the error
finalState.eventSink(LoggedInEvents.CloseErrorDialog)
val lastState = awaitItem()
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
} }
} }

View file

@ -20,3 +20,7 @@ sealed class ClientException(message: String) : Exception(message) {
class Generic(message: String) : ClientException(message) class Generic(message: String) : ClientException(message)
class Other(message: String) : ClientException(message) class Other(message: String) : ClientException(message)
} }
fun ClientException.isNetworkError(): Boolean {
return this is ClientException.Generic && message?.contains("error sending request for url", ignoreCase = true) == true
}

View file

@ -17,9 +17,11 @@
package io.element.android.libraries.matrix.impl.pushers package io.element.android.libraries.matrix.impl.pushers
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.libraries.matrix.impl.exception.mapClientException
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.HttpPusherData import org.matrix.rustcomponents.sdk.HttpPusherData
@ -52,6 +54,7 @@ class RustPushersService(
lang = setHttpPusherData.lang lang = setHttpPusherData.lang
) )
} }
.mapFailure { it.mapClientException() }
} }
} }

View file

@ -18,14 +18,17 @@ package io.element.android.libraries.push.impl
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.PushConfig import io.element.android.appconfig.PushConfig
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient 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.SessionId
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushproviders.api.RegistrationFailure
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import timber.log.Timber import timber.log.Timber
@ -50,7 +53,8 @@ class DefaultPusherSubscriber @Inject constructor(
gateway: String, gateway: String,
): Result<Unit> { ): Result<Unit> {
val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId) val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { val isRegisteringAgain = userDataStore.getCurrentRegisteredPushKey() == pushKey
if (isRegisteringAgain) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.d("Unnecessary to register again the same pusher, but do it in case the pusher has been removed from the server") .d("Unnecessary to register again the same pusher, but do it in case the pusher has been removed from the server")
} }
@ -61,8 +65,14 @@ class DefaultPusherSubscriber @Inject constructor(
.onSuccess { .onSuccess {
userDataStore.setCurrentRegisteredPushKey(pushKey) userDataStore.setCurrentRegisteredPushKey(pushKey)
} }
.onFailure { throwable -> .mapFailure { throwable ->
Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher") Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher")
if (throwable is ClientException) {
// It should always be the case.
RegistrationFailure(throwable, isRegisteringAgain = isRegisteringAgain)
} else {
throwable
}
} }
} }

View file

@ -17,8 +17,21 @@
package io.element.android.libraries.pushproviders.api package io.element.android.libraries.pushproviders.api
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.exception.ClientException
interface PusherSubscriber { interface PusherSubscriber {
/**
* Register a pusher. Note that failure will be a [RegistrationFailure].
*/
suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit> suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit>
/**
* Unregister a pusher.
*/
suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit> suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit>
} }
class RegistrationFailure(
val clientException: ClientException,
val isRegisteringAgain: Boolean
) : Exception(clientException)

View file

@ -135,6 +135,9 @@
<string name="common_encryption_enabled">"Encryption enabled"</string> <string name="common_encryption_enabled">"Encryption enabled"</string>
<string name="common_enter_your_pin">"Enter your PIN"</string> <string name="common_enter_your_pin">"Enter your PIN"</string>
<string name="common_error">"Error"</string> <string name="common_error">"Error"</string>
<string name="common_error_registering_pusher_android">"An error occurred, you may not receive notifications for new messages. Please troubleshoot notifications from the settings.
Reason: %1$s."</string>
<string name="common_everyone">"Everyone"</string> <string name="common_everyone">"Everyone"</string>
<string name="common_failed">"Failed"</string> <string name="common_failed">"Failed"</string>
<string name="common_favourite">"Favourite"</string> <string name="common_favourite">"Favourite"</string>