diff --git a/.gitignore b/.gitignore index 7608eac..2ee3bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,8 @@ android/.gradle/ android/local.properties android/app/google-services.json android/key.properties -android/app/src/main/java/io/flutter/plugins/ +# android/app/src/main/java/io/flutter/plugins/ intentionally tracked so that +# GeneratedPluginRegistrant.java (catch Throwable) is committed and used by CI. .android/ Android/ .gradle/ diff --git a/android/.gitignore b/android/.gitignore index be3943c..2b6a400 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -4,7 +4,6 @@ gradle-wrapper.jar /gradlew /gradlew.bat /local.properties -GeneratedPluginRegistrant.java .cxx/ # Remember to never publicly share your keystore. diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..d086c3a --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,84 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new com.mr.flutter.plugin.filepicker.FilePickerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin file_picker, com.mr.flutter.plugin.filepicker.FilePickerPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_local_notifications, com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_secure_storage, com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.flutter.plugins.integration_test.IntegrationTestPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin integration_test, dev.flutter.plugins.integration_test.IntegrationTestPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.steenbakker.mobile_scanner.MobileScannerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin mobile_scanner, dev.steenbakker.mobile_scanner.MobileScannerPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.crazecoder.openfile.OpenFilePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin open_filex, com.crazecoder.openfile.OpenFilePlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.share.SharePlusPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin share_plus, dev.fluttercommunity.plus.share.SharePlusPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.webviewflutter.WebViewFlutterPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin webview_flutter_android, io.flutter.plugins.webviewflutter.WebViewFlutterPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.workmanager.WorkmanagerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin workmanager_android, dev.fluttercommunity.workmanager.WorkmanagerPlugin", e); + } + } +} diff --git a/lib/ui/screens/about_screen.dart b/lib/ui/screens/about_screen.dart index 35c42dc..9252c8f 100644 --- a/lib/ui/screens/about_screen.dart +++ b/lib/ui/screens/about_screen.dart @@ -95,6 +95,30 @@ class _AboutScreenState extends ConsumerState { } } + Future _launchUrl(BuildContext context, Uri url) async { + try { + final launched = + await launchUrl(url, mode: LaunchMode.externalApplication); + if (!launched && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + duration: Duration(seconds: 5), + content: Text('Could not open browser.'), + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + content: Text('Error: $e'), + ), + ); + } + } + } + Future _createIssue( BuildContext context, int imapCount, @@ -167,10 +191,7 @@ class _AboutScreenState extends ConsumerState { onTapLink: (text, href, title) { if (href != null) { unawaited( - launchUrl( - Uri.parse(href), - mode: LaunchMode.externalApplication, - ), + _launchUrl(context, Uri.parse(href)), ); } }, diff --git a/pubspec.lock b/pubspec.lock index 7edea8a..2be3b42 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1117,13 +1117,13 @@ packages: source: hosted version: "6.3.2" url_launcher_android: - dependency: transitive + dependency: "direct overridden" description: name: url_launcher_android - sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" + sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9" url: "https://pub.dev" source: hosted - version: "6.3.30" + version: "6.3.24" url_launcher_ios: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 17ecabb..e1e977b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,3 +89,7 @@ dependency_overrides: # (SIGSEGV in libdartjni.so FindClassUnchecked). Pin to 2.2.20 which uses # stable Pigeon and is known to work reliably. path_provider_android: ">=2.2.0 <2.2.21" + # url_launcher_android 6.3.25 updated to Pigeon 26, which causes a + # channel-error on launchUrl on some Android devices (same root cause as + # path_provider_android). Pin to <6.3.25 which uses stable Pigeon. + url_launcher_android: ">=6.3.0 <6.3.25" diff --git a/test/widget/about_screen_test.dart b/test/widget/about_screen_test.dart index f1814ca..6782fae 100644 --- a/test/widget/about_screen_test.dart +++ b/test/widget/about_screen_test.dart @@ -27,6 +27,22 @@ class MockUrlLauncher extends Mock } } +class ThrowingUrlLauncher extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform { + @override + Future canLaunch(String? url) async => true; + + @override + Future launchUrl(String? url, LaunchOptions? options) async { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: ' + '"dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl".', + ); + } +} + Widget _buildScreen({List accounts = const []}) { return ProviderScope( overrides: [ @@ -180,4 +196,24 @@ void main() { ); expect(mock.launchedUrl, contains('1.2.3%2B99')); }); + + testWidgets( + 'AboutScreen link tap with failed url_launcher shows error snackbar', + (tester) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + UrlLauncherPlatform.instance = ThrowingUrlLauncher(); + + await tester.pumpWidget(_buildScreen()); + await tester.pumpAndSettle(); + + await tester.tap(find.textContaining('sharedinbox.de').first); + await tester.pumpAndSettle(); + + expect(find.textContaining('Error:'), findsOneWidget); + }, + ); }