diff options
Diffstat (limited to 'Source/Android/app/src/main/java')
| -rw-r--r-- | Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt | 136 |
1 files changed, 131 insertions, 5 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt index ae222e8722..8b532b22de 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt @@ -1,9 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) package org.dolphinemu.dolphinemu.features.netplay.ui +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -19,6 +24,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator @@ -42,13 +48,22 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -58,6 +73,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType import org.dolphinemu.dolphinemu.ui.theme.DolphinScaffold import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer +import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox private data class ErrorDialogState(val message: String) { val onDismissed = CompletableDeferred<Unit>() @@ -87,6 +103,23 @@ fun NetplaySetupScreen( onHostClicked: () -> Unit, onConnectClicked: () -> Unit, ) { + val localNetworkPermissionState: PermissionState? = + if ( + connectionType is ConnectionType.DirectConnection && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN + ) { + rememberPermissionState(Manifest.permission.ACCESS_LOCAL_NETWORK) { _ -> + // Try to connect even if the user denied the permission. The warning is on screen + // so it's up to them to determine if they are using local network and need to grant it. + when (connectionRole) { + ConnectionRole.Host -> onHostClicked() + ConnectionRole.Connect -> onConnectClicked() + } + } + } else { + null + } + DolphinScaffold( title = { Text(stringResource(R.string.netplay_setup_title)) }, navigationIcon = { @@ -99,9 +132,19 @@ fun NetplaySetupScreen( }, floatingActionButton = { ExtendedFloatingActionButton( - onClick = when(connectionRole) { - ConnectionRole.Host -> onHostClicked - ConnectionRole.Connect -> onConnectClicked + onClick = { + if ( + localNetworkPermissionState != null && + !localNetworkPermissionState.status.isGranted && + !localNetworkPermissionState.status.shouldShowRationale + ) { + localNetworkPermissionState.launchPermissionRequest() + } else { + when (connectionRole) { + ConnectionRole.Host -> onHostClicked() + ConnectionRole.Connect -> onConnectClicked() + } + } }, ) { if (connecting) { @@ -238,6 +281,10 @@ private fun NetplaySetupContent( onUseUpnpChanged = onUseUpnpChanged, ) } + + LocalNetworkPermissionInfo( + connectionType = connectionType, + ) } @Composable @@ -374,8 +421,87 @@ private fun HostMenu( ) } } + } +} + +@Composable +private fun LocalNetworkPermissionInfo( + connectionType: ConnectionType, +) { + if ( + connectionType is ConnectionType.DirectConnection && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN + ) { + val context = LocalContext.current + + // Android permissions do not differentiate between isGranted being false because the user + // has never been asked and isGranted being false because the user denied twice. We only + // get shouldShowRationale which, if the user keeps denying goes false, true, always false. + // So if the value does not change after calling launchPermissionRequest we know we are in + // the final denied state and launch app settings for the user to manually grant from there. + var previousShouldShowRationale by rememberSaveable { mutableStateOf(false) } - MenuSpacer() + // Check if the permission dialog actually showed by detecting an onPause event, this allows + // us to detect if the dialog was dismissed with the back button which would prevent + // previousShouldShowRationale from toggling as described above. + var dialogShown by rememberSaveable { mutableStateOf(false) } + LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) { dialogShown = true } + + lateinit var permissionState: PermissionState + permissionState = + rememberPermissionState(Manifest.permission.ACCESS_LOCAL_NETWORK) { granted -> + if ( + !granted && + !dialogShown && + previousShouldShowRationale == permissionState.status.shouldShowRationale + ) { + context.startActivity( + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ) + ) + } + previousShouldShowRationale = permissionState.status.shouldShowRationale + } + + if (!permissionState.status.isGranted) { + MenuSpacer() + + OutlinedBox( + label = { + Icon( + imageVector = Icons.Default.Warning, + tint = MaterialTheme.colorScheme.error, + contentDescription = null, + ) + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(R.string.netplay_local_network_permission_info), + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = { + previousShouldShowRationale = permissionState.status.shouldShowRationale + dialogShown = false + permissionState.launchPermissionRequest() + }, + modifier = Modifier.align(Alignment.End) + ) { + Text(text = stringResource(R.string.netplay_give_permission)) + } + } + } + } } } |
