From c2e8912fe209753c59647b044c35787edff5b154 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:48 +0200 Subject: [PATCH] Add LocalNetworkPermissionGate with rationale dialog --- .../LocalNetworkPermissionDialogView.kt | 44 +++++++++ .../LocalNetworkPermissionGate.kt | 95 +++++++++++++++++++ .../LocalNetworkPermissionGateState.kt | 27 ++++++ .../src/main/res/values/temporary.xml | 2 + 4 files changed, 168 insertions(+) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt 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 new file mode 100644 index 0000000000..216b84fb11 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt @@ -0,0 +1,44 @@ +/* + * 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 new file mode 100644 index 0000000000..857a8f9287 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt @@ -0,0 +1,95 @@ +/* + * 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 android.Manifest +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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 kotlinx.coroutines.launch + +@Inject +class LocalNetworkPermissionGate( + private val advisor: LocalNetworkPermissionAdvisor, + permissionsPresenterFactory: PermissionsPresenter.Factory, +) { + private val permissionsPresenter: PermissionsPresenter = + permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK) + + @Composable + fun present( + urlOf: (T) -> String, + onProceed: suspend (T) -> Unit, + ): LocalNetworkPermissionGateState { + val coroutineScope = rememberCoroutineScope() + val permissionsState = permissionsPresenter.present() + var pendingSubmit by remember { mutableStateOf(null) } + + val urlOf by rememberUpdatedState(urlOf) + val onProceed by rememberUpdatedState(onProceed) + + LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) { + val pending = pendingSubmit + if (pending != null && permissionsState.permissionGranted) { + coroutineScope.launch { onProceed(pending) } + pendingSubmit = null + } + } + + val dialog by rememberUpdatedState( + when { + pendingSubmit == null -> LocalNetworkPermissionDialog.None + permissionsState.permissionGranted -> LocalNetworkPermissionDialog.None + permissionsState.shouldShowRationale || !permissionsState.permissionAlreadyAsked -> LocalNetworkPermissionDialog.Rationale + else -> LocalNetworkPermissionDialog.Settings + } + ) + + fun submit(value: T) { + coroutineScope.launch { + if (advisor.shouldRequestPermissionFor(urlOf(value))) { + pendingSubmit = value + } else { + onProceed(value) + } + } + } + + fun requestPermission() { + when (dialog) { + LocalNetworkPermissionDialog.Settings -> { + permissionsState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog) + } + LocalNetworkPermissionDialog.Rationale -> { + permissionsState.eventSink(PermissionsEvent.ForceRequestPermissions) + } + else -> Unit + } + } + + fun abort() { + pendingSubmit = null + } + + return LocalNetworkPermissionGateState( + dialog = dialog, + submit = ::submit, + requestPermission = ::requestPermission, + abort = ::abort, + ) + } +} 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 new file mode 100644 index 0000000000..3213a96cd8 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt @@ -0,0 +1,27 @@ +/* + * 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 + +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/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml index fd1edb0759..a192e2c9df 100644 --- a/libraries/ui-strings/src/main/res/values/temporary.xml +++ b/libraries/ui-strings/src/main/res/values/temporary.xml @@ -8,4 +8,6 @@ "Mark as read" "Jump to first unread message" + Allow access to local network + Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network.