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