From 51242be72bd655b9df3d171256367030992f4d31 Mon Sep 17 00:00:00 2001 From: gd Date: Mon, 3 Feb 2025 22:01:16 +0200 Subject: [PATCH] android: changed permissions handling UI in status screen when show rationale is false Android will ignore permission request and will not show the request dialog if the user's action implies "don't ask again." This leaves the app in a crippled state and the user confused. Google says "don't try to convince the user", so it returns false for `shouldShowRequestPermissionRationale`. To help the user proceed, we show the `Request permission` button only if `shouldShowRequestPermissionRationale == true` because there's a good chance the premission request dialog will not be ignored. If `shouldShowRequestPermissionRationale == false` we instead show the "rationale" message and a button to open the app info dialog where the user can explicitly grand the permission. --- android/README.md | 30 ++++++ .../main/java/org/musicpd/ui/StatusScreen.kt | 101 +++++++++++------- .../java/org/musicpd/utils/IntentUtils.kt | 30 ++++++ android/app/src/main/res/values/strings.xml | 1 + 4 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 android/app/src/main/java/org/musicpd/utils/IntentUtils.kt diff --git a/android/README.md b/android/README.md index edef6a103..371a0a1b6 100644 --- a/android/README.md +++ b/android/README.md @@ -6,6 +6,9 @@ Notes and resources for MPD android maintainers. ### Version control + +git ignoring .idea directory completely until a good reason emerges not to + * [How to manage projects under Version Control Systems (jetbrains.com)](https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) * [gradle.xml should work like workspace.xml? (jetbrains.com)](https://youtrack.jetbrains.com/issue/IDEA-55923) @@ -15,3 +18,30 @@ Notes and resources for MPD android maintainers. * [Include prebuilt native libraries (developer.android.com)](https://developer.android.com/studio/projects/gradle-external-native-builds#jniLibs) +### Permissions + +#### Files access + +The required permission depends on android SDK version: + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + Manifest.permission.READ_MEDIA_AUDIO + else + Manifest.permission.READ_EXTERNAL_STORAGE + +#### Permission request + +[Request runtime permissions](https://developer.android.com/training/permissions/requesting) + +Since Android 6.0 (API level 23): + +Android will ignore permission request and will not show the request dialog +if the user's action implies "don't ask again." +This leaves the app in a crippled state and the user confused. +Google says "don't try to convince the user", so it returns false for `shouldShowRequestPermissionRationale`. + +To help the user proceed, we show the `Request permission` button only if `shouldShowRequestPermissionRationale == true` +because there's a good chance the permission request dialog will not be ignored. + +If `shouldShowRequestPermissionRationale == false` we instead show the "rationale" message and a button to open +the app info dialog where the user can explicitly grand the permission. \ No newline at end of file diff --git a/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt b/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt index 8b24ec143..b913c57b5 100644 --- a/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt +++ b/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt @@ -1,6 +1,7 @@ package org.musicpd.ui import android.Manifest +import android.os.Build import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -24,54 +25,36 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp 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 org.musicpd.R +import org.musicpd.utils.openAppSettings @OptIn(ExperimentalPermissionsApi::class) @Composable fun StatusScreen(settingsViewModel: SettingsViewModel) { - val storagePermissionState = rememberPermissionState( - Manifest.permission.READ_EXTERNAL_STORAGE - ) - - if (storagePermissionState.status.shouldShowRationale) { - Column( - Modifier - .padding(4.dp) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text(stringResource(id = R.string.external_files_permission_request)) - Button(onClick = { }) { - Text("Request permission") - } - } + val storagePermissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState( + Manifest.permission.READ_MEDIA_AUDIO + ) } else { - Column( - Modifier - .padding(4.dp) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - NetworkAddress() - ServerStatus(settingsViewModel) - if (!storagePermissionState.status.isGranted) { - OutlinedButton( - onClick = { storagePermissionState.launchPermissionRequest() }, Modifier - .padding(4.dp) - .fillMaxWidth() - ) { - Text( - "Request external storage permission", - color = MaterialTheme.colorScheme.secondary - ) - } - } - } + rememberPermissionState( + Manifest.permission.READ_EXTERNAL_STORAGE + ) + } + + Column( + Modifier + .padding(4.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + NetworkAddress() + ServerStatus(settingsViewModel) + AudioMediaPermission(storagePermissionState) } } @@ -116,4 +99,44 @@ fun ServerStatus(settingsViewModel: SettingsViewModel) { Text(text = statusUiState.statusMessage) } } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun AudioMediaPermission(storagePermissionState: PermissionState) { + val permissionStatus = storagePermissionState.status + if (!permissionStatus.isGranted) { + val context = LocalContext.current + Column( + Modifier + .padding(4.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + stringResource(id = R.string.external_files_permission_request), + Modifier.padding(16.dp) + ) + if (storagePermissionState.status.shouldShowRationale) { + Button(onClick = { + storagePermissionState.launchPermissionRequest() + }) { + Text("Request permission") + } + } else { + OutlinedButton( + onClick = { + openAppSettings(context, context.packageName) + }, + Modifier.padding(16.dp) + ) { + Text( + stringResource(id = R.string.title_open_app_info), + color = MaterialTheme.colorScheme.secondary + ) + } + } + } + } } \ No newline at end of file diff --git a/android/app/src/main/java/org/musicpd/utils/IntentUtils.kt b/android/app/src/main/java/org/musicpd/utils/IntentUtils.kt new file mode 100644 index 000000000..806ef51a4 --- /dev/null +++ b/android/app/src/main/java/org/musicpd/utils/IntentUtils.kt @@ -0,0 +1,30 @@ +package org.musicpd.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import android.util.Log + +private const val TAG = "IntentUtils" + +fun openAppSettings( + context: Context, + packageName: String +) { + try { + context.startActivity(Intent().apply { + setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + setData(Uri.parse("package:$packageName")) + addCategory(Intent.CATEGORY_DEFAULT) + addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + }) + } catch (e: ActivityNotFoundException) { + Log.e( + TAG, + "failed to open app settings for package: $packageName", e + ) + } +} diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 92f8139b2..32877eb03 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -10,4 +10,5 @@ Prevent suspend when MPD is running (Wakelock) Pause MPD when headphones disconnect MPD requires access to external files to play local music. Please grant the permission. + Open app info