Address PR review comments

This commit is contained in:
ganfra
2026-07-09 17:22:15 +02:00
parent f25ca5b963
commit 5f4bb30b18
26 changed files with 112 additions and 134 deletions
@@ -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.sync.SyncService
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.permissions.api.LocalNetworkPermissionAdvisor
import io.element.android.libraries.permissions.api.PermissionsEvent import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter 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.PushService
import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@@ -9,6 +9,7 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
data class LoggedInState( data class LoggedInState(
val showSyncSpinner: Boolean, val showSyncSpinner: Boolean,
@@ -19,9 +20,3 @@ data class LoggedInState(
val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
val eventSink: (LoggedInEvents) -> Unit, val eventSink: (LoggedInEvents) -> Unit,
) )
enum class LocalNetworkPermissionDialog {
None,
Rationale,
Settings,
}
@@ -10,6 +10,7 @@ package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.api.PusherRegistrationFailure
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> { open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
@@ -19,13 +19,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import io.element.android.appnav.R import io.element.android.appnav.R
import io.element.android.libraries.architecture.AsyncData 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.ErrorDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain
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.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.matrix.api.exception.isNetworkError 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.push.api.PusherRegistrationFailure
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -84,38 +84,11 @@ fun LoggedInView(
) )
} }
LocalNetworkPermissionDialog(state = state) LocalNetworkPermissionDialogView(
} dialog = state.localNetworkPermissionDialog,
onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) },
@Composable onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) },
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)
},
)
}
} }
private fun Throwable.getReason(): String? { private fun Throwable.getReason(): String? {
@@ -8,8 +8,8 @@
package io.element.android.features.login.impl.changeserver 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.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
data class ChangeServerState( data class ChangeServerState(
val changeServerAction: AsyncData<Unit>, val changeServerAction: AsyncData<Unit>,
@@ -10,8 +10,8 @@ package io.element.android.features.login.impl.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.error.ChangeServerError 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.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> { open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
override val values: Sequence<ChangeServerState> override val values: Sequence<ChangeServerState>
@@ -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.R
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog 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.error.ChangeServerError
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView
import io.element.android.libraries.androidutils.system.openGooglePlay import io.element.android.libraries.androidutils.system.openGooglePlay
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog 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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.LocalBuildMeta 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 import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@@ -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,
)
}
}
@@ -17,9 +17,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject 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.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter 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 import kotlinx.coroutines.launch
@Inject @Inject
@@ -39,13 +40,13 @@ class LocalNetworkPermissionGate(
val permissionsState = permissionsPresenter.present() val permissionsState = permissionsPresenter.present()
var pendingSubmit by remember { mutableStateOf<T?>(null) } var pendingSubmit by remember { mutableStateOf<T?>(null) }
val urlOf by rememberUpdatedState(urlOf) val latestUrlOf by rememberUpdatedState(urlOf)
val onProceed by rememberUpdatedState(onProceed) val latestOnProceed by rememberUpdatedState(onProceed)
LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) { LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) {
val pending = pendingSubmit val pending = pendingSubmit
if (pending != null && permissionsState.permissionGranted) { if (pending != null && permissionsState.permissionGranted) {
coroutineScope.launch { onProceed(pending) } coroutineScope.launch { latestOnProceed(pending) }
pendingSubmit = null pendingSubmit = null
} }
} }
@@ -61,10 +62,10 @@ class LocalNetworkPermissionGate(
fun submit(value: T) { fun submit(value: T) {
coroutineScope.launch { coroutineScope.launch {
if (advisor.shouldRequestPermissionFor(urlOf(value))) { if (advisor.shouldRequestPermissionFor(latestUrlOf(value))) {
pendingSubmit = value pendingSubmit = value
} else { } else {
onProceed(value) latestOnProceed(value)
} }
} }
} }
@@ -7,21 +7,11 @@
package io.element.android.features.login.impl.localnetwork package io.element.android.features.login.impl.localnetwork
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
data class LocalNetworkPermissionGateState<T>( data class LocalNetworkPermissionGateState<T>(
val dialog: LocalNetworkPermissionDialog, val dialog: LocalNetworkPermissionDialog,
val submit: (T) -> Unit, val submit: (T) -> Unit,
val requestPermission: () -> Unit, val requestPermission: () -> Unit,
val abort: () -> 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,
}
@@ -15,7 +15,11 @@ import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.login.impl.error.ChangeServerError 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.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.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.features.login.impl.web.WebClientUrlForAuthenticationRetriever
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter 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.OAuthAction
import io.element.android.libraries.oauth.api.OAuthActionFlow 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 @Inject
class LoginModePresenter( class LoginModePresenter(
private val oAuthActionFlow: OAuthActionFlow, private val oAuthActionFlow: OAuthActionFlow,
@@ -7,9 +7,9 @@
package io.element.android.features.login.impl.login 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.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
data class LoginModeState( data class LoginModeState(
val loginMode: AsyncData<LoginMode>, val loginMode: AsyncData<LoginMode>,
@@ -7,8 +7,8 @@
package io.element.android.features.login.impl.login 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.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
fun aLoginModeState( fun aLoginModeState(
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
@@ -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.ensureProtocol
import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.core.uri.isValidUrl
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker 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.currentCoroutineContext
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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 — // 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 // 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. // candidate directly; the actual sign-in flow triggers the permission prompt.
if (localNetworkPermissionAdvisor.shouldRequestPermissionFor(url)) { val shouldRequestPermissionOrIsValid = localNetworkPermissionAdvisor.shouldRequestPermissionFor(url) ||
currentList.add(HomeserverData(homeserverUrl = url)) homeServerLoginCompatibilityChecker.check(url)
withContext(flowContext) { .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
emit(currentList.toList()) .getOrNull()
} ?: return@parallelMap
return@parallelMap
}
val isValid = homeServerLoginCompatibilityChecker.check(url)
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
.getOrNull()
?: return@parallelMap
// Emit the list as soon as possible // Emit the list as soon as possible
if (isValid) { if (shouldRequestPermissionOrIsValid) {
currentList.add(HomeserverData(homeserverUrl = url)) currentList.add(HomeserverData(homeserverUrl = url))
withContext(flowContext) { withContext(flowContext) {
emit(currentList.toList()) emit(currentList.toList())
@@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.accountprovider.AccountProviderView 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.LoginModeEvent
import io.element.android.features.login.impl.login.LoginModeView import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData 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.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.auth.OAuthDetails 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 import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@@ -35,7 +35,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.impl.R 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.LoginModeEvent
import io.element.android.features.login.impl.login.LoginModeView import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData 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.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.auth.OAuthDetails 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.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R 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.LoginModeEvent
import io.element.android.features.login.impl.login.LoginModeView import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData 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.Button
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OAuthDetails 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.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R 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.LoginModeEvent
import io.element.android.features.login.impl.login.LoginModeView import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData 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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OAuthDetails 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.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -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.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource 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.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.login.impl.localnetwork.LocalNetworkPermissionGate
import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever
import io.element.android.features.wellknown.test.anElementWellKnown 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.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails 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.FakeLocalNetworkPermissionAdvisor
import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
@@ -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.matrix.test.core.aBuildMeta
import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.oauth.api.OAuthActionFlow
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow 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.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.sessionstorage.test.aSessionData
@@ -316,7 +317,7 @@ fun createLoginModePresenter(
oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
localNetworkPermissionAdvisor: io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor = localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor =
io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(), io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(),
permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory = permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory =
io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(), io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(),
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * 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 { interface LocalNetworkPermissionAdvisor {
/** /**
@@ -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,
}
@@ -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,
)
}
@@ -5,16 +5,16 @@
* Please see LICENSE files in the repository root for full details. * 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 android.os.Build
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier
import io.element.android.libraries.androidutils.network.LocalNetworkClassification 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.PermissionStateProvider
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@@ -25,7 +25,7 @@ class DefaultLocalNetworkPermissionAdvisor(
) : LocalNetworkPermissionAdvisor { ) : LocalNetworkPermissionAdvisor {
override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean { override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean {
if (!buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.CINNAMON_BUN)) return false 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)) { return when (classifier.classify(homeserverUrl)) {
LocalNetworkClassification.LocalIp -> true LocalNetworkClassification.LocalIp -> true
LocalNetworkClassification.PublicIp, LocalNetworkClassification.PublicIp,
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * 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 com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier
@@ -7,7 +7,7 @@
package io.element.android.libraries.permissions.test 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( class FakeLocalNetworkPermissionAdvisor(
private val shouldPrompt: Boolean = false, private val shouldPrompt: Boolean = false,