From a9a85bebf72170aff14618940eb1a70173bdef2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Zag=C3=B3rski?= Date: Mon, 16 Dec 2024 14:16:34 +0100 Subject: [PATCH 1/2] feat: Custom server no network dialog #WPB-11627 (#3543) --- .../com/wire/android/ui/WireActivity.kt | 18 +++++----- .../wire/android/ui/WireActivityDialogs.kt | 13 +++++--- .../wire/android/ui/WireActivityViewModel.kt | 21 +++++++----- ...alog.kt => CustomServerNoNetworkDialog.kt} | 33 ++++++++++++------- app/src/main/res/values-hu/strings.xml | 3 +- app/src/main/res/values-ru/strings.xml | 3 +- app/src/main/res/values/strings.xml | 5 +-- .../android/ui/WireActivityViewModelTest.kt | 16 ++++----- 8 files changed, 63 insertions(+), 49 deletions(-) rename app/src/main/kotlin/com/wire/android/ui/common/dialogs/{CustomServerInvalidJsonDialog.kt => CustomServerNoNetworkDialog.kt} (59%) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index a95911f1af0..54d9e7ff6d6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -517,16 +517,14 @@ class WireActivity : AppCompatActivity() { ) CustomBackendDialog( viewModel.globalAppState, - viewModel::dismissCustomBackendDialog - ) { - viewModel.customBackendDialogProceedButtonClicked { - navigate( - NavigationCommand( - WelcomeScreenDestination - ) - ) - } - } + viewModel::dismissCustomBackendDialog, + onConfirm = { + viewModel.customBackendDialogProceedButtonClicked { + navigate(NavigationCommand(WelcomeScreenDestination)) + } + }, + onTryAgain = viewModel::onCustomServerConfig + ) MaxAccountDialog( shouldShow = viewModel.globalAppState.maxAccountDialog, onConfirm = { diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt index 8b8ee6e66b3..564950de627 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt @@ -57,8 +57,8 @@ import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dialogs.CustomServerDetailsDialog import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState -import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialog -import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialogState +import com.wire.android.ui.common.dialogs.CustomServerNoNetworkDialog +import com.wire.android.ui.common.dialogs.CustomServerNoNetworkDialogState import com.wire.android.ui.common.dialogs.MaxAccountAllowedDialogContent import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.wireDialogPropertiesBuilder @@ -244,7 +244,8 @@ fun JoinConversationDialog( fun CustomBackendDialog( globalAppState: GlobalAppState, onDismiss: () -> Unit, - onConfirm: () -> Unit + onConfirm: () -> Unit, + onTryAgain: (String) -> Unit ) { when (globalAppState.customBackendDialog) { is CustomServerDetailsDialogState -> { @@ -255,8 +256,9 @@ fun CustomBackendDialog( ) } - is CustomServerInvalidJsonDialogState -> { - CustomServerInvalidJsonDialog( + is CustomServerNoNetworkDialogState -> { + CustomServerNoNetworkDialog( + onTryAgain = { onTryAgain(globalAppState.customBackendDialog.customServerUrl) }, onDismiss = onDismiss ) } @@ -581,6 +583,7 @@ fun PreviewCustomBackendDialog() { ) ), {}, + {}, {} ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index d4ee4acfd2b..fc173d079cb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -42,7 +42,7 @@ import com.wire.android.services.ServicesManager import com.wire.android.ui.authentication.devices.model.displayName import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState import com.wire.android.ui.common.dialogs.CustomServerDialogState -import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialogState +import com.wire.android.ui.common.dialogs.CustomServerNoNetworkDialogState import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.theme.ThemeOption import com.wire.android.util.CurrentScreen @@ -320,7 +320,7 @@ class WireActivityViewModel @Inject constructor( when (val result = deepLinkProcessor.get().invoke(intent?.data, isSharingIntent)) { DeepLinkResult.AuthorizationNeeded -> onAuthorizationNeeded() is DeepLinkResult.SSOLogin -> onSSOLogin(result) - is DeepLinkResult.CustomServerConfig -> onCustomServerConfig(result) + is DeepLinkResult.CustomServerConfig -> onCustomServerConfig(result.url) is DeepLinkResult.Failure.OngoingCall -> onCannotLoginDuringACall() is DeepLinkResult.Failure.Unknown -> appLogger.e("unknown deeplink failure") is DeepLinkResult.JoinConversation -> onConversationInviteDeepLink( @@ -429,13 +429,16 @@ class WireActivityViewModel @Inject constructor( } } - private suspend fun onCustomServerConfig(result: DeepLinkResult.CustomServerConfig) { - val customBackendDialogData = loadServerConfig(result.url)?.let { serverLinks -> - CustomServerDetailsDialogState(serverLinks = serverLinks) - } ?: CustomServerInvalidJsonDialogState - globalAppState = globalAppState.copy( - customBackendDialog = customBackendDialogData - ) + fun onCustomServerConfig(customServerUrl: String) { + viewModelScope.launch(dispatchers.io()) { + val customBackendDialogData = loadServerConfig(customServerUrl) + ?.let { serverLinks -> CustomServerDetailsDialogState(serverLinks = serverLinks) } + ?: CustomServerNoNetworkDialogState(customServerUrl) + + globalAppState = globalAppState.copy( + customBackendDialog = customBackendDialogData + ) + } } private suspend fun onConversationInviteDeepLink( diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerInvalidJsonDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerNoNetworkDialog.kt similarity index 59% rename from app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerInvalidJsonDialog.kt rename to app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerNoNetworkDialog.kt index 356d9b12f17..1ad8d752f0b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerInvalidJsonDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerNoNetworkDialog.kt @@ -28,29 +28,40 @@ import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes @Composable -internal fun CustomServerInvalidJsonDialog( +internal fun CustomServerNoNetworkDialog( + onTryAgain: () -> Unit, onDismiss: () -> Unit ) { WireDialog( - title = stringResource(R.string.custom_backend_invalid_deeplink_data_title), - text = stringResource(R.string.custom_backend_invalid_deeplink_data_body), + title = stringResource(R.string.custom_backend_error_title), + text = stringResource(R.string.custom_backend_error_no_internet_connection_body), onDismiss = onDismiss, + buttonsHorizontalAlignment = false, optionButton1Properties = WireDialogButtonProperties( - onClick = onDismiss, - text = stringResource(id = R.string.label_ok), + onClick = { + onTryAgain() + onDismiss() + }, + text = stringResource(id = R.string.custom_backend_error_no_internet_connection_try_again), type = WireDialogButtonType.Primary, - state = - WireButtonState.Default + state = WireButtonState.Default ), + optionButton2Properties = WireDialogButtonProperties( + onClick = onDismiss, + text = stringResource(id = R.string.label_cancel), + type = WireDialogButtonType.Secondary, + state = WireButtonState.Default + ) ) } -data object CustomServerInvalidJsonDialogState : CustomServerDialogState() +data class CustomServerNoNetworkDialogState(val customServerUrl: String) : CustomServerDialogState() @PreviewMultipleThemes @Composable -fun PreviewCustomServerInvalidJsonDialog() = WireTheme { - CustomServerInvalidJsonDialog( - onDismiss = { } +fun PreviewCustomServerNoNetworkDialog() = WireTheme { + CustomServerNoNetworkDialog( + onTryAgain = {}, + onDismiss = {} ) } diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ff5e1b7e98a..57ed2e7001d 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1133,8 +1133,7 @@ Ez a beállítás az összes beszélgetésre érvényes ezen az eszközön.Fiókok URL: Honlap URL: Kiszolgáló WSURL: - Hiba történt - A saját kiszolgálóra történő átirányítás nem volt lehetséges, mivel a JSON fájl érvénytelen beállítást tartalmazott.\n\nLépjen kapcsolatba a rendszergazdával, vagy ellenőrizze a mélylinket, ami ide vezette. + Hiba történt Új üzenetek lekérdezése Szöveg a vágólapra másolva Naplók diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8dfcce63cb6..99ecf9a8a1b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1171,8 +1171,7 @@ URL аккаунта: URL веб-сайта: WSURL бэкэнда: - Произошла ошибка - Перенаправление на локальный бэкэнд было неудачным, поскольку в JSON-файле была неверная конфигурация.\n\nСвяжитесь с администратором или проверьте ссылку, которая привела вас сюда. + Произошла ошибка Получение новых сообщений Текст скопирован в буфер обмена Журналы diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 718e9d41961..4811da7ef86 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1183,8 +1183,9 @@ In group conversations, the group admin can overwrite this setting. Accounts URL: Website URL: Backend WSURL: - An error occurred - Redirecting to an on-premises backend was not possible, as there was an invalid configuration in the JSON file.\n\nContact your admin or check the deeplink that brought you here. + An error occurred + Redirecting to an on-premises backend was not possible, you don’t seem to be connected to the internet.\n\nEstablish an internet connection and try again. + Try again Receiving new messages Text copied to clipboard Logs diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 6169a00d52f..f7070a642bf 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -38,7 +38,7 @@ import com.wire.android.framework.TestUser import com.wire.android.migration.MigrationManager import com.wire.android.services.ServicesManager import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState -import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialogState +import com.wire.android.ui.common.dialogs.CustomServerNoNetworkDialogState import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModelTest import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.theme.ThemeOption @@ -141,13 +141,13 @@ class WireActivityViewModelTest { } @Test - fun `given Intent with malformed ServerConfig json, when currentSessions is present, then initialAppState is LOGGED_IN and customBackEndInvalidJson dialog is shown`() = + fun `given intent with correct ServerConfig json, when no network is present, then initialAppState is LOGGED_IN and no network dialog is shown`() = runTest { val result = DeepLinkResult.CustomServerConfig("url") val (arrangement, viewModel) = Arrangement() .withSomeCurrentSession() .withDeepLinkResult(result) - .withMalformedServerJson() + .withNoNetworkConnectionWhenGettingServerConfig() .withNoOngoingCall() .arrange() @@ -155,17 +155,17 @@ class WireActivityViewModelTest { assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState()) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } - assertInstanceOf(CustomServerInvalidJsonDialogState::class.java, viewModel.globalAppState.customBackendDialog) + assertInstanceOf(CustomServerNoNetworkDialogState::class.java, viewModel.globalAppState.customBackendDialog) } @Test - fun `given Intent with malformed ServerConfig json, when currentSessions is present, then initialAppState is NOT_LOGGED_IN and customBackEndInvalidJson dialog is shown`() = + fun `given Intent with malformed ServerConfig json, when currentSessions is absent, then initialAppState is NOT_LOGGED_IN and no network dialog is shown`() = runTest { val result = DeepLinkResult.CustomServerConfig("url") val (arrangement, viewModel) = Arrangement() .withNoCurrentSession() .withDeepLinkResult(result) - .withMalformedServerJson() + .withNoNetworkConnectionWhenGettingServerConfig() .withNoOngoingCall() .arrange() @@ -173,7 +173,7 @@ class WireActivityViewModelTest { assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState()) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } - assertInstanceOf(CustomServerInvalidJsonDialogState::class.java, viewModel.globalAppState.customBackendDialog) + assertInstanceOf(CustomServerNoNetworkDialogState::class.java, viewModel.globalAppState.customBackendDialog) } @Test @@ -919,7 +919,7 @@ class WireActivityViewModelTest { coEvery { coreLogic.getSessionScope(TEST_ACCOUNT_INFO.userId).observeIfE2EIRequiredDuringLogin() } returns flowOf(false) } - fun withMalformedServerJson() = apply { + fun withNoNetworkConnectionWhenGettingServerConfig() = apply { coEvery { getServerConfigUseCase(any()) } returns GetServerConfigResult.Failure.Generic(NetworkFailure.NoNetworkConnection(null)) } From 7a53935227c43625454eddaf1439cc96b80eeab9 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Mon, 16 Dec 2024 10:59:00 -0300 Subject: [PATCH 2/2] fix: harden countly sdk integration (WPB-15007) (#3746) --- .../AnonymousAnalyticsRecorderImpl.kt | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt b/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt index 0bb72c5545d..71fe3723667 100644 --- a/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt +++ b/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt @@ -20,6 +20,7 @@ package com.wire.android.feature.analytics import android.app.Activity import android.app.Application import android.content.Context +import android.util.Log import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.feature.analytics.model.AnalyticsEventConstants import com.wire.android.feature.analytics.model.AnalyticsSettings @@ -34,8 +35,8 @@ class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder { override fun configure( context: Context, analyticsSettings: AnalyticsSettings - ) { - if (isConfigured) return + ) = wrapCountlyRequest { + if (isConfigured) return@wrapCountlyRequest val countlyConfig = CountlyConfig( context, @@ -54,24 +55,24 @@ class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder { } } - Countly.sharedInstance().init(countlyConfig) - Countly.sharedInstance().consent().giveConsent(arrayOf("apm")) + Countly.sharedInstance()?.init(countlyConfig) + Countly.sharedInstance()?.consent()?.giveConsent(arrayOf("apm")) val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val globalSegmentations = mapOf( AnalyticsEventConstants.APP_NAME to AnalyticsEventConstants.APP_NAME_ANDROID, AnalyticsEventConstants.APP_VERSION to packageInfo.versionName ) - Countly.sharedInstance().views().setGlobalViewSegmentation(globalSegmentations) + Countly.sharedInstance()?.views()?.setGlobalViewSegmentation(globalSegmentations) isConfigured = true } - override fun onStart(activity: Activity) { - Countly.sharedInstance().onStart(activity) + override fun onStart(activity: Activity) = wrapCountlyRequest { + Countly.sharedInstance()?.onStart(activity) } - override fun onStop() { - Countly.sharedInstance().onStop() + override fun onStop() = wrapCountlyRequest { + Countly.sharedInstance()?.onStop() } /** @@ -79,11 +80,11 @@ class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder { * Countly is doing additional operations on it. * See [UtilsInternalLimits.removeUnsupportedDataTypes] */ - override fun sendEvent(event: AnalyticsEvent) { - Countly.sharedInstance().events().recordEvent(event.key, event.toSegmentation().toMutableMap()) + override fun sendEvent(event: AnalyticsEvent) = wrapCountlyRequest { + Countly.sharedInstance()?.events()?.recordEvent(event.key, event.toSegmentation().toMutableMap()) } - override fun halt() { + override fun halt() = wrapCountlyRequest { isConfigured = false Countly.sharedInstance().consent().removeConsentAll() } @@ -93,7 +94,9 @@ class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder { isTeamMember: Boolean, migrationComplete: suspend () -> Unit ) { - Countly.sharedInstance().deviceId().changeWithMerge(identifier).also { + wrapCountlyRequest { + Countly.sharedInstance()?.deviceId()?.changeWithMerge(identifier) + }.also { migrationComplete() } @@ -106,7 +109,9 @@ class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder { isTeamMember: Boolean, propagateIdentifier: suspend () -> Unit ) { - Countly.sharedInstance().deviceId().changeWithoutMerge(identifier) + wrapCountlyRequest { + Countly.sharedInstance()?.deviceId()?.changeWithoutMerge(identifier) + } setUserProfileProperties(isTeamMember = isTeamMember) @@ -115,27 +120,42 @@ class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder { } } - private fun setUserProfileProperties(isTeamMember: Boolean) { - Countly.sharedInstance().userProfile().setProperty( + private fun setUserProfileProperties(isTeamMember: Boolean) = wrapCountlyRequest { + Countly.sharedInstance()?.userProfile()?.setProperty( AnalyticsEventConstants.TEAM_IS_TEAM, isTeamMember ) - Countly.sharedInstance().userProfile().save() + Countly.sharedInstance()?.userProfile()?.save() } override fun isAnalyticsInitialized(): Boolean = Countly.sharedInstance().isInitialized - override fun applicationOnCreate() { - if (isConfigured) return + override fun applicationOnCreate() = wrapCountlyRequest { + if (isConfigured) return@wrapCountlyRequest Countly.applicationOnCreate() } - override fun recordView(screen: String) { - Countly.sharedInstance().views().startAutoStoppedView(screen) + override fun recordView(screen: String) = wrapCountlyRequest { + Countly.sharedInstance()?.views()?.startAutoStoppedView(screen) + } + + override fun stopView(screen: String) = wrapCountlyRequest { + Countly.sharedInstance()?.views()?.stopViewWithName(screen) + } + + @Suppress("TooGenericExceptionCaught") + private fun wrapCountlyRequest(block: () -> Unit) { + try { + block() + } catch (e: Exception) { + // Countly SDK throws exceptions on some cases, just log it + // We don't want to crash the app because of that. + Log.wtf(TAG, "Countly SDK request failed", e) + } } - override fun stopView(screen: String) { - Countly.sharedInstance().views().stopViewWithName(screen) + companion object { + private const val TAG = "AnonymousAnalyticsRecorderImpl" } }