diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt new file mode 100644 index 0000000000..d93e4fabfc --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt @@ -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 +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 737b5248ec..b28e33dc16 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -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.SessionVerifiedStatus import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.pushproviders.api.RegistrationFailure import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.flow.map import timber.log.Timber @@ -81,9 +82,16 @@ class LoggedInPresenter @Inject constructor( reportCryptoStatusToAnalytics(verificationState, recoveryState) } + fun handleEvent(event: LoggedInEvents) { + when (event) { + LoggedInEvents.CloseErrorDialog -> pusherRegistrationState.value = AsyncData.Uninitialized + } + } + return LoggedInState( showSyncSpinner = showSyncSpinner, pusherRegistrationState = pusherRegistrationState.value, + eventSink = ::handleEvent ) } @@ -122,7 +130,13 @@ class LoggedInPresenter @Inject constructor( }, onFailure = { 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) + } } ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index 2e3db40c9a..fabaff786e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -21,4 +21,5 @@ import io.element.android.libraries.architecture.AsyncData data class LoggedInState( val showSyncSpinner: Boolean, val pusherRegistrationState: AsyncData, + val eventSink: (LoggedInEvents) -> Unit, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index 89f71f384a..bdb0aaacf0 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -22,15 +22,17 @@ import io.element.android.libraries.architecture.AsyncData open class LoggedInStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aLoggedInState(false), - aLoggedInState(true), - // Add other state here + aLoggedInState(), + aLoggedInState(showSyncSpinner = true), + aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoProvidersAvailable())), ) } fun aLoggedInState( - showSyncSpinner: Boolean = true, + showSyncSpinner: Boolean = false, + pusherRegistrationState: AsyncData = AsyncData.Uninitialized, ) = LoggedInState( showSyncSpinner = showSyncSpinner, - pusherRegistrationState = AsyncData.Uninitialized, + pusherRegistrationState = pusherRegistrationState, + eventSink = {}, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 74c71d387c..b997bc0b42 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -22,9 +22,14 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.exception.isNetworkError +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun LoggedInView( @@ -41,6 +46,38 @@ fun LoggedInView( 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 diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/PusherRegistrationFailure.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/PusherRegistrationFailure.kt index 791043c6e2..1fe1009b07 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/PusherRegistrationFailure.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/PusherRegistrationFailure.kt @@ -16,9 +16,19 @@ package io.element.android.appnav.loggedin +import io.element.android.libraries.matrix.api.exception.ClientException + sealed class PusherRegistrationFailure : Exception() { class AccountNotVerified : PusherRegistrationFailure() class NoProvidersAvailable : PusherRegistrationFailure() class NoDistributorsAvailable : PusherRegistrationFailure() - 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() } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 27f44eaeef..aa2f720343 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -389,6 +389,10 @@ class LoggedInPresenterTest { .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java) lambda.assertions() .isNeverCalled() + // Reset the error + finalState.eventSink(LoggedInEvents.CloseErrorDialog) + val lastState = awaitItem() + assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue() } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt index 52dbd2eb12..afe7fca1be 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt @@ -20,3 +20,7 @@ sealed class ClientException(message: String) : Exception(message) { class Generic(message: String) : ClientException(message) class Other(message: String) : ClientException(message) } + +fun ClientException.isNetworkError(): Boolean { + return this is ClientException.Generic && message?.contains("error sending request for url", ignoreCase = true) == true +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt index 2686d03c6b..3274ad17ad 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -17,9 +17,11 @@ package io.element.android.libraries.matrix.impl.pushers import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData +import io.element.android.libraries.matrix.impl.exception.mapClientException import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.HttpPusherData @@ -52,6 +54,7 @@ class RustPushersService( lang = setHttpPusherData.lang ) } + .mapFailure { it.mapClientException() } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt index 481081de1f..da64ebb9ec 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt @@ -18,14 +18,17 @@ package io.element.android.libraries.push.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.PushConfig +import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData import io.element.android.libraries.pushproviders.api.PusherSubscriber +import io.element.android.libraries.pushproviders.api.RegistrationFailure import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber @@ -50,7 +53,8 @@ class DefaultPusherSubscriber @Inject constructor( gateway: String, ): Result { val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId) - if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { + val isRegisteringAgain = userDataStore.getCurrentRegisteredPushKey() == pushKey + if (isRegisteringAgain) { Timber.tag(loggerTag.value) .d("Unnecessary to register again the same pusher, but do it in case the pusher has been removed from the server") } @@ -61,8 +65,14 @@ class DefaultPusherSubscriber @Inject constructor( .onSuccess { userDataStore.setCurrentRegisteredPushKey(pushKey) } - .onFailure { throwable -> + .mapFailure { throwable -> Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher") + if (throwable is ClientException) { + // It should always be the case. + RegistrationFailure(throwable, isRegisteringAgain = isRegisteringAgain) + } else { + throwable + } } } diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt index d38f5dec1e..7a9e49d287 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt @@ -17,8 +17,21 @@ package io.element.android.libraries.pushproviders.api import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.exception.ClientException interface PusherSubscriber { + /** + * Register a pusher. Note that failure will be a [RegistrationFailure]. + */ suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result + + /** + * Unregister a pusher. + */ suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result } + +class RegistrationFailure( + val clientException: ClientException, + val isRegisteringAgain: Boolean +) : Exception(clientException) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 25c2183d0a..fd7f695c58 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -135,6 +135,9 @@ "Encryption enabled" "Enter your PIN" "Error" + "An error occurred, you may not receive notifications for new messages. Please troubleshoot notifications from the settings. + +Reason: %1$s." "Everyone" "Failed" "Favourite"