mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-10-11 22:49:50 +00:00
Merge pull request #14889 from tom-pratt/local-network-permission
Request ACCESS_LOCAL_NETWORK permission on android 17 and up
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1063,4 +1063,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