From 5f4bb30b18a3f0f45bfe001e3bf9caba37bdb097 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Jul 2026 17:22:15 +0200 Subject: [PATCH] Address PR review comments --- .../appnav/loggedin/LoggedInPresenter.kt | 3 +- .../android/appnav/loggedin/LoggedInState.kt | 7 +-- .../appnav/loggedin/LoggedInStateProvider.kt | 1 + .../android/appnav/loggedin/LoggedInView.kt | 39 +++------------- .../impl/changeserver/ChangeServerState.kt | 2 +- .../changeserver/ChangeServerStateProvider.kt | 2 +- .../impl/changeserver/ChangeServerView.kt | 2 +- .../LocalNetworkPermissionDialogView.kt | 44 ------------------- .../LocalNetworkPermissionGate.kt | 13 +++--- .../LocalNetworkPermissionGateState.kt | 14 +----- .../login/impl/login/LoginModePresenter.kt | 10 +++++ .../login/impl/login/LoginModeState.kt | 2 +- .../impl/login/LoginModeStateProvider.kt | 2 +- .../login/impl/resolver/HomeserverResolver.kt | 21 +++------ .../ChooseAccountProviderView.kt | 2 +- .../loginwithclassic/LoginWithClassicView.kt | 2 +- .../ConfirmAccountProviderView.kt | 2 +- .../impl/screens/onboarding/OnBoardingView.kt | 2 +- .../changeserver/ChangeServerPresenterTest.kt | 2 +- .../onboarding/OnBoardingPresenterTest.kt | 3 +- .../LocalNetworkPermissionAdvisor.kt | 2 +- .../LocalNetworkPermissionDialog.kt | 20 +++++++++ .../LocalNetworkPermissionDialogView.kt | 37 ++++++++++++++++ .../DefaultLocalNetworkPermissionAdvisor.kt | 8 ++-- ...efaultLocalNetworkPermissionAdvisorTest.kt | 2 +- .../test/FakeLocalNetworkPermissionAdvisor.kt | 2 +- 26 files changed, 112 insertions(+), 134 deletions(-) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt rename libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/{ => localnetwork}/LocalNetworkPermissionAdvisor.kt (87%) create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt rename libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/{ => localnetwork}/DefaultLocalNetworkPermissionAdvisor.kt (83%) rename libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/{ => localnetwork}/DefaultLocalNetworkPermissionAdvisorTest.kt (98%) 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 fab9797c76..0723ad4606 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,9 +38,10 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion 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.permissions.api.LocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.api.PermissionsEvent import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.services.analytics.api.AnalyticsService 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 fdfff9bf75..edc12383bf 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 @@ -9,6 +9,7 @@ package io.element.android.appnav.loggedin import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class LoggedInState( val showSyncSpinner: Boolean, @@ -19,9 +20,3 @@ data class LoggedInState( val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (LoggedInEvents) -> Unit, ) - -enum class LocalNetworkPermissionDialog { - None, - Rationale, - Settings, -} 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 afe09ec78b..67fcb95c2c 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 @@ -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.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.push.api.PusherRegistrationFailure open class LoggedInStateProvider : PreviewParameterProvider { 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 198a731e49..e57d378da4 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 @@ -19,13 +19,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.Lifecycle import io.element.android.appnav.R import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain 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.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.ui.strings.CommonStrings @@ -84,38 +84,11 @@ fun LoggedInView( ) } - LocalNetworkPermissionDialog(state = state) -} - -@Composable -private fun LocalNetworkPermissionDialog(state: LoggedInState) { - when (state.localNetworkPermissionDialog) { - LocalNetworkPermissionDialog.None -> Unit - LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.dialog_allow_access), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = { - state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) - }, - onDismiss = { - state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) - }, - ) - LocalNetworkPermissionDialog.Settings -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.action_open_settings), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = { - state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) - }, - onDismiss = { - state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) - }, - ) - } + LocalNetworkPermissionDialogView( + dialog = state.localNetworkPermissionDialog, + onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) }, + onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) }, + ) } private fun Throwable.getReason(): String? { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt index a3467b21ba..6d8ce25d75 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt @@ -8,8 +8,8 @@ package io.element.android.features.login.impl.changeserver -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class ChangeServerState( val changeServerAction: AsyncData, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt index 693bca34bd..183158bceb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt @@ -10,8 +10,8 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog open class ChangeServerStateProvider : PreviewParameterProvider { override val values: Sequence diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt index 15b987ce16..30ae644a70 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.login.impl.R import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.androidutils.system.openGooglePlay import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.ProgressDialog @@ -28,6 +27,7 @@ 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.designsystem.theme.LocalBuildMeta +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt deleted file mode 100644 index 216b84fb11..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2026 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.features.login.impl.localnetwork - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.ui.strings.CommonStrings - -@Composable -fun LocalNetworkPermissionDialogView( - dialog: LocalNetworkPermissionDialog, - onSubmit: () -> Unit, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, -) { - when (dialog) { - LocalNetworkPermissionDialog.None -> Unit - LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.dialog_allow_access), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = onSubmit, - onDismiss = onDismiss, - modifier = modifier, - ) - LocalNetworkPermissionDialog.Settings -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.action_open_settings), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = onSubmit, - onDismiss = onDismiss, - modifier = modifier, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt index 857a8f9287..37a97e71dd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt @@ -17,9 +17,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.api.PermissionsEvent import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import kotlinx.coroutines.launch @Inject @@ -39,13 +40,13 @@ class LocalNetworkPermissionGate( val permissionsState = permissionsPresenter.present() var pendingSubmit by remember { mutableStateOf(null) } - val urlOf by rememberUpdatedState(urlOf) - val onProceed by rememberUpdatedState(onProceed) + val latestUrlOf by rememberUpdatedState(urlOf) + val latestOnProceed by rememberUpdatedState(onProceed) LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) { val pending = pendingSubmit if (pending != null && permissionsState.permissionGranted) { - coroutineScope.launch { onProceed(pending) } + coroutineScope.launch { latestOnProceed(pending) } pendingSubmit = null } } @@ -61,10 +62,10 @@ class LocalNetworkPermissionGate( fun submit(value: T) { coroutineScope.launch { - if (advisor.shouldRequestPermissionFor(urlOf(value))) { + if (advisor.shouldRequestPermissionFor(latestUrlOf(value))) { pendingSubmit = value } else { - onProceed(value) + latestOnProceed(value) } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt index 3213a96cd8..f3ecee729e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt @@ -7,21 +7,11 @@ package io.element.android.features.login.impl.localnetwork +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog + data class LocalNetworkPermissionGateState( val dialog: LocalNetworkPermissionDialog, val submit: (T) -> Unit, val requestPermission: () -> Unit, val abort: () -> Unit, ) - -/** - * Which rationale dialog (if any) should be rendered on top of the caller's UI. - * - * [Rationale] is shown before the runtime prompt so the user knows why the permission is needed. - * [Settings] is shown when the OS says a rationale can no longer be shown (permanently denied). - */ -enum class LocalNetworkPermissionDialog { - None, - Rationale, - Settings, -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt index 43c03bfbe3..34188095b0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt @@ -15,7 +15,11 @@ import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter +import io.element.android.features.login.impl.screens.classic.loginwithclassic.LoginWithClassicPresenter +import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -25,6 +29,12 @@ import io.element.android.libraries.matrix.api.auth.OAuthPrompt import io.element.android.libraries.oauth.api.OAuthAction import io.element.android.libraries.oauth.api.OAuthActionFlow +/** + * Presenter responsible for managing the login flow, including handling OAuth actions and + * submitting login requests. + * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter], + * [ChooseAccountProviderPresenter] and [LoginWithClassicPresenter]. + */ @Inject class LoginModePresenter( private val oAuthActionFlow: OAuthActionFlow, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt index 3ca3a42a5d..849ff31f85 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt @@ -7,9 +7,9 @@ package io.element.android.features.login.impl.login -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class LoginModeState( val loginMode: AsyncData, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt index a0adc327fe..1d33a1c48a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt @@ -7,8 +7,8 @@ package io.element.android.features.login.impl.login -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog fun aLoginModeState( loginMode: AsyncData = AsyncData.Uninitialized, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt index b6eb26c87f..8c6cab79ce 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -44,21 +44,14 @@ class HomeserverResolver( // Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first — // otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a // candidate directly; the actual sign-in flow triggers the permission prompt. - if (localNetworkPermissionAdvisor.shouldRequestPermissionFor(url)) { - currentList.add(HomeserverData(homeserverUrl = url)) - withContext(flowContext) { - emit(currentList.toList()) - } - return@parallelMap - } - - val isValid = homeServerLoginCompatibilityChecker.check(url) - .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } - .getOrNull() - ?: return@parallelMap + val shouldRequestPermissionOrIsValid = localNetworkPermissionAdvisor.shouldRequestPermissionFor(url) || + homeServerLoginCompatibilityChecker.check(url) + .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } + .getOrNull() + ?: return@parallelMap // Emit the list as soon as possible - if (isValid) { + if (shouldRequestPermissionOrIsValid) { currentList.add(HomeserverData(homeserverUrl = url)) withContext(flowContext) { emit(currentList.toList()) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index be37775965..17e7d10efb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.AccountProviderView -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -46,6 +45,7 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt index 389d07ea1a..75181a068e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -52,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index 3db0748a04..95fbff25ab 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -33,6 +32,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 74fdbffab1..be86bbee9c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -52,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 02ee9d5042..5d9040adac 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -15,7 +15,6 @@ import io.element.android.features.login.impl.accesscontrol.DefaultAccountProvid import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.anElementWellKnown @@ -25,6 +24,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 56ccc230b0..79268aa1e4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -316,7 +317,7 @@ fun createLoginModePresenter( oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), - localNetworkPermissionAdvisor: io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor = + localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor = io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(), permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory = io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(), diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt similarity index 87% rename from libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt rename to libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt index 7ed46a63e1..9863dbb751 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.permissions.api +package io.element.android.libraries.permissions.api.localnetwork interface LocalNetworkPermissionAdvisor { /** diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt new file mode 100644 index 0000000000..68a440680f --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 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.permissions.api.localnetwork + +/** + * Which rationale dialog (if any) should be rendered on top of the caller's UI. + * + * [Rationale] is shown before the runtime prompt so the user knows why the permission is needed. + * [Settings] is shown when the OS says a rationale can no longer be shown (permanently denied). + */ +enum class LocalNetworkPermissionDialog { + None, + Rationale, + Settings, +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt new file mode 100644 index 0000000000..d2337fdf5b --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 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.permissions.api.localnetwork + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LocalNetworkPermissionDialogView( + dialog: LocalNetworkPermissionDialog, + onSubmit: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val submitTextRes = when (dialog) { + LocalNetworkPermissionDialog.None -> return + LocalNetworkPermissionDialog.Rationale -> CommonStrings.dialog_allow_access + LocalNetworkPermissionDialog.Settings -> CommonStrings.action_open_settings + } + ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(submitTextRes), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = onSubmit, + onDismiss = onDismiss, + modifier = modifier, + ) +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt similarity index 83% rename from libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt rename to libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt index a34ece0859..a1f4022c3c 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt @@ -5,16 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.permissions.impl +package io.element.android.libraries.permissions.impl.localnetwork -import android.Manifest.permission.ACCESS_LOCAL_NETWORK +import android.Manifest import android.os.Build import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier import io.element.android.libraries.androidutils.network.LocalNetworkClassification -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider @ContributesBinding(AppScope::class) @@ -25,7 +25,7 @@ class DefaultLocalNetworkPermissionAdvisor( ) : LocalNetworkPermissionAdvisor { override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean { if (!buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.CINNAMON_BUN)) return false - if (permissionStateProvider.isPermissionGranted(ACCESS_LOCAL_NETWORK)) return false + if (permissionStateProvider.isPermissionGranted(Manifest.permission.ACCESS_LOCAL_NETWORK)) return false return when (classifier.classify(homeserverUrl)) { LocalNetworkClassification.LocalIp -> true LocalNetworkClassification.PublicIp, diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt similarity index 98% rename from libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt rename to libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt index 2ed1dd7548..1609ee1f20 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.permissions.impl +package io.element.android.libraries.permissions.impl.localnetwork import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt index 1a3363d6dc..f16872441c 100644 --- a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.permissions.test -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor class FakeLocalNetworkPermissionAdvisor( private val shouldPrompt: Boolean = false,