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
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.bollwerk.shared.model.ResourceDto
|
||||
|
|
@ -67,16 +64,7 @@ internal fun ResourceDetailScreen(
|
|||
// Handle download success
|
||||
if (downloadState is DownloadState.Success) {
|
||||
LaunchedEffect(downloadState) {
|
||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", downloadState.file)
|
||||
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")
|
||||
}
|
||||
openResourceFile(context, downloadState.file, downloadState.mimeType, snackbarHostState)
|
||||
viewModel.clearDownloadState(guid)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
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.Box
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.bollwerk.shared.model.ResourceDto
|
||||
|
|
@ -112,7 +108,7 @@ internal fun ResourceListScreen(
|
|||
when (state) {
|
||||
is DownloadState.Success -> {
|
||||
LaunchedEffect(guid, state) {
|
||||
openFile(context, state.file, state.mimeType, snackbarHostState)
|
||||
openResourceFile(context, state.file, state.mimeType, snackbarHostState)
|
||||
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