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; } }