Add LocalNetworkPermissionGate with rationale dialog
This commit is contained in:
+44
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+95
@@ -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 <T : Any> present(
|
||||
urlOf: (T) -> String,
|
||||
onProceed: suspend (T) -> Unit,
|
||||
): LocalNetworkPermissionGateState<T> {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val permissionsState = permissionsPresenter.present()
|
||||
var pendingSubmit by remember { mutableStateOf<T?>(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -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<T>(
|
||||
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,
|
||||
}
|
||||
@@ -8,4 +8,6 @@
|
||||
<resources>
|
||||
<string name="action_mark_as_read">"Mark as read"</string>
|
||||
<string name="a11y_jump_to_unread_messages">"Jump to first unread message"</string>
|
||||
<string name="screen_local_network_opt_in_title">Allow access to local network</string>
|
||||
<string name="screen_local_network_opt_in_subtitle">Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user