mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
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>
|
<resources>
|
||||||
<string name="action_mark_as_read">"Mark as read"</string>
|
<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="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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user