mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-10-19 09:08:59 +02:00
Request ACCESS_LOCAL_NETWORK permission on android 17 and up
Show the permission dialog the first time host or connect is clicked. While the permission is not granted, show an info box at the bottom of the netplay setup screen explaining the need for the permission with a button to grant permission. Launch the app settings menu where the permission can be granted manually if the user has denied twice previously preventing us from showing the permission dialog. Workarounds using the shouldShowRationale flag and detecting if the dialog showed via onPause to account for well known limitations in the android permissions API.
This commit is contained in:
1 parent
5b295d5587
commit
b5ded04314
5 files changed
+138
-5
No files matched your search
@@ -170,6 +170,8 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.tooling)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
|
||||
implementation(libs.accompanist)
|
||||
}
|
||||
|
||||
fun getGitVersion(): String {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK"/>
|
||||
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
|
||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
+131
-5
@@ -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(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSpacer()
|
||||
@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) }
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1062,4 +1062,6 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<string name="netplay_address_share">Share address</string>
|
||||
<string name="netplay_address_retry">Retry</string>
|
||||
<string name="netplay_transfer_progress">%1$s/%2$s MiB</string>
|
||||
<string name="netplay_local_network_permission_info">Dolphin needs permission to connect to devices on a local network. If you only want to use netplay over the internet, the permission is not required.</string>
|
||||
<string name="netplay_give_permission">Give permission</string>
|
||||
</resources>
|
||||
@@ -26,6 +26,7 @@ slidingpanelayout = "1.2.0"
|
||||
swiperefreshlayout = "1.2.0"
|
||||
tvprovider = "1.1.0"
|
||||
uiautomator = "2.4.0"
|
||||
accompanist = "0.37.3"
|
||||
|
||||
[libraries]
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose" }
|
||||
@@ -64,6 +65,7 @@ filepicker = { group = "com.nononsenseapps", name = "filepicker", version.ref =
|
||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
accompanist = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in new issue
Block a user