From 0d72c6c1a4107e88c19b1bc7d3cbd6c16619708d Mon Sep 17 00:00:00 2001
From: edde746 <86283021+edde746@users.noreply.github.com>
Date: Sat, 14 Feb 2026 20:19:27 +0100
Subject: [PATCH] fix: external player on Android 11+
---
android/app/src/main/AndroidManifest.xml | 5 +++
.../kotlin/com/edde746/plezy/MainActivity.kt | 24 +++++++++----
.../src/main/res/xml/file_provider_paths.xml | 7 ++++
lib/services/external_player_service.dart | 35 ++++++++-----------
4 files changed, 44 insertions(+), 27 deletions(-)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2b3ab39e..e1c79469 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -111,5 +111,10 @@
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt b/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt
index 578fcca5..7ea9bc96 100644
--- a/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/edde746/plezy/MainActivity.kt
@@ -82,25 +82,37 @@ class MainActivity : FlutterActivity() {
}
try {
- val uri: Uri = if (filePath.startsWith("content://")) {
- Uri.parse(filePath)
+ val uri: Uri
+ val grantRead: Boolean
+
+ if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
+ uri = Uri.parse(filePath)
+ grantRead = false
+ } else if (filePath.startsWith("content://")) {
+ uri = Uri.parse(filePath)
+ grantRead = true
} else {
- // Convert file path to content:// URI via FileProvider
val path = if (filePath.startsWith("file://")) filePath.removePrefix("file://") else filePath
- FileProvider.getUriForFile(this, "com.edde746.plezy.fileprovider", File(path))
+ uri = FileProvider.getUriForFile(this, "com.edde746.plezy.fileprovider", File(path))
+ grantRead = true
}
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "video/*")
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ if (grantRead) {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
if (packageName != null) {
setPackage(packageName)
}
}
startActivity(intent)
result.success(true)
+ } catch (e: android.content.ActivityNotFoundException) {
+ result.error("APP_NOT_FOUND", "No app found for package: $packageName", null)
} catch (e: Exception) {
- result.success(false)
+ result.error("LAUNCH_FAILED", e.message ?: e.javaClass.simpleName, null)
}
}
else -> result.notImplemented()
diff --git a/android/app/src/main/res/xml/file_provider_paths.xml b/android/app/src/main/res/xml/file_provider_paths.xml
index 2bd5b9a8..64d1dd8c 100644
--- a/android/app/src/main/res/xml/file_provider_paths.xml
+++ b/android/app/src/main/res/xml/file_provider_paths.xml
@@ -4,4 +4,11 @@
+
+
+
+
+
+
+
diff --git a/lib/services/external_player_service.dart b/lib/services/external_player_service.dart
index 5a9d7228..abea2b24 100644
--- a/lib/services/external_player_service.dart
+++ b/lib/services/external_player_service.dart
@@ -49,11 +49,9 @@ class ExternalPlayerService {
final settings = await SettingsService.getInstance();
final player = settings.getSelectedExternalPlayer();
- appLogger.d('Launching external player: ${player.name} with URL: $resolvedUrl');
-
- // On Android, use native intent for local files (file:// and content://)
- if (Platform.isAndroid && _isLocalUrl(resolvedUrl)) {
- return _launchAndroidLocalFile(resolvedUrl, player, context);
+ // On Android, always use native intent to avoid url_launcher opening in browser
+ if (Platform.isAndroid) {
+ return _launchAndroidNative(resolvedUrl, player, context);
}
final launched = await player.launch(resolvedUrl);
@@ -70,27 +68,22 @@ class ExternalPlayerService {
}
}
- static bool _isLocalUrl(String url) {
- return url.startsWith('file://') || url.startsWith('content://') || url.startsWith('/');
- }
-
- /// Launch a local video file on Android using native ACTION_VIEW intent
- /// with FileProvider content:// URI and FLAG_GRANT_READ_URI_PERMISSION.
- static Future _launchAndroidLocalFile(String url, ExternalPlayer player, BuildContext context) async {
+ /// Launch a video on Android using native ACTION_VIEW intent.
+ /// Handles local files (file://, content://, absolute paths) and remote URLs.
+ static Future _launchAndroidNative(String url, ExternalPlayer player, BuildContext context) async {
try {
- final filePath = url.startsWith('file://') ? url.substring(7) : url;
- final result = await _externalPlayerChannel.invokeMethod('openVideo', {
- 'filePath': filePath,
+ await _externalPlayerChannel.invokeMethod('openVideo', {
+ 'filePath': url,
if (player.id != 'system_default') 'package': _getAndroidPackage(player),
});
- final launched = result ?? false;
- if (!launched && context.mounted) {
+ return true;
+ } on PlatformException catch (e) {
+ if (e.code == 'APP_NOT_FOUND' && context.mounted) {
showErrorSnackBar(context, t.externalPlayer.appNotInstalled(name: player.name));
+ } else if (context.mounted) {
+ showErrorSnackBar(context, t.externalPlayer.launchFailed);
}
- return launched;
- } catch (e) {
- appLogger.w('Android native intent failed, falling back to player.launch', error: e);
- return player.launch(url);
+ return false;
}
}