fix(resources): grant explicit URI permissions to all resolved activities before startActivity
Resolves 'Keine App gefunden' for ePub and PDF opening failures (e.g. Firefox). FLAG_GRANT_READ_URI_PERMISSION alone is not sufficient when Android shows the Chooser – each potential app must receive grantUriPermission() before startActivity() is called. - Extract openResourceFile() into FileOpenHelper.kt (eliminates DRY violation) - Use PackageManager.ResolveInfoFlags (API 33+) with DEPRECATION suppress fallback - Remove duplicate inline intent code from ResourceDetailScreen Closes #129
This commit is contained in:
parent
96375cb9ea
commit
a84d130495
3 changed files with 58 additions and 35 deletions
|
|
@ -0,0 +1,55 @@
|
||||||
|
package de.bollwerk.app.ui.resources
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet eine Ressourcen-Datei in einer externen App via FileProvider.
|
||||||
|
*
|
||||||
|
* Erteilt vor dem `startActivity`-Aufruf explizit URI-Berechtigungen an alle Apps,
|
||||||
|
* die den Intent auflösen können – notwendig damit der Android-Chooser die Berechtigung
|
||||||
|
* korrekt weitergibt (z.B. Firefox für PDFs).
|
||||||
|
*/
|
||||||
|
internal suspend fun openResourceFile(
|
||||||
|
context: Context,
|
||||||
|
file: File,
|
||||||
|
mimeType: String,
|
||||||
|
snackbarHostState: SnackbarHostState
|
||||||
|
) {
|
||||||
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, mimeType)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explizit URI-Berechtigung an alle Apps vergeben, die diesen Intent auflösen können,
|
||||||
|
// damit der Android-Chooser die Berechtigung korrekt weitergibt (z.B. Firefox für PDFs).
|
||||||
|
val resolvedActivities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager.queryIntentActivities(
|
||||||
|
intent,
|
||||||
|
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
}
|
||||||
|
resolvedActivities.forEach { resolveInfo ->
|
||||||
|
context.grantUriPermission(
|
||||||
|
resolveInfo.activityInfo.packageName,
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
snackbarHostState.showSnackbar("Keine App zum Öffnen dieser Datei gefunden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package de.bollwerk.app.ui.resources
|
package de.bollwerk.app.ui.resources
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
|
@ -42,7 +40,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.bollwerk.shared.model.ResourceDto
|
import de.bollwerk.shared.model.ResourceDto
|
||||||
|
|
@ -67,16 +64,7 @@ internal fun ResourceDetailScreen(
|
||||||
// Handle download success
|
// Handle download success
|
||||||
if (downloadState is DownloadState.Success) {
|
if (downloadState is DownloadState.Success) {
|
||||||
LaunchedEffect(downloadState) {
|
LaunchedEffect(downloadState) {
|
||||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", downloadState.file)
|
openResourceFile(context, downloadState.file, downloadState.mimeType, snackbarHostState)
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(uri, downloadState.mimeType)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
context.startActivity(intent)
|
|
||||||
} catch (_: ActivityNotFoundException) {
|
|
||||||
snackbarHostState.showSnackbar("Keine App zum Öffnen dieser Datei gefunden")
|
|
||||||
}
|
|
||||||
viewModel.clearDownloadState(guid)
|
viewModel.clearDownloadState(guid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
package de.bollwerk.app.ui.resources
|
package de.bollwerk.app.ui.resources
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -61,7 +58,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.bollwerk.shared.model.ResourceDto
|
import de.bollwerk.shared.model.ResourceDto
|
||||||
|
|
@ -112,7 +108,7 @@ internal fun ResourceListScreen(
|
||||||
when (state) {
|
when (state) {
|
||||||
is DownloadState.Success -> {
|
is DownloadState.Success -> {
|
||||||
LaunchedEffect(guid, state) {
|
LaunchedEffect(guid, state) {
|
||||||
openFile(context, state.file, state.mimeType, snackbarHostState)
|
openResourceFile(context, state.file, state.mimeType, snackbarHostState)
|
||||||
viewModel.clearDownloadState(guid)
|
viewModel.clearDownloadState(guid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -519,20 +515,4 @@ private fun formatFileSize(bytes: Long): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun openFile(
|
|
||||||
context: Context,
|
|
||||||
file: File,
|
|
||||||
mimeType: String,
|
|
||||||
snackbarHostState: SnackbarHostState
|
|
||||||
) {
|
|
||||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(uri, mimeType)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
context.startActivity(intent)
|
|
||||||
} catch (_: ActivityNotFoundException) {
|
|
||||||
snackbarHostState.showSnackbar("Keine App zum Öffnen dieser Datei gefunden")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue